Skip to content

Commit 263a0e0

Browse files
Configurable Command Plugin Build Configuration (#1409)
* 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 Co-authored-by: Rishi <[email protected]>
1 parent 306d130 commit 263a0e0

File tree

5 files changed

+247
-58
lines changed

5 files changed

+247
-58
lines changed

docs/settings.md

+19-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ A plugin may require permissions to perform tasks like writing to the file syste
2323
}
2424
```
2525

26-
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.
26+
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.
2727

2828
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`.
2929

@@ -43,6 +43,24 @@ Alternatively, you can define a task in your tasks.json and define permissions d
4343
}
4444
```
4545

46+
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.
47+
48+
```json
49+
{
50+
"swift.pluginArguments": ["-c", "release"]
51+
}
52+
```
53+
54+
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.
55+
56+
```json
57+
{
58+
"swift.pluginArguments": {
59+
"PluginName:command": ["-c", "release"]
60+
}
61+
}
62+
```
63+
4664
## SourceKit-LSP
4765

4866
[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.

package.json

+24
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,30 @@
444444
"default": true,
445445
"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`."
446446
},
447+
"swift.pluginArguments": {
448+
"default": [],
449+
"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 command names as the key where the value is an array of arguments.",
450+
"scope": "machine-overridable",
451+
"oneOf": [
452+
{
453+
"type": "array",
454+
"items": {
455+
"type": "string"
456+
}
457+
},
458+
{
459+
"type": "object",
460+
"patternProperties": {
461+
"^([a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)?)$": {
462+
"type": "array",
463+
"items": {
464+
"type": "string"
465+
}
466+
}
467+
}
468+
}
469+
]
470+
},
447471
"swift.pluginPermissions": {
448472
"type": "object",
449473
"default": {},

src/configuration.ts

+44-7
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ export interface FolderConfiguration {
7878
/** location to save swift-testing attachments */
7979
readonly attachmentsPath: string;
8080
/** look up saved permissions for the supplied plugin */
81-
pluginPermissions(pluginId: string): PluginPermissionConfiguration;
81+
pluginPermissions(pluginId?: string): PluginPermissionConfiguration;
82+
/** look up saved arguments for the supplied plugin, or global plugin arguments if no plugin id is provided */
83+
pluginArguments(pluginId?: string): string[];
8284
}
8385

8486
export interface PluginPermissionConfiguration {
@@ -143,6 +145,42 @@ const configuration = {
143145
},
144146

145147
folder(workspaceFolder: vscode.WorkspaceFolder): FolderConfiguration {
148+
function pluginSetting<T>(
149+
setting: string,
150+
pluginId?: string,
151+
resultIsArray: boolean = false
152+
): T | undefined {
153+
if (!pluginId) {
154+
// Check for * as a wildcard plugin ID for configurations that want both
155+
// global arguments as well as specific additional arguments for a plugin.
156+
const wildcardSetting = pluginSetting(setting, "*", resultIsArray) as T | undefined;
157+
if (wildcardSetting) {
158+
return wildcardSetting;
159+
}
160+
161+
// Check if there is a global setting like `"swift.pluginArguments": ["-c", "release"]`
162+
// that should apply to all plugins.
163+
const args = vscode.workspace
164+
.getConfiguration("swift", workspaceFolder)
165+
.get<T>(setting);
166+
167+
if (resultIsArray && Array.isArray(args)) {
168+
return args;
169+
} else if (
170+
!resultIsArray &&
171+
args !== null &&
172+
typeof args === "object" &&
173+
Object.keys(args).length !== 0
174+
) {
175+
return args;
176+
}
177+
return undefined;
178+
}
179+
180+
return vscode.workspace.getConfiguration("swift", workspaceFolder).get<{
181+
[key: string]: T;
182+
}>(setting, {})[pluginId];
183+
}
146184
return {
147185
/** Environment variables to set when running tests */
148186
get testEnvironmentVariables(): { [key: string]: string } {
@@ -179,12 +217,11 @@ const configuration = {
179217
.getConfiguration("swift", workspaceFolder)
180218
.get<string>("attachmentsPath", "./.build/attachments");
181219
},
182-
pluginPermissions(pluginId: string): PluginPermissionConfiguration {
183-
return (
184-
vscode.workspace.getConfiguration("swift", workspaceFolder).get<{
185-
[key: string]: PluginPermissionConfiguration;
186-
}>("pluginPermissions", {})[pluginId] ?? {}
187-
);
220+
pluginPermissions(pluginId?: string): PluginPermissionConfiguration {
221+
return pluginSetting("pluginPermissions", pluginId, false) ?? {};
222+
},
223+
pluginArguments(pluginId?: string): string[] {
224+
return pluginSetting("pluginArguments", pluginId, true) ?? [];
188225
},
189226
};
190227
},

src/tasks/SwiftPluginTaskProvider.ts

+17-6
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider {
202202
* are keyed by either plugin command name (package), or in the form `name:command`.
203203
* User-configured permissions take precedence over the hardcoded permissions, and the more
204204
* specific form of `name:command` takes precedence over the more general form of `name`.
205-
* @param folderContext The folder context to search for the `swift.pluginPermissions` key.
205+
* @param folderContext The folder context to search for the `swift.pluginPermissions` and `swift.pluginArguments` keys.
206206
* @param taskDefinition The task definition to search for the `disableSandbox` and `allowWritingToPackageDirectory` keys.
207207
* @param plugin The plugin to generate arguments for.
208208
* @returns A list of permission related arguments to pass when invoking the plugin.
@@ -213,9 +213,14 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider {
213213
plugin: PackagePlugin
214214
): string[] {
215215
const config = configuration.folder(folderContext);
216+
const globalPackageConfig = config.pluginPermissions();
216217
const packageConfig = config.pluginPermissions(plugin.package);
217218
const commandConfig = config.pluginPermissions(`${plugin.package}:${plugin.command}`);
218219

220+
const globalPackageArgs = config.pluginArguments();
221+
const packageArgs = config.pluginArguments(plugin.package);
222+
const commandArgs = config.pluginArguments(`${plugin.package}:${plugin.command}`);
223+
219224
const taskDefinitionConfiguration: PluginPermissionConfiguration = {};
220225
if (taskDefinition.disableSandbox) {
221226
taskDefinitionConfiguration.disableSandbox = true;
@@ -232,11 +237,17 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider {
232237
taskDefinition.allowNetworkConnections;
233238
}
234239

235-
return this.pluginArguments({
236-
...packageConfig,
237-
...commandConfig,
238-
...taskDefinitionConfiguration,
239-
});
240+
return [
241+
...globalPackageArgs,
242+
...packageArgs,
243+
...commandArgs,
244+
...this.pluginArguments({
245+
...globalPackageConfig,
246+
...packageConfig,
247+
...commandConfig,
248+
...taskDefinitionConfiguration,
249+
}),
250+
];
240251
}
241252

242253
private pluginArguments(config: PluginPermissionConfiguration): string[] {

test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts

+143-44
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import * as vscode from "vscode";
1616
import * as assert from "assert";
17+
import { beforeEach, afterEach } from "mocha";
1718
import { expect } from "chai";
1819
import { WorkspaceContext } from "../../../src/WorkspaceContext";
1920
import { SwiftPluginTaskProvider } from "../../../src/tasks/SwiftPluginTaskProvider";
@@ -38,57 +39,155 @@ suite("SwiftPluginTaskProvider Test Suite", function () {
3839

3940
this.timeout(60000); // Mostly only when running suite with .only
4041

41-
suite("settings plugin arguments", () => {
42-
activateExtensionForSuite({
43-
async setup(ctx) {
44-
workspaceContext = ctx;
45-
folderContext = await folderInRootWorkspace("command-plugin", workspaceContext);
46-
await folderContext.loadSwiftPlugins();
47-
expect(workspaceContext.folders).to.not.have.lengthOf(0);
48-
return await updateSettings({
49-
"swift.pluginPermissions": {
50-
"command-plugin:command_plugin": {
51-
disableSandbox: true,
52-
allowWritingToPackageDirectory: true,
53-
allowWritingToDirectory: ["/foo", "/bar"],
54-
allowNetworkConnections: "all",
55-
},
42+
activateExtensionForSuite({
43+
async setup(ctx) {
44+
workspaceContext = ctx;
45+
folderContext = await folderInRootWorkspace("command-plugin", workspaceContext);
46+
await folderContext.loadSwiftPlugins();
47+
expect(workspaceContext.folders).to.not.have.lengthOf(0);
48+
},
49+
});
50+
51+
const expectedPluginPermissions = [
52+
"--disable-sandbox",
53+
"--allow-writing-to-package-directory",
54+
"--allow-writing-to-directory",
55+
"/foo",
56+
"/bar",
57+
"--allow-network-connections",
58+
"all",
59+
];
60+
61+
[
62+
{
63+
name: "global plugin permissions",
64+
settings: {
65+
"swift.pluginPermissions": {
66+
disableSandbox: true,
67+
allowWritingToPackageDirectory: true,
68+
allowWritingToDirectory: ["/foo", "/bar"],
69+
allowNetworkConnections: "all",
70+
},
71+
},
72+
expected: expectedPluginPermissions,
73+
},
74+
{
75+
name: "plugin scoped plugin permissions",
76+
settings: {
77+
"swift.pluginPermissions": {
78+
"command-plugin": {
79+
disableSandbox: true,
80+
allowWritingToPackageDirectory: true,
81+
allowWritingToDirectory: ["/foo", "/bar"],
82+
allowNetworkConnections: "all",
5683
},
57-
});
84+
},
5885
},
59-
});
86+
expected: expectedPluginPermissions,
87+
},
88+
{
89+
name: "command scoped plugin permissions",
90+
settings: {
91+
"swift.pluginPermissions": {
92+
"command-plugin:command_plugin": {
93+
disableSandbox: true,
94+
allowWritingToPackageDirectory: true,
95+
allowWritingToDirectory: ["/foo", "/bar"],
96+
allowNetworkConnections: "all",
97+
},
98+
},
99+
},
100+
expected: expectedPluginPermissions,
101+
},
102+
{
103+
name: "wildcard scoped plugin permissions",
104+
settings: {
105+
"swift.pluginPermissions": {
106+
"*": {
107+
disableSandbox: true,
108+
allowWritingToPackageDirectory: true,
109+
allowWritingToDirectory: ["/foo", "/bar"],
110+
allowNetworkConnections: "all",
111+
},
112+
},
113+
},
114+
expected: expectedPluginPermissions,
115+
},
116+
{
117+
name: "global plugin arguments",
118+
settings: {
119+
"swift.pluginArguments": ["-c", "release"],
120+
},
121+
expected: ["-c", "release"],
122+
},
123+
{
124+
name: "plugin scoped plugin arguments",
125+
settings: {
126+
"swift.pluginArguments": {
127+
"command-plugin": ["-c", "release"],
128+
},
129+
},
130+
expected: ["-c", "release"],
131+
},
132+
{
133+
name: "command scoped plugin arguments",
134+
settings: {
135+
"swift.pluginArguments": {
136+
"command-plugin:command_plugin": ["-c", "release"],
137+
},
138+
},
139+
expected: ["-c", "release"],
140+
},
141+
{
142+
name: "wildcard scoped plugin arguments",
143+
settings: {
144+
"swift.pluginArguments": {
145+
"*": ["-c", "release"],
146+
},
147+
},
148+
expected: ["-c", "release"],
149+
},
150+
{
151+
name: "overlays settings",
152+
settings: {
153+
"swift.pluginArguments": {
154+
"*": ["-a"],
155+
"command-plugin": ["-b"],
156+
"command-plugin:command_plugin": ["-c"],
157+
},
158+
},
159+
expected: ["-a", "-b", "-c"],
160+
},
161+
].forEach(({ name, settings, expected }) => {
162+
suite(name, () => {
163+
let resetSettings: (() => Promise<void>) | undefined;
164+
beforeEach(async function () {
165+
resetSettings = await updateSettings(settings);
166+
});
167+
168+
afterEach(async () => {
169+
if (resetSettings) {
170+
await resetSettings();
171+
}
172+
});
60173

61-
test("provides a task with permissions set via settings", async () => {
62-
const tasks = await vscode.tasks.fetchTasks({ type: "swift-plugin" });
63-
const task = tasks.find(t => t.name === "command-plugin");
64-
const swiftExecution = task?.execution as SwiftExecution;
65-
assert.deepEqual(
66-
swiftExecution.args,
67-
workspaceContext.toolchain.buildFlags.withAdditionalFlags([
68-
"package",
69-
"--disable-sandbox",
70-
"--allow-writing-to-package-directory",
71-
"--allow-writing-to-directory",
72-
"/foo",
73-
"/bar",
74-
"--allow-network-connections",
75-
"all",
76-
"command_plugin",
77-
])
78-
);
174+
test("sets arguments", async () => {
175+
const tasks = await vscode.tasks.fetchTasks({ type: "swift-plugin" });
176+
const task = tasks.find(t => t.name === "command-plugin");
177+
const swiftExecution = task?.execution as SwiftExecution;
178+
assert.deepEqual(
179+
swiftExecution.args,
180+
workspaceContext.toolchain.buildFlags.withAdditionalFlags([
181+
"package",
182+
...expected,
183+
"command_plugin",
184+
])
185+
);
186+
});
79187
});
80188
});
81189

82190
suite("execution", () => {
83-
activateExtensionForSuite({
84-
async setup(ctx) {
85-
workspaceContext = ctx;
86-
folderContext = await folderInRootWorkspace("command-plugin", workspaceContext);
87-
await folderContext.loadSwiftPlugins();
88-
expect(workspaceContext.folders).to.not.have.lengthOf(0);
89-
},
90-
});
91-
92191
suite("createSwiftPluginTask", () => {
93192
let taskProvider: SwiftPluginTaskProvider;
94193

0 commit comments

Comments
 (0)