From 85e5062db30e9ea3fdd8641d783706c33bcfd843 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Wed, 26 Feb 2025 11:49:36 -0500 Subject: [PATCH] Configurable Command Plugin Build Configuration Packages can define their own plugins either directly or through their dependencies. These plugins define commands, and the extension exposes a list of these when you use `> Swift: Run Command Plugin`. It may be desirable to build and run these plugins with specific arguments. This patch introduces a new setting that can be specified globally or on a per workspace folder basis that allows users to configure arguments to pass to plugin command invocations. The setting is defined under `swift.pluginArguments`, and is specified as an object in the following form: ```json { "PluginCommandName:intent-name": ["--some", "--args"] } ``` - The top level string key is the command id in the form `PluginCommandName:intent-name`. For instance, swift-format's format-source-code command would be specified as `swift-format:format-source-code` - Specifying `PluginCommandName` will apply the arguments to all intents in the command plugin - Specifying `*` will apply the arguments to all commands. This patch also adds this wildcard functionality to the `swift.pluginPermissions` setting. Issue: #1365 --- docs/settings.md | 22 ++- package.json | 24 +++ src/configuration.ts | 51 ++++- src/tasks/SwiftPluginTaskProvider.ts | 23 ++- .../tasks/SwiftPluginTaskProvider.test.ts | 187 +++++++++++++----- 5 files changed, 248 insertions(+), 59 deletions(-) diff --git a/docs/settings.md b/docs/settings.md index 60b67aa92..107c8f6ad 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -6,7 +6,7 @@ This document outlines useful configuration options not covered by the settings ## Command Plugins -Swift packages can define [command plugins](https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/Plugins.md) that can perform arbitrary tasks. For example, the [swift-format](https://github.com/swiftlang/swift-format) package exposes a `format-source-code` command which will use swift-format to format source code in a folder. These plugin commands can be invoked from VS Code using `> Swift: Run Command Plugin`. +Swift packages can define [command plugins](https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/Plugins.md) that can perform arbitrary tasks. For example, the [swift-format](https://github.com/swiftlang/swift-format) package exposes a `format-source-code` command which will use swift-format to format source code in a folder. These plugin commands can be invoked from VS Code using `> Swift: Run Command Plugin`. A plugin may require permissions to perform tasks like writing to the file system or using the network. If a plugin command requires one of these permissions, you will be prompted in the integrated terminal to accept them. If you trust the command and wish to apply permissions on every command execution, you can configure a setting in your `settings.json`. @@ -23,7 +23,7 @@ A plugin may require permissions to perform tasks like writing to the file syste } ``` -A key of `PluginName:command` will set permissions for a specific command. A key of `PluginName` will set permissions for all commands in the plugin. +A key of `PluginName:command` will set permissions for a specific command. A key of `PluginName` will set permissions for all commands in the plugin. If you'd like the same permissions to be applied to all plugins use `*` as the plugin name. Precedence order is determined by specificity, where more specific names take priority. The name `*` is the least specific and `PluginName:command` is the most specific. Alternatively, you can define a task in your tasks.json and define permissions directly on the task. This will create a new entry in the list shown by `> Swift: Run Command Plugin`. @@ -43,6 +43,24 @@ Alternatively, you can define a task in your tasks.json and define permissions d } ``` +If you'd like to provide specific arguments to your plugin command invocation you can use the `swift.pluginArguments` setting. Defining an array for this setting applies the same arguments to all plugin command invocations. + +```json +{ + "swift.pluginArguments": ["-c", "release"] +} +``` + +Alternatively you can specfiy which specific command the arguments should apply to using `PluginName:command`. A key of `PluginName` will use the arguments for all commands in the plugin. If you'd like the same arguments to be used for all plugins use `*` as the plugin name. + +```json +{ + "swift.pluginArguments": { + "PluginName:command": ["-c", "release"] + } +} +``` + ## SourceKit-LSP [SourceKit-LSP](https://github.com/apple/sourcekit-lsp) is the language server used by the the Swift extension to provide symbol completion, jump to definition etc. It is developed by Apple to provide Swift and C language support for any editor that supports the Language Server Protocol. diff --git a/package.json b/package.json index 112ae5d89..d46a211be 100644 --- a/package.json +++ b/package.json @@ -418,6 +418,30 @@ "default": true, "markdownDescription": "Controls whether or not the extension will contribute environment variables defined in `Swift: Environment Variables` to the integrated terminal. If this is set to `true` and a custom `Swift: Path` is also set then the swift path is appended to the terminal's `PATH`." }, + "swift.pluginArguments": { + "default": [], + "markdownDescription": "Configure a list of arguments to pass to command invocations. This can either be an array of arguments, which will apply to all command invocations, or an object with an with command names as the key where the value is an array of arguments.", + "scope": "machine-overridable", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "object", + "patternProperties": { + "^([a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)?)$": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + }, "swift.pluginPermissions": { "type": "object", "default": {}, diff --git a/src/configuration.ts b/src/configuration.ts index 79e95fa0e..5397071b8 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -78,7 +78,9 @@ export interface FolderConfiguration { /** location to save swift-testing attachments */ readonly attachmentsPath: string; /** look up saved permissions for the supplied plugin */ - pluginPermissions(pluginId: string): PluginPermissionConfiguration; + pluginPermissions(pluginId?: string): PluginPermissionConfiguration; + /** look up saved arguments for the supplied plugin, or global plugin arguments if no plugin id is provided */ + pluginArguments(pluginId?: string): string[]; } export interface PluginPermissionConfiguration { @@ -143,6 +145,42 @@ const configuration = { }, folder(workspaceFolder: vscode.WorkspaceFolder): FolderConfiguration { + function pluginSetting( + setting: string, + pluginId?: string, + resultIsArray: boolean = false + ): T | undefined { + if (!pluginId) { + // Check for * as a wildcard plugin ID for configurations that want both + // global arguments as well as specific additional arguments for a plugin. + const wildcardSetting = pluginSetting(setting, "*", resultIsArray) as T | undefined; + if (wildcardSetting) { + return wildcardSetting; + } + + // Check if there is a global setting like `"swift.pluginArguments": ["-c", "release"]` + // that should apply to all plugins. + const args = vscode.workspace + .getConfiguration("swift", workspaceFolder) + .get(setting); + + if (resultIsArray && Array.isArray(args)) { + return args; + } else if ( + !resultIsArray && + args !== null && + typeof args === "object" && + Object.keys(args).length !== 0 + ) { + return args; + } + return undefined; + } + + return vscode.workspace.getConfiguration("swift", workspaceFolder).get<{ + [key: string]: T; + }>(setting, {})[pluginId]; + } return { /** Environment variables to set when running tests */ get testEnvironmentVariables(): { [key: string]: string } { @@ -179,12 +217,11 @@ const configuration = { .getConfiguration("swift", workspaceFolder) .get("attachmentsPath", "./.build/attachments"); }, - pluginPermissions(pluginId: string): PluginPermissionConfiguration { - return ( - vscode.workspace.getConfiguration("swift", workspaceFolder).get<{ - [key: string]: PluginPermissionConfiguration; - }>("pluginPermissions", {})[pluginId] ?? {} - ); + pluginPermissions(pluginId?: string): PluginPermissionConfiguration { + return pluginSetting("pluginPermissions", pluginId, false) ?? {}; + }, + pluginArguments(pluginId?: string): string[] { + return pluginSetting("pluginArguments", pluginId, true) ?? []; }, }; }, diff --git a/src/tasks/SwiftPluginTaskProvider.ts b/src/tasks/SwiftPluginTaskProvider.ts index 1de21d238..7aaca41f4 100644 --- a/src/tasks/SwiftPluginTaskProvider.ts +++ b/src/tasks/SwiftPluginTaskProvider.ts @@ -202,7 +202,7 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider { * are keyed by either plugin command name (package), or in the form `name:command`. * User-configured permissions take precedence over the hardcoded permissions, and the more * specific form of `name:command` takes precedence over the more general form of `name`. - * @param folderContext The folder context to search for the `swift.pluginPermissions` key. + * @param folderContext The folder context to search for the `swift.pluginPermissions` and `swift.pluginArguments` keys. * @param taskDefinition The task definition to search for the `disableSandbox` and `allowWritingToPackageDirectory` keys. * @param plugin The plugin to generate arguments for. * @returns A list of permission related arguments to pass when invoking the plugin. @@ -213,9 +213,14 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider { plugin: PackagePlugin ): string[] { const config = configuration.folder(folderContext); + const globalPackageConfig = config.pluginPermissions(); const packageConfig = config.pluginPermissions(plugin.package); const commandConfig = config.pluginPermissions(`${plugin.package}:${plugin.command}`); + const globalPackageArgs = config.pluginArguments(); + const packageArgs = config.pluginArguments(plugin.package); + const commandArgs = config.pluginArguments(`${plugin.package}:${plugin.command}`); + const taskDefinitionConfiguration: PluginPermissionConfiguration = {}; if (taskDefinition.disableSandbox) { taskDefinitionConfiguration.disableSandbox = true; @@ -232,11 +237,17 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider { taskDefinition.allowNetworkConnections; } - return this.pluginArguments({ - ...packageConfig, - ...commandConfig, - ...taskDefinitionConfiguration, - }); + return [ + ...globalPackageArgs, + ...packageArgs, + ...commandArgs, + ...this.pluginArguments({ + ...globalPackageConfig, + ...packageConfig, + ...commandConfig, + ...taskDefinitionConfiguration, + }), + ]; } private pluginArguments(config: PluginPermissionConfiguration): string[] { diff --git a/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts b/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts index 85b17112a..bf3715c49 100644 --- a/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts +++ b/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts @@ -14,6 +14,7 @@ import * as vscode from "vscode"; import * as assert from "assert"; +import { beforeEach, afterEach } from "mocha"; import { expect } from "chai"; import { WorkspaceContext } from "../../../src/WorkspaceContext"; import { SwiftPluginTaskProvider } from "../../../src/tasks/SwiftPluginTaskProvider"; @@ -38,57 +39,155 @@ suite("SwiftPluginTaskProvider Test Suite", function () { this.timeout(60000); // Mostly only when running suite with .only - suite("settings plugin arguments", () => { - activateExtensionForSuite({ - async setup(ctx) { - workspaceContext = ctx; - folderContext = await folderInRootWorkspace("command-plugin", workspaceContext); - await folderContext.loadSwiftPlugins(); - expect(workspaceContext.folders).to.not.have.lengthOf(0); - return await updateSettings({ - "swift.pluginPermissions": { - "command-plugin:command_plugin": { - disableSandbox: true, - allowWritingToPackageDirectory: true, - allowWritingToDirectory: ["/foo", "/bar"], - allowNetworkConnections: "all", - }, + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + folderContext = await folderInRootWorkspace("command-plugin", workspaceContext); + await folderContext.loadSwiftPlugins(); + expect(workspaceContext.folders).to.not.have.lengthOf(0); + }, + }); + + const expectedPluginPermissions = [ + "--disable-sandbox", + "--allow-writing-to-package-directory", + "--allow-writing-to-directory", + "/foo", + "/bar", + "--allow-network-connections", + "all", + ]; + + [ + { + name: "global plugin permissions", + settings: { + "swift.pluginPermissions": { + disableSandbox: true, + allowWritingToPackageDirectory: true, + allowWritingToDirectory: ["/foo", "/bar"], + allowNetworkConnections: "all", + }, + }, + expected: expectedPluginPermissions, + }, + { + name: "plugin scoped plugin permissions", + settings: { + "swift.pluginPermissions": { + "command-plugin": { + disableSandbox: true, + allowWritingToPackageDirectory: true, + allowWritingToDirectory: ["/foo", "/bar"], + allowNetworkConnections: "all", }, - }); + }, }, - }); + expected: expectedPluginPermissions, + }, + { + name: "command scoped plugin permissions", + settings: { + "swift.pluginPermissions": { + "command-plugin:command_plugin": { + disableSandbox: true, + allowWritingToPackageDirectory: true, + allowWritingToDirectory: ["/foo", "/bar"], + allowNetworkConnections: "all", + }, + }, + }, + expected: expectedPluginPermissions, + }, + { + name: "wildcard scoped plugin permissions", + settings: { + "swift.pluginPermissions": { + "*": { + disableSandbox: true, + allowWritingToPackageDirectory: true, + allowWritingToDirectory: ["/foo", "/bar"], + allowNetworkConnections: "all", + }, + }, + }, + expected: expectedPluginPermissions, + }, + { + name: "global plugin arguments", + settings: { + "swift.pluginArguments": ["-c", "release"], + }, + expected: ["-c", "release"], + }, + { + name: "plugin scoped plugin arguments", + settings: { + "swift.pluginArguments": { + "command-plugin": ["-c", "release"], + }, + }, + expected: ["-c", "release"], + }, + { + name: "command scoped plugin arguments", + settings: { + "swift.pluginArguments": { + "command-plugin:command_plugin": ["-c", "release"], + }, + }, + expected: ["-c", "release"], + }, + { + name: "wildcard scoped plugin arguments", + settings: { + "swift.pluginArguments": { + "*": ["-c", "release"], + }, + }, + expected: ["-c", "release"], + }, + { + name: "overlays settings", + settings: { + "swift.pluginArguments": { + "*": ["-a"], + "command-plugin": ["-b"], + "command-plugin:command_plugin": ["-c"], + }, + }, + expected: ["-a", "-b", "-c"], + }, + ].forEach(({ name, settings, expected }) => { + suite(name, () => { + let resetSettings: (() => Promise) | undefined; + beforeEach(async function () { + resetSettings = await updateSettings(settings); + }); + + afterEach(async () => { + if (resetSettings) { + await resetSettings(); + } + }); - test("provides a task with permissions set via settings", async () => { - const tasks = await vscode.tasks.fetchTasks({ type: "swift-plugin" }); - const task = tasks.find(t => t.name === "command-plugin"); - const swiftExecution = task?.execution as SwiftExecution; - assert.deepEqual( - swiftExecution.args, - workspaceContext.toolchain.buildFlags.withAdditionalFlags([ - "package", - "--disable-sandbox", - "--allow-writing-to-package-directory", - "--allow-writing-to-directory", - "/foo", - "/bar", - "--allow-network-connections", - "all", - "command_plugin", - ]) - ); + test("sets arguments", async () => { + const tasks = await vscode.tasks.fetchTasks({ type: "swift-plugin" }); + const task = tasks.find(t => t.name === "command-plugin"); + const swiftExecution = task?.execution as SwiftExecution; + assert.deepEqual( + swiftExecution.args, + workspaceContext.toolchain.buildFlags.withAdditionalFlags([ + "package", + ...expected, + "command_plugin", + ]) + ); + }); }); }); suite("execution", () => { - activateExtensionForSuite({ - async setup(ctx) { - workspaceContext = ctx; - folderContext = await folderInRootWorkspace("command-plugin", workspaceContext); - await folderContext.loadSwiftPlugins(); - expect(workspaceContext.folders).to.not.have.lengthOf(0); - }, - }); - suite("createSwiftPluginTask", () => { let taskProvider: SwiftPluginTaskProvider;