Repository: rust-lang/rust-analyzer
Pull request #17549 (2024-07-06 18:21:08)

fix: Fix runnables being incorrectly constructed
I've misunderstood parts of the code here which caused runnables to arbitrarily break :) (I have yet to understand the conditions that made them break though, there is some odd caching involved I feel like ...)
Fixes https://github.com/rust-lang/rust-analyzer/issues/17402

Veykril: @bors r+
bors: :pushpin: Commit 86a4844f28f932a8a207e17c9d1e1ff901a162ad has been approved by `Veykril`

It is now in the [queue](https://bors.rust-lang.org/homu/queue/rust-analyzer) for this repository.

<!-- @bors r=Veykril 86a4844f28f932a8a207e17c9d1e1ff901a162ad -->
<!-- homu: {"type":"Approved","sha":"86a4844f28f932a8a207e17c9d1e1ff901a162ad","approver":"Veykril","queue":"https://bors.rust-lang.org/homu/queue/rust-analyzer"} -->
bors: :hourglass: Testing commit 86a4844f28f932a8a207e17c9d1e1ff901a162ad with merge 1833fe3c1be68f69a72dbef41430616b04607be0...
<!-- homu: {"type":"BuildStarted","head_sha":"86a4844f28f932a8a207e17c9d1e1ff901a162ad","merge_sha":"1833fe3c1be68f69a72dbef41430616b04607be0"} -->

Patch:
diff --git a/crates/hir-ty/src/infer/expr.rs b/crates/hir-ty/src/infer/expr.rs
index 7a0f7872a6..3647243537 100644
--- a/crates/hir-ty/src/infer/expr.rs
+++ b/crates/hir-ty/src/infer/expr.rs
@@ -440,8 +440,7 @@ impl InferenceContext<'_> {
                 let ty = match self.infer_path(p, tgt_expr.into()) {
                     Some(ty) => ty,
                     None => {
-                        if matches!(p, Path::Normal { mod_path, .. } if mod_path.is_ident() || mod_path.is_self())
-                        {
+                        if matches!(p, Path::Normal { mod_path, .. } if mod_path.is_ident()) {
                             self.push_diagnostic(InferenceDiagnostic::UnresolvedIdent {
                                 expr: tgt_expr,
                             });
diff --git a/crates/ide-diagnostics/src/handlers/unresolved_ident.rs b/crates/ide-diagnostics/src/handlers/unresolved_ident.rs
index 9a81682aae..7aa3e16536 100644
--- a/crates/ide-diagnostics/src/handlers/unresolved_ident.rs
+++ b/crates/ide-diagnostics/src/handlers/unresolved_ident.rs
@@ -53,22 +53,6 @@ fn main() {
     let x = 5;
     let _ = x;
 }
-"#,
-        );
-    }
-
-    #[test]
-    fn unresolved_self_val() {
-        check_diagnostics(
-            r#"
-fn main() {
-    self.a;
-  //^^^^ error: no such value in this scope
-    let self:
-         self =
-            self;
-          //^^^^ error: no such value in this scope
-}
 "#,
         );
     }
diff --git a/editors/code/package.json b/editors/code/package.json
index 0730610a84..0ad5e20af7 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -305,16 +305,6 @@
                 "command": "rust-analyzer.toggleLSPLogs",
                 "title": "Toggle LSP Logs",
                 "category": "rust-analyzer"
-            },
-            {
-                "command": "rust-analyzer.openWalkthrough",
-                "title": "Open Walkthrough",
-                "category": "rust-analyzer"
-            },
-            {
-                "command": "rust-analyzer.openFAQ",
-                "title": "Open FAQ",
-                "category": "rust-analyzer"
             }
         ],
         "keybindings": [
@@ -333,14 +323,6 @@
             {
                 "title": "general",
                 "properties": {
-                    "rust-analyzer.cargoRunner": {
-                        "type": [
-                            "null",
-                            "string"
-                        ],
-                        "default": null,
-                        "description": "Custom cargo runner extension ID."
-                    },
                     "rust-analyzer.restartServerOnConfigChange": {
                         "markdownDescription": "Whether to restart the server automatically when certain settings that require a restart are changed.",
                         "default": false,
@@ -3152,14 +3134,6 @@
                 {
                     "command": "rust-analyzer.toggleLSPLogs",
                     "when": "inRustProject"
-                },
-                {
-                    "command": "rust-analyzer.openWalkthrough",
-                    "when": "inRustProject"
-                },
-                {
-                    "command": "rust-analyzer.openFAQ",
-                    "when": "inRustProject"
                 }
             ],
             "editor/context": [
@@ -3189,94 +3163,6 @@
                 "fileMatch": "rust-project.json",
                 "url": "https://json.schemastore.org/rust-project.json"
             }
-        ],
-        "walkthroughs": [
-            {
-                "id": "landing",
-                "title": "Learn about rust-analyzer",
-                "description": "A brief introduction to get started with rust-analyzer. Learn about key features and resources to help you get the most out of the extension.",
-                "steps": [
-                    {
-                        "id": "docs",
-                        "title": "Visit the docs!",
-                        "description": "Confused about configurations? Want to learn more about rust-analyzer? Visit the [User Manual](https://rust-analyzer.github.io/manual.html)!",
-                        "media": {
-                            "image": "./icon.png",
-                            "altText": "rust-analyzer logo"
-                        },
-                        "completionEvents": [
-                            "onLink:https://rust-analyzer.github.io/manual.html"
-                        ]
-                    },
-                    {
-                        "id": "faq",
-                        "title": "FAQ",
-                        "description": "Have questions about rust-analyzer? Check out the [FAQ Walkthrough](command:rust-analyzer.openFAQ)!",
-                        "media": {
-                            "image": "icon.png",
-                            "altText": "rust-analyzer logo"
-                        }
-                    },
-                    {
-                        "id": "changelog",
-                        "title": "Changelog",
-                        "description": "Stay up-to-date with the latest changes in rust-analyzer. Check out the changelog [here](https://rust-analyzer.github.io/thisweek)!",
-                        "media": {
-                            "image": "icon.png",
-                            "altText": "rust-analyzer logo"
-                        },
-                        "completionEvents": [
-                            "onLink:https://rust-analyzer.github.io/thisweek"
-                        ]
-                    },
-                    {
-                        "id": "revisit",
-                        "title": "Want to revisit a walkthrough?",
-                        "description": "Use the ``Welcome: Open Walkthrough`` command to revisit any walkthrough!",
-                        "media": {
-                            "image": "icon.png",
-                            "altText": "rust-analyzer logo"
-                        }
-                    }
-                ]
-            },
-            {
-                "id": "faq",
-                "title": "FAQ",
-                "description": "Here are some frequently asked questions about rust-analyzer.",
-                "steps": [
-                    {
-                        "id": "faq1",
-                        "title": "What is rust-analyzer?",
-                        "description": "rust-analyzer is a language server for Rust. It provides features like code completion, find references, and more.",
-                        "media": {
-                            "image": "icon.png",
-                            "altText": "rust-analyzer logo"
-                        }
-                    },
-                    {
-                        "id": "faq2",
-                        "title": "Why are all of these type hints showing up in my code?",
-                        "description": "By default, rust-analyzer displays __inlay hints__ to help you understand your code better. You can disable them in your settings.json file with ``\"editor.inlayHints.enabled\": \"off\"``",
-                        "media": {
-                            "image": "icon.png",
-                            "altText": "rust-analyzer logo"
-                        }
-                    },
-                    {
-                        "id": "faq3",
-                        "title": "Where can I find more information about rust-analyzer?",
-                        "description": "You can find more information about rust-analyzer in the [User Manual](https://rust-analyzer.github.io/manual.html).",
-                        "media": {
-                            "image": "icon.png",
-                            "altText": "rust-analyzer logo"
-                        },
-                        "completionEvents": [
-                            "onLink:https://rust-analyzer.github.io/manual.html"
-                        ]
-                    }
-                ]
-            }
         ]
     }
 }
diff --git a/editors/code/src/bootstrap.ts b/editors/code/src/bootstrap.ts
index 5a92b285ae..f2884ad0b0 100644
--- a/editors/code/src/bootstrap.ts
+++ b/editors/code/src/bootstrap.ts
@@ -36,6 +36,12 @@ async function getServer(
     config: Config,
     state: PersistentState,
 ): Promise<string | undefined> {
+    const packageJson: {
+        version: string;
+        releaseTag: string | null;
+        enableProposedApi: boolean | undefined;
+    } = context.extension.packageJSON;
+
     const explicitPath = process.env["__RA_LSP_SERVER_DEBUG"] ?? config.serverPath;
     if (explicitPath) {
         if (explicitPath.startsWith("~/")) {
@@ -43,7 +49,7 @@ async function getServer(
         }
         return explicitPath;
     }
-    if (config.package.releaseTag === null) return "rust-analyzer";
+    if (packageJson.releaseTag === null) return "rust-analyzer";
 
     const ext = process.platform === "win32" ? ".exe" : "";
     const bundled = vscode.Uri.joinPath(context.extensionUri, "server", `rust-analyzer${ext}`);
@@ -54,8 +60,15 @@ async function getServer(
     if (bundledExists) {
         let server = bundled;
         if (await isNixOs()) {
-            server = await getNixOsServer(config, ext, state, bundled, server);
-            await state.updateServerVersion(config.package.version);
+            server = await getNixOsServer(
+                context.globalStorageUri,
+                packageJson.version,
+                ext,
+                state,
+                bundled,
+                server,
+            );
+            await state.updateServerVersion(packageJson.version);
         }
         return server.fsPath;
     }
@@ -86,19 +99,20 @@ export function isValidExecutable(path: string, extraEnv: Env): boolean {
 }
 
 async function getNixOsServer(
-    config: Config,
+    globalStorageUri: vscode.Uri,
+    version: string,
     ext: string,
     state: PersistentState,
     bundled: vscode.Uri,
     server: vscode.Uri,
 ) {
-    await vscode.workspace.fs.createDirectory(config.globalStorageUri).then();
-    const dest = vscode.Uri.joinPath(config.globalStorageUri, `rust-analyzer${ext}`);
+    await vscode.workspace.fs.createDirectory(globalStorageUri).then();
+    const dest = vscode.Uri.joinPath(globalStorageUri, `rust-analyzer${ext}`);
     let exists = await vscode.workspace.fs.stat(dest).then(
         () => true,
         () => false,
     );
-    if (exists && config.package.version !== state.serverVersion) {
+    if (exists && version !== state.serverVersion) {
         await vscode.workspace.fs.delete(dest);
         exists = false;
     }
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 2b0b300106..f0f9fab1c6 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -1502,23 +1502,3 @@ export function toggleLSPLogs(ctx: Ctx): Cmd {
         }
     };
 }
-
-export function openWalkthrough(_: Ctx): Cmd {
-    return async () => {
-        await vscode.commands.executeCommand(
-            "workbench.action.openWalkthrough",
-            "rust-lang.rust-analyzer#landing",
-            false,
-        );
-    };
-}
-
-export function openFAQ(_: Ctx): Cmd {
-    return async () => {
-        await vscode.commands.executeCommand(
-            "workbench.action.openWalkthrough",
-            "rust-lang.rust-analyzer#faq",
-            true,
-        );
-    };
-}
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index 527fad944f..ca77215004 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -4,6 +4,7 @@ import * as path from "path";
 import * as vscode from "vscode";
 import { type Env, log, unwrapUndefinable, expectNotUndefined } from "./util";
 import type { JsonProject } from "./rust_project";
+import type { Disposable } from "./ctx";
 
 export type RunnableEnvCfgItem = {
     mask?: string;
@@ -29,22 +30,9 @@ export class Config {
         (opt) => `${this.rootSection}.${opt}`,
     );
 
-    readonly package: {
-        version: string;
-        releaseTag: string | null;
-        enableProposedApi: boolean | undefined;
-    } = vscode.extensions.getExtension(this.extensionId)!.packageJSON;
-
-    readonly globalStorageUri: vscode.Uri;
-
-    constructor(ctx: vscode.ExtensionContext) {
-        this.globalStorageUri = ctx.globalStorageUri;
+    constructor(disposables: Disposable[]) {
         this.discoveredWorkspaces = [];
-        vscode.workspace.onDidChangeConfiguration(
-            this.onDidChangeConfiguration,
-            this,
-            ctx.subscriptions,
-        );
+        vscode.workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, disposables);
         this.refreshLogging();
         this.configureLanguage();
     }
@@ -55,7 +43,10 @@ export class Config {
 
     private refreshLogging() {
         log.setEnabled(this.traceExtension ?? false);
-        log.info("Extension version:", this.package.version);
+        log.info(
+            "Extension version:",
+            vscode.extensions.getExtension(this.extensionId)!.packageJSON.version,
+        );
 
         const cfg = Object.entries(this.cfg).filter(([_, val]) => !(val instanceof Function));
         log.info("Using configuration", Object.fromEntries(cfg));
@@ -277,10 +268,6 @@ export class Config {
         return this.get<string[]>("runnables.problemMatcher") || [];
     }
 
-    get cargoRunner() {
-        return this.get<string | undefined>("cargoRunner");
-    }
-
     get testExplorer() {
         return this.get<boolean | undefined>("testExplorer");
     }
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
index bf0b84ec35..caa99d7619 100644
--- a/editors/code/src/ctx.ts
+++ b/editors/code/src/ctx.ts
@@ -118,7 +118,7 @@ export class Ctx implements RustAnalyzerExtensionApi {
         extCtx.subscriptions.push(this);
         this.version = extCtx.extension.packageJSON.version ?? "<unknown>";
         this._serverVersion = "<not running>";
-        this.config = new Config(extCtx);
+        this.config = new Config(extCtx.subscriptions);
         this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
         if (this.config.testExplorer) {
             this.testController = vscode.tests.createTestController(
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index c96f2ae869..ff67bb7bd5 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -178,8 +178,6 @@ function createCommands(): Record<string, CommandFactory> {
         viewMemoryLayout: { enabled: commands.viewMemoryLayout },
         toggleCheckOnSave: { enabled: commands.toggleCheckOnSave },
         toggleLSPLogs: { enabled: commands.toggleLSPLogs },
-        openWalkthrough: { enabled: commands.openWalkthrough },
-        openFAQ: { enabled: commands.openFAQ },
         // Internal commands which are invoked by the server.
         applyActionGroup: { enabled: commands.applyActionGroup },
         applySnippetWorkspaceEdit: { enabled: commands.applySnippetWorkspaceEditCommand },
diff --git a/editors/code/src/run.ts b/editors/code/src/run.ts
index 583f803d9a..783bbc1607 100644
--- a/editors/code/src/run.ts
+++ b/editors/code/src/run.ts
@@ -111,26 +111,31 @@ export async function createTaskFromRunnable(
     runnable: ra.Runnable,
     config: Config,
 ): Promise<vscode.Task> {
-    let definition: tasks.RustTargetDefinition;
+    const target = vscode.workspace.workspaceFolders?.[0];
+
+    let definition: tasks.TaskDefinition;
+    let options;
+    let cargo;
     if (runnable.kind === "cargo") {
         const runnableArgs = runnable.args;
         let args = createCargoArgs(runnableArgs);
 
-        let program: string;
         if (runnableArgs.overrideCargo) {
             // Split on spaces to allow overrides like "wrapper cargo".
             const cargoParts = runnableArgs.overrideCargo.split(" ");
 
-            program = unwrapUndefinable(cargoParts[0]);
+            cargo = unwrapUndefinable(cargoParts[0]);
             args = [...cargoParts.slice(1), ...args];
         } else {
-            program = await toolchain.cargoPath();
+            cargo = await toolchain.cargoPath();
         }
 
         definition = {
             type: tasks.CARGO_TASK_TYPE,
-            command: program,
-            args,
+            command: unwrapUndefinable(args[0]),
+            args: args.slice(1),
+        };
+        options = {
             cwd: runnableArgs.workspaceRoot || ".",
             env: prepareEnv(runnable.label, runnableArgs, config.runnablesExtraEnv),
         };
@@ -140,13 +145,14 @@ export async function createTaskFromRunnable(
             type: tasks.SHELL_TASK_TYPE,
             command: runnableArgs.program,
             args: runnableArgs.args,
+        };
+        options = {
             cwd: runnableArgs.cwd,
             env: prepareBaseEnv(),
         };
     }
 
-    const target = vscode.workspace.workspaceFolders?.[0];
-    const exec = await tasks.targetToExecution(definition, config.cargoRunner, true);
+    const exec = await tasks.targetToExecution(definition, options, cargo);
     const task = await tasks.buildRustTask(
         target,
         definition,
diff --git a/editors/code/src/tasks.ts b/editors/code/src/tasks.ts
index 6f4fbf9188..fac1cc6394 100644
--- a/editors/code/src/tasks.ts
+++ b/editors/code/src/tasks.ts
@@ -1,6 +1,5 @@
 import * as vscode from "vscode";
 import type { Config } from "./config";
-import { log, unwrapUndefinable } from "./util";
 import * as toolchain from "./toolchain";
 
 // This ends up as the `type` key in tasks.json. RLS also uses `cargo` and
@@ -10,21 +9,21 @@ export const SHELL_TASK_TYPE = "shell";
 
 export const RUST_TASK_SOURCE = "rust";
 
-export type RustTargetDefinition = {
+export type TaskDefinition = vscode.TaskDefinition & {
     readonly type: typeof CARGO_TASK_TYPE | typeof SHELL_TASK_TYPE;
-} & vscode.TaskDefinition &
-    RustTarget;
-export type RustTarget = {
-    // The command to run, usually `cargo`.
-    command: string;
-    // Additional arguments passed to the command.
     args?: string[];
-    // The working directory to run the command in.
-    cwd?: string;
-    // The shell environment.
-    env?: { [key: string]: string };
+    command: string;
 };
 
+export type CargoTaskDefinition = {
+    env?: Record<string, string>;
+    type: typeof CARGO_TASK_TYPE;
+} & TaskDefinition;
+
+function isCargoTask(definition: vscode.TaskDefinition): definition is CargoTaskDefinition {
+    return definition.type === CARGO_TASK_TYPE;
+}
+
 class RustTaskProvider implements vscode.TaskProvider {
     private readonly config: Config;
 
@@ -58,13 +57,13 @@ class RustTaskProvider implements vscode.TaskProvider {
         for (const workspaceTarget of vscode.workspace.workspaceFolders) {
             for (const def of defs) {
                 const definition = {
-                    command: cargo,
-                    args: [def.command],
-                };
-                const exec = await targetToExecution(definition, this.config.cargoRunner);
+                    command: def.command,
+                    type: CARGO_TASK_TYPE,
+                } as const;
+                const exec = await targetToExecution(definition, {}, cargo);
                 const vscodeTask = await buildRustTask(
                     workspaceTarget,
-                    { ...definition, type: CARGO_TASK_TYPE },
+                    definition,
                     `cargo ${def.command}`,
                     this.config.problemMatcher,
                     exec,
@@ -81,23 +80,13 @@ class RustTaskProvider implements vscode.TaskProvider {
         // VSCode calls this for every cargo task in the user's tasks.json,
         // we need to inform VSCode how to execute that command by creating
         // a ShellExecution for it.
-        if (task.definition.type === CARGO_TASK_TYPE) {
-            const taskDefinition = task.definition as RustTargetDefinition;
-            const cargo = await toolchain.cargoPath();
-            const exec = await targetToExecution(
-                {
-                    command: cargo,
-                    args: [taskDefinition.command].concat(taskDefinition.args || []),
-                    cwd: taskDefinition.cwd,
-                    env: taskDefinition.env,
-                },
-                this.config.cargoRunner,
-            );
-            return await buildRustTask(
+        if (isCargoTask(task.definition)) {
+            const exec = await targetToExecution(task.definition, { env: task.definition.env });
+            return buildRustTask(
                 task.scope,
-                taskDefinition,
+                task.definition,
                 task.name,
-                this.config.problemMatcher,
+                task.problemMatchers,
                 exec,
             );
         }
@@ -108,7 +97,7 @@ class RustTaskProvider implements vscode.TaskProvider {
 
 export async function buildRustTask(
     scope: vscode.WorkspaceFolder | vscode.TaskScope | undefined,
-    definition: RustTargetDefinition,
+    definition: TaskDefinition,
     name: string,
     problemMatcher: string[],
     exec: vscode.ProcessExecution | vscode.ShellExecution,
@@ -126,40 +115,23 @@ export async function buildRustTask(
 }
 
 export async function targetToExecution(
-    definition: RustTarget,
-    customRunner?: string,
-    throwOnError: boolean = false,
+    definition: TaskDefinition,
+    options?: {
+        env?: { [key: string]: string };
+        cwd?: string;
+    },
+    cargo?: string,
 ): Promise<vscode.ProcessExecution | vscode.ShellExecution> {
-    if (customRunner) {
-        const runnerCommand = `${customRunner}.buildShellExecution`;
-
-        try {
-            const runnerArgs = {
-                kind: CARGO_TASK_TYPE,
-                args: definition.args,
-                cwd: definition.cwd,
-                env: definition.env,
-            };
-            const customExec = await vscode.commands.executeCommand(runnerCommand, runnerArgs);
-            if (customExec) {
-                if (customExec instanceof vscode.ShellExecution) {
-                    return customExec;
-                } else {
-                    log.debug("Invalid cargo ShellExecution", customExec);
-                    throw "Invalid cargo ShellExecution.";
-                }
-            }
-            // fallback to default processing
-        } catch (e) {
-            if (throwOnError) throw `Cargo runner '${customRunner}' failed! ${e}`;
-            // fallback to default processing
-        }
+    let command, args;
+    if (isCargoTask(definition)) {
+        // FIXME: The server should provide cargo
+        command = cargo || (await toolchain.cargoPath());
+        args = [definition.command].concat(definition.args || []);
+    } else {
+        command = definition.command;
+        args = definition.args || [];
     }
-    const args = unwrapUndefinable(definition.args);
-    return new vscode.ProcessExecution(definition.command, args, {
-        cwd: definition.cwd,
-        env: definition.env,
-    });
+    return new vscode.ProcessExecution(command, args, options);
 }
 
 export function activateTaskProvider(config: Config): vscode.Disposable {


Test patch:
diff --git a/editors/code/tests/unit/settings.test.ts b/editors/code/tests/unit/settings.test.ts
index bafb9569a1..84501dde6c 100644
--- a/editors/code/tests/unit/settings.test.ts
+++ b/editors/code/tests/unit/settings.test.ts
@@ -13,7 +13,7 @@ export async function getTests(ctx: Context) {
                 USING_MY_VAR: "test test test",
                 MY_VAR: "test",
             };
-            const actualEnv = await substituteVariablesInEnv(envJson);
+            const actualEnv = substituteVariablesInEnv(envJson);
             assert.deepStrictEqual(actualEnv, expectedEnv);
         });
 
@@ -34,7 +34,7 @@ export async function getTests(ctx: Context) {
                 E_IS_ISOLATED: "test",
                 F_USES_E: "test",
             };
-            const actualEnv = await substituteVariablesInEnv(envJson);
+            const actualEnv = substituteVariablesInEnv(envJson);
             assert.deepStrictEqual(actualEnv, expectedEnv);
         });
 
@@ -47,7 +47,7 @@ export async function getTests(ctx: Context) {
                 USING_EXTERNAL_VAR: "test test test",
             };
 
-            const actualEnv = await substituteVariablesInEnv(envJson);
+            const actualEnv = substituteVariablesInEnv(envJson);
             assert.deepStrictEqual(actualEnv, expectedEnv);
             delete process.env["TEST_VARIABLE"];
         });
@@ -56,7 +56,7 @@ export async function getTests(ctx: Context) {
             const envJson = {
                 USING_VSCODE_VAR: "${workspaceFolderBasename}",
             };
-            const actualEnv = await substituteVariablesInEnv(envJson);
+            const actualEnv = substituteVariablesInEnv(envJson);
             assert.deepStrictEqual(actualEnv["USING_VSCODE_VAR"], "code");
         });
     });
diff --git a/editors/code/tests/unit/tasks.test.ts b/editors/code/tests/unit/tasks.test.ts
new file mode 100644
index 0000000000..9bccaaf3d4
--- /dev/null
+++ b/editors/code/tests/unit/tasks.test.ts
@@ -0,0 +1,139 @@
+import type { Context } from ".";
+import * as vscode from "vscode";
+import * as assert from "assert";
+import { targetToExecution } from "../../src/tasks";
+
+export async function getTests(ctx: Context) {
+    await ctx.suite("Tasks", (suite) => {
+        suite.addTest("cargo targetToExecution", async () => {
+            assert.deepStrictEqual(
+                await targetToExecution({
+                    type: "cargo",
+                    command: "check",
+                    args: ["foo"],
+                }).then(executionToSimple),
+                {
+                    process: "cargo",
+                    args: ["check", "foo"],
+                },
+            );
+        });
+
+        suite.addTest("shell targetToExecution", async () => {
+            assert.deepStrictEqual(
+                await targetToExecution({
+                    type: "shell",
+                    command: "thing",
+                    args: ["foo"],
+                }).then(executionToSimple),
+                {
+                    process: "thing",
+                    args: ["foo"],
+                },
+            );
+        });
+
+        suite.addTest("base tasks", async () => {
+            const tasks = await vscode.tasks.fetchTasks({ type: "cargo" });
+            const expectedTasks = [
+                {
+                    definition: { type: "cargo", command: "build" },
+                    name: "cargo build",
+                    execution: {
+                        process: "cargo",
+                        args: ["build"],
+                    },
+                },
+                {
+                    definition: {
+                        type: "cargo",
+                        command: "check",
+                    },
+                    name: "cargo check",
+                    execution: {
+                        process: "cargo",
+                        args: ["check"],
+                    },
+                },
+                {
+                    definition: { type: "cargo", command: "clippy" },
+                    name: "cargo clippy",
+                    execution: {
+                        process: "cargo",
+                        args: ["clippy"],
+                    },
+                },
+                {
+                    definition: { type: "cargo", command: "test" },
+                    name: "cargo test",
+                    execution: {
+                        process: "cargo",
+                        args: ["test"],
+                    },
+                },
+                {
+                    definition: {
+                        type: "cargo",
+                        command: "clean",
+                    },
+                    name: "cargo clean",
+                    execution: {
+                        process: "cargo",
+                        args: ["clean"],
+                    },
+                },
+                {
+                    definition: { type: "cargo", command: "run" },
+                    name: "cargo run",
+                    execution: {
+                        process: "cargo",
+                        args: ["run"],
+                    },
+                },
+            ];
+            tasks.map(f).forEach((actual, i) => {
+                const expected = expectedTasks[i];
+                assert.deepStrictEqual(actual, expected);
+            });
+        });
+    });
+}
+
+function f(task: vscode.Task): {
+    definition: vscode.TaskDefinition;
+    name: string;
+    execution: {
+        args: string[];
+    } & ({ command: string } | { process: string });
+} {
+    const execution = executionToSimple(task.execution!);
+
+    return {
+        definition: task.definition,
+        name: task.name,
+        execution,
+    };
+}
+function executionToSimple(
+    taskExecution: vscode.ProcessExecution | vscode.ShellExecution | vscode.CustomExecution,
+): {
+    args: string[];
+} & ({ command: string } | { process: string }) {
+    const exec = taskExecution as vscode.ProcessExecution | vscode.ShellExecution;
+    if (exec instanceof vscode.ShellExecution) {
+        return {
+            command: typeof exec.command === "string" ? exec.command : exec.command.value,
+            args: exec.args.map((arg) => {
+                if (typeof arg === "string") {
+                    return arg;
+                }
+                return arg.value;
+            }),
+        };
+    } else {
+        return {
+            process: exec.process,
+            args: exec.args,
+        };
+    }
+}