taskConfiguration.ts 35.0 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

import nls = require('vs/nls');

import * as Objects from 'vs/base/common/objects';
import { IStringDictionary } from 'vs/base/common/collections';
import * as Platform from 'vs/base/common/platform';
import * as Types from 'vs/base/common/types';
import * as UUID from 'vs/base/common/uuid';
import { Config as ProcessConfig } from 'vs/base/common/processes';

16
import { ValidationStatus, IProblemReporter as IProblemReporterBase } from 'vs/base/common/parsers';
17 18
import {
	NamedProblemMatcher, ProblemMatcher, ProblemMatcherParser, Config as ProblemMatcherConfig,
19
	isNamedProblemMatcher, ProblemMatcherRegistry
20 21 22
} from 'vs/platform/markers/common/problemMatcher';

import * as TaskSystem from './taskSystem';
E
Erich Gamma 已提交
23 24 25 26 27 28 29 30 31 32 33 34

/**
 * Defines the problem handling strategy
 */
export class ProblemHandling {
	/**
	 * Cleans all problems for the owner defined in the
	 * error pattern.
	 */
	public static clean: string = 'cleanMatcherMatchers';
}

35
export interface PlatformTaskDescription {
E
Erich Gamma 已提交
36
	/**
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
	 * The command to be executed. Can be an external program or a shell
	 * command.
	 */
	command?: string;

	/**
	 * Specifies whether the command is a shell command and therefore must
	 * be executed in a shell interpreter (e.g. cmd.exe, bash, ...).
	 *
	 * Defaults to false if omitted.
	 */
	isShellCommand?: boolean;

	/**
	 * The command options used when the command is executed. Can be omitted.
	 */
	options?: ProcessConfig.CommandOptions;

	/**
	 * The arguments passed to the command or additional arguments passed to the
	 * command when using a global command.
E
Erich Gamma 已提交
58 59
	 */
	args?: string[];
60 61
}

62 63
export interface CommandBinding {
	/**
64
	 * The command identifer the task is bound to.
65
	 */
66
	identifier?: string;
67 68 69 70 71 72 73 74 75 76 77 78 79

	/**
	 * The title to use
	 */
	title?: string;

	/**
	 * An optional category
	 */
	category?: string;
}


80 81 82 83 84 85 86 87 88 89
/**
 * The description of a task.
 */
export interface TaskDescription extends PlatformTaskDescription {

	/**
	 * The task's name
	 */
	taskName: string;

D
Dirk Baeumer 已提交
90 91 92 93 94 95
	/**
	 * A unique optional identifier in case the name
	 * can't be used as such.
	 */
	identifier?: string;

96 97 98 99 100 101 102 103 104 105 106 107 108 109
	/**
	 * Windows specific task configuration
	 */
	windows?: PlatformTaskDescription;

	/**
	 * Mac specific task configuration
	 */
	osx?: PlatformTaskDescription;

	/**
	 * Linux speciif task configuration
	 */
	linux?: PlatformTaskDescription;
E
Erich Gamma 已提交
110 111

	/**
112
	 * @deprecated Use `isBackground` instead.
E
Erich Gamma 已提交
113 114
	 * Whether the executed command is kept alive and is watching the file system.
	 */
J
Johannes Rieken 已提交
115
	isWatching?: boolean;
E
Erich Gamma 已提交
116

117 118 119 120 121
	/**
	 * Whether the executed command is kept alive and runs in the background.
	 */
	isBackground?: boolean;

D
Dirk Baeumer 已提交
122 123 124 125 126
	/**
	 * Whether the task should prompt on close for confirmation if running.
	 */
	promptOnClose?: boolean;

E
Erich Gamma 已提交
127 128 129
	/**
	 * Whether this task maps to the default build command.
	 */
J
Johannes Rieken 已提交
130
	isBuildCommand?: boolean;
E
Erich Gamma 已提交
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152

	/**
	 * Whether this task maps to the default test command.
	 */
	isTestCommand?: boolean;

	/**
	 * Controls whether the output view of the running tasks is brought to front or not.
	 * See BaseTaskRunnerConfiguration#showOutput for details.
	 */
	showOutput?: string;

	/**
	 * Controls whether the executed command is printed to the output windows as well.
	 */
	echoCommand?: boolean;

	/**
	 * See BaseTaskRunnerConfiguration#suppressTaskName for details.
	 */
	suppressTaskName?: boolean;

153
	/**
D
Dirk Baeumer 已提交
154
	 * The other tasks the task depend on
155
	 */
D
Dirk Baeumer 已提交
156
	dependsOn?: string | string[];
157

E
Erich Gamma 已提交
158 159 160 161 162 163 164 165 166 167
	/**
	 * The problem matcher(s) to use to capture problems in the tasks
	 * output.
	 */
	problemMatcher?: ProblemMatcherConfig.ProblemMatcherType;
}

/**
 * The base task runner configuration
 */
168
export interface BaseTaskRunnerConfiguration {
E
Erich Gamma 已提交
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221

	/**
	 * The command to be executed. Can be an external program or a shell
	 * command.
	 */
	command?: string;

	/**
	 * Specifies whether the command is a shell command and therefore must
	 * be executed in a shell interpreter (e.g. cmd.exe, bash, ...).
	 *
	 * Defaults to false if omitted.
	 */
	isShellCommand?: boolean;

	/**
	 * The command options used when the command is executed. Can be omitted.
	 */
	options?: ProcessConfig.CommandOptions;

	/**
	 * The arguments passed to the command. Can be omitted.
	 */
	args?: string[];

	/**
	 * Controls whether the output view of the running tasks is brought to front or not.
	 * Valid values are:
	 *   "always": bring the output window always to front when a task is executed.
	 *   "silent": only bring it to front if no problem matcher is defined for the task executed.
	 *   "never": never bring the output window to front.
	 *
	 * If omitted "always" is used.
	 */
	showOutput?: string;

	/**
	 * Controls whether the executed command is printed to the output windows as well.
	 */
	echoCommand?: boolean;

	/**
	 * If set to false the task name is added as an additional argument to the
	 * command when executed. If set to true the task name is suppressed. If
	 * omitted false is used.
	 */
	suppressTaskName?: boolean;

	/**
	 * Some commands require that the task argument is highlighted with a special
	 * prefix (e.g. /t: for msbuild). This property can be used to control such
	 * a prefix.
	 */
J
Johannes Rieken 已提交
222
	taskSelector?: string;
E
Erich Gamma 已提交
223 224 225 226 227 228 229 230 231

	/**
	 * The problem matcher(s) to used if a global command is exucuted (e.g. no tasks
	 * are defined). A tasks.json file can either contain a global problemMatcher
	 * property or a tasks property but not both.
	 */
	problemMatcher?: ProblemMatcherConfig.ProblemMatcherType;

	/**
232 233
	 * @deprecated Use `isBackground` instead.
	 *
E
Erich Gamma 已提交
234
	 * Specifies whether a global command is a watching the filesystem. A task.json
235
	 * file can either contain a global isWatching property or a tasks property
E
Erich Gamma 已提交
236 237 238 239
	 * but not both.
	 */
	isWatching?: boolean;

240 241 242 243 244
	/**
	 * Specifies whether a global command is a background task.
	 */
	isBackground?: boolean;

D
Dirk Baeumer 已提交
245 246 247 248 249
	/**
	 * Whether the task should prompt on close for confirmation if running.
	 */
	promptOnClose?: boolean;

E
Erich Gamma 已提交
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
	/**
	 * The configuration of the available tasks. A tasks.json file can either
	 * contain a global problemMatcher property or a tasks property but not both.
	 */
	tasks?: TaskDescription[];

	/**
	 * Problem matcher declarations
	 */
	declares?: ProblemMatcherConfig.NamedProblemMatcher[];
}

/**
 * A configuration of an external build system. BuildConfiguration.buildSystem
 * must be set to 'program'
 */
export interface ExternalTaskRunnerConfiguration extends BaseTaskRunnerConfiguration {

268 269
	_runner?: string;

E
Erich Gamma 已提交
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
	/**
	 * The config's version number
	 */
	version: string;

	/**
	 * Windows specific task configuration
	 */
	windows?: BaseTaskRunnerConfiguration;

	/**
	 * Mac specific task configuration
	 */
	osx?: BaseTaskRunnerConfiguration;

	/**
	 * Linux speciif task configuration
	 */
	linux?: BaseTaskRunnerConfiguration;
}

enum ProblemMatcherKind {
	Unknown,
	String,
	ProblemMatcher,
	Array
}

298 299
const EMPTY_ARRAY: any[] = [];
Object.freeze(EMPTY_ARRAY);
E
Erich Gamma 已提交
300

301 302 303 304
function mergeProperty<T, K extends keyof T>(target: T, source: T, key: K) {
	if (source[key] !== void 0) {
		target[key] = source[key];
	}
E
Erich Gamma 已提交
305 306
}

307 308 309 310
function fillProperty<T, K extends keyof T>(target: T, source: T, key: K) {
	if (target[key] === void 0 && source[key] !== void 0) {
		target[key] = source[key];
	}
E
Erich Gamma 已提交
311 312
}

313
interface ParseContext {
314
	problemReporter: IProblemReporter;
315
	namedProblemMatchers: IStringDictionary<NamedProblemMatcher>;
316
	isTermnial: boolean;
E
Erich Gamma 已提交
317 318
}

319 320 321 322 323 324 325
namespace CommandOptions {
	export function from(this: void, options: ProcessConfig.CommandOptions, context: ParseContext): TaskSystem.CommandOptions {
		let result: TaskSystem.CommandOptions = {};
		if (options.cwd !== void 0) {
			if (Types.isString(options.cwd)) {
				result.cwd = options.cwd;
			} else {
326
				context.problemReporter.warn(nls.localize('ConfigurationParser.invalidCWD', 'Warning: options.cwd must be of type string. Ignoring value {0}\n', options.cwd));
327 328 329 330 331 332
			}
		}
		if (options.env !== void 0) {
			result.env = Objects.clone(options.env);
		}
		return isEmpty(result) ? undefined : result;
E
Erich Gamma 已提交
333 334
	}

335 336
	export function isEmpty(value: TaskSystem.CommandOptions): boolean {
		return !value || value.cwd === void 0 && value.env === void 0;
E
Erich Gamma 已提交
337 338
	}

339 340 341
	export function merge(target: TaskSystem.CommandOptions, source: TaskSystem.CommandOptions): TaskSystem.CommandOptions {
		if (isEmpty(source)) {
			return target;
E
Erich Gamma 已提交
342
		}
343 344
		if (isEmpty(target)) {
			return source;
E
Erich Gamma 已提交
345
		}
346 347 348 349 350 351 352 353 354 355
		mergeProperty(target, source, 'cwd');
		if (target.env === void 0) {
			target.env = source.env;
		} else if (source.env !== void 0) {
			let env: { [key: string]: string; } = Object.create(null);
			Object.keys(target.env).forEach(key => env[key] = target.env[key]);
			Object.keys(source.env).forEach(key => env[key = source.env[key]]);
			target.env = env;
		}
		return target;
E
Erich Gamma 已提交
356 357
	}

358 359 360
	export function fillDefaults(value: TaskSystem.CommandOptions): TaskSystem.CommandOptions {
		if (value && Object.isFrozen(value)) {
			return value;
E
Erich Gamma 已提交
361
		}
362 363
		if (value === void 0) {
			value = {};
E
Erich Gamma 已提交
364
		}
365 366
		if (value.cwd === void 0) {
			value.cwd = '${workspaceRoot}';
E
Erich Gamma 已提交
367
		}
368 369 370 371 372 373 374
		return value;
	}

	export function freeze(value: TaskSystem.CommandOptions): void {
		Object.freeze(value);
		if (value.env) {
			Object.freeze(value.env);
E
Erich Gamma 已提交
375 376
		}
	}
377
}
E
Erich Gamma 已提交
378

379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
interface ShellConfiguration {
	executable: string;
	args?: string[];
}

namespace ShellConfiguration {
	export function is(value: any): value is ShellConfiguration {
		let candidate: ShellConfiguration = value;
		return candidate && Types.isString(candidate.executable) && (candidate.args === void 0 || Types.isStringArray(candidate.args));
	}

	export function from(this: void, config: ShellConfiguration, context: ParseContext): TaskSystem.ShellConfiguration {
		if (!is(config)) {
			return undefined;
		}
		let result: ShellConfiguration = { executable: config.executable };
		if (config.args !== void 0) {
			result.args = config.args.slice();
		}
		return result;
	}

	export function isEmpty(value: TaskSystem.ShellConfiguration): boolean {
		return !value || value.executable === void 0 && (value.args === void 0 || value.args.length === 0);
	}

	export function merge(target: TaskSystem.ShellConfiguration, source: TaskSystem.ShellConfiguration): TaskSystem.ShellConfiguration {
		if (isEmpty(source)) {
			return target;
		}
		if (isEmpty(target)) {
			return source;
		}
		mergeProperty(target, source, 'executable');
		mergeProperty(target, source, 'args');
		return target;
	}

	export function fillDefaults(value: TaskSystem.ShellConfiguration): void {
	}

	export function freeze(value: TaskSystem.ShellConfiguration): void {
		if (!value) {
			return;
		}
		Object.freeze(value);
	}
}

428
namespace CommandConfiguration {
429
	interface BaseCommandConfiguationShape {
430
		command?: string;
431
		isShellCommand?: boolean | ShellConfiguration;
432 433 434 435 436 437
		args?: string[];
		options?: ProcessConfig.CommandOptions;
		echoCommand?: boolean;
		taskSelector?: string;
	}

438 439 440 441 442 443
	interface CommandConfiguationShape extends BaseCommandConfiguationShape {
		windows?: BaseCommandConfiguationShape;
		osx?: BaseCommandConfiguationShape;
		linux?: BaseCommandConfiguationShape;
	}

444
	export function from(this: void, config: CommandConfiguationShape, context: ParseContext): TaskSystem.CommandConfiguration {
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
		let result: TaskSystem.CommandConfiguration = fromBase(config, context);

		let osConfig: TaskSystem.CommandConfiguration = undefined;
		if (config.windows && Platform.platform === Platform.Platform.Windows) {
			osConfig = fromBase(config.windows, context);
		} else if (config.osx && Platform.platform === Platform.Platform.Mac) {
			osConfig = fromBase(config.osx, context);
		} else if (config.linux && Platform.platform === Platform.Platform.Linux) {
			osConfig = fromBase(config.linux, context);
		}
		if (osConfig) {
			result = merge(result, osConfig);
		}
		fillDefaults(result);
		return isEmpty(result) ? undefined : result;
	}

	function fromBase(this: void, config: BaseCommandConfiguationShape, context: ParseContext): TaskSystem.CommandConfiguration {
463 464 465
		let result: TaskSystem.CommandConfiguration = {};
		if (Types.isString(config.command)) {
			result.name = config.command;
E
Erich Gamma 已提交
466
		}
467 468
		if (Types.isBoolean(config.isShellCommand)) {
			result.isShellCommand = config.isShellCommand;
469 470 471
		} else if (ShellConfiguration.is(config.isShellCommand)) {
			result.isShellCommand = ShellConfiguration.from(config.isShellCommand, context);
			if (!context.isTermnial) {
472
				context.problemReporter.warn(nls.localize('ConfigurationParser.noShell', 'Warning: shell configuration is only supported when executing tasks in the terminal.'));
473 474 475
			}
		} else if (config.isShellCommand !== void 0) {
			result.isShellCommand = !!config.isShellCommand;
E
Erich Gamma 已提交
476
		}
477 478 479 480
		if (config.args !== void 0) {
			if (Types.isStringArray(config.args)) {
				result.args = config.args.slice(0);
			} else {
481
				context.problemReporter.fatal(nls.localize('ConfigurationParser.noargs', 'Error: command arguments must be an array of strings. Provided value is:\n{0}', config.args ? JSON.stringify(config.args, undefined, 4) : 'undefined'));
482
			}
E
Erich Gamma 已提交
483
		}
484 485
		if (config.options !== void 0) {
			result.options = CommandOptions.from(config.options, context);
E
Erich Gamma 已提交
486
		}
487 488
		if (Types.isBoolean(config.echoCommand)) {
			result.echo = config.echoCommand;
E
Erich Gamma 已提交
489
		}
490 491
		if (Types.isString(config.taskSelector)) {
			result.taskSelector = config.taskSelector;
E
Erich Gamma 已提交
492
		}
493
		return isEmpty(result) ? undefined : result;
E
Erich Gamma 已提交
494 495
	}

496 497 498 499 500 501 502 503 504 505 506
	export function isEmpty(value: TaskSystem.CommandConfiguration): boolean {
		return !value || value.name === void 0 && value.isShellCommand === void 0 && value.args === void 0 && CommandOptions.isEmpty(value.options) && value.echo === void 0;
	}

	export function onlyEcho(value: TaskSystem.CommandConfiguration): boolean {
		return value && value.echo !== void 0 && value.name === void 0 && value.isShellCommand === void 0 && value.args === void 0 && CommandOptions.isEmpty(value.options);
	}

	export function merge(target: TaskSystem.CommandConfiguration, source: TaskSystem.CommandConfiguration): TaskSystem.CommandConfiguration {
		if (isEmpty(source)) {
			return target;
E
Erich Gamma 已提交
507
		}
508 509
		if (isEmpty(target)) {
			return source;
E
Erich Gamma 已提交
510
		}
511
		mergeProperty(target, source, 'name');
512 513 514 515 516 517 518 519 520 521 522
		// Merge isShellCommand
		if (target.isShellCommand === void 0) {
			target.isShellCommand = source.isShellCommand;
		} if (Types.isBoolean(target.isShellCommand) && Types.isBoolean(source.isShellCommand)) {
			mergeProperty(target, source, 'isShellCommand');
		} else if (ShellConfiguration.is(target.isShellCommand) && ShellConfiguration.is(source.isShellCommand)) {
			ShellConfiguration.merge(target.isShellCommand, source.isShellCommand);
		} else if (Types.isBoolean(target.isShellCommand) && ShellConfiguration.is(source.isShellCommand)) {
			target.isShellCommand = source.isShellCommand;
		}

523 524 525 526 527 528 529
		mergeProperty(target, source, 'echo');
		mergeProperty(target, source, 'taskSelector');
		if (source.args !== void 0) {
			if (target.args === void 0) {
				target.args = source.args;
			} else {
				target.args = target.args.concat(source.args);
E
Erich Gamma 已提交
530 531
			}
		}
532 533
		target.options = CommandOptions.merge(target.options, source.options);
		return target;
E
Erich Gamma 已提交
534 535
	}

536 537 538 539 540 541 542 543 544 545 546 547 548 549 550
	export function fillDefaults(value: TaskSystem.CommandConfiguration): void {
		if (!value || Object.isFrozen(value)) {
			return;
		}
		if (value.name !== void 0 && value.isShellCommand === void 0) {
			value.isShellCommand = false;
		}
		if (value.echo === void 0) {
			value.echo = false;
		}
		if (value.args === void 0) {
			value.args = EMPTY_ARRAY;
		}
		if (!isEmpty(value)) {
			value.options = CommandOptions.fillDefaults(value.options);
E
Erich Gamma 已提交
551 552 553
		}
	}

554 555 556 557 558 559 560
	export function freeze(value: TaskSystem.CommandConfiguration): void {
		Object.freeze(value);
		if (value.args) {
			Object.freeze(value.args);
		}
		if (value.options) {
			CommandOptions.freeze(value.options);
E
Erich Gamma 已提交
561
		}
562 563 564
		if (ShellConfiguration.is(value.isShellCommand)) {
			ShellConfiguration.freeze(value.isShellCommand);
		}
E
Erich Gamma 已提交
565
	}
566
}
E
Erich Gamma 已提交
567

568 569 570
namespace ProblemMatcherConverter {

	export function namedFrom(this: void, declares: ProblemMatcherConfig.NamedProblemMatcher[], context: ParseContext): IStringDictionary<NamedProblemMatcher> {
J
Johannes Rieken 已提交
571
		let result: IStringDictionary<NamedProblemMatcher> = Object.create(null);
572 573

		if (!Types.isArray(declares)) {
E
Erich Gamma 已提交
574 575
			return result;
		}
576
		(<ProblemMatcherConfig.NamedProblemMatcher[]>declares).forEach((value) => {
577
			let namedProblemMatcher = (new ProblemMatcherParser(context.problemReporter)).parse(value);
578
			if (isNamedProblemMatcher(namedProblemMatcher)) {
E
Erich Gamma 已提交
579
				result[namedProblemMatcher.name] = namedProblemMatcher;
580
			} else {
581
				context.problemReporter.error(nls.localize('ConfigurationParser.noName', 'Error: Problem Matcher in declare scope must have a name:\n{0}\n', JSON.stringify(value, undefined, 4)));
E
Erich Gamma 已提交
582 583 584 585 586
			}
		});
		return result;
	}

587 588 589 590 591 592 593
	export function from(this: void, config: ProblemMatcherConfig.ProblemMatcherType, context: ParseContext): ProblemMatcher[] {
		let result: ProblemMatcher[] = [];
		if (config === void 0) {
			return result;
		}
		let kind = getProblemMatcherKind(config);
		if (kind === ProblemMatcherKind.Unknown) {
594
			context.problemReporter.warn(nls.localize(
595 596 597
				'ConfigurationParser.unknownMatcherKind',
				'Warning: the defined problem matcher is unknown. Supported types are string | ProblemMatcher | (string | ProblemMatcher)[].\n{0}\n',
				JSON.stringify(config, null, 4)));
E
Erich Gamma 已提交
598
			return result;
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622
		} else if (kind === ProblemMatcherKind.String || kind === ProblemMatcherKind.ProblemMatcher) {
			let matcher = resolveProblemMatcher(config, context);
			if (matcher) {
				result.push(matcher);
			}
		} else if (kind === ProblemMatcherKind.Array) {
			let problemMatchers = <(string | ProblemMatcherConfig.ProblemMatcher)[]>config;
			problemMatchers.forEach(problemMatcher => {
				let matcher = resolveProblemMatcher(problemMatcher, context);
				if (matcher) {
					result.push(matcher);
				}
			});
		}
		return result;
	}

	function getProblemMatcherKind(this: void, value: ProblemMatcherConfig.ProblemMatcherType): ProblemMatcherKind {
		if (Types.isString(value)) {
			return ProblemMatcherKind.String;
		} else if (Types.isArray(value)) {
			return ProblemMatcherKind.Array;
		} else if (!Types.isUndefined(value)) {
			return ProblemMatcherKind.ProblemMatcher;
E
Erich Gamma 已提交
623
		} else {
624
			return ProblemMatcherKind.Unknown;
E
Erich Gamma 已提交
625 626 627
		}
	}

628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644
	function resolveProblemMatcher(this: void, value: string | ProblemMatcherConfig.ProblemMatcher, context: ParseContext): ProblemMatcher {
		if (Types.isString(value)) {
			let variableName = <string>value;
			if (variableName.length > 1 && variableName[0] === '$') {
				variableName = variableName.substring(1);
				let global = ProblemMatcherRegistry.get(variableName);
				if (global) {
					return Objects.clone(global);
				}
				let localProblemMatcher = context.namedProblemMatchers[variableName];
				if (localProblemMatcher) {
					localProblemMatcher = Objects.clone(localProblemMatcher);
					// remove the name
					delete localProblemMatcher.name;
					return localProblemMatcher;
				}
			}
645
			context.problemReporter.error(nls.localize('ConfigurationParser.invalidVaraibleReference', 'Error: Invalid problemMatcher reference: {0}\n', value));
646 647 648
			return undefined;
		} else {
			let json = <ProblemMatcherConfig.ProblemMatcher>value;
649
			return new ProblemMatcherParser(context.problemReporter).parse(json);
650 651 652 653
		}
	}
}

654 655
namespace CommandBinding {
	export function isEmpty(value: TaskSystem.CommandBinding): boolean {
656
		return !value || value.identifier === void 0 && value.title === void 0 && value.category === void 0;
657 658 659 660 661 662 663
	}

	export function from(this: void, binding: CommandBinding, context: ParseContext): TaskSystem.CommandBinding {
		if (!binding) {
			return undefined;
		}

664
		if (!Types.isString(binding.identifier)) {
665
			context.problemReporter.warn(nls.localize('noCommandId', 'Warning: a command binding must defined an identifier. Ignoring binding.'));
666 667 668
			return undefined;
		}
		let result: TaskSystem.CommandBinding = {
669 670
			identifier: binding.identifier,
			title: ''
671 672 673 674 675 676 677 678
		};
		if (Types.isString(binding.category)) {
			result.category = binding.category;
		}
		return result;
	}
}

679 680 681 682 683 684 685 686
namespace TaskDescription {

	export interface TaskConfiguration {
		tasks: IStringDictionary<TaskSystem.TaskDescription>;
		buildTask?: string;
		testTask?: string;
	}

687 688 689 690
	export function isEmpty(value: TaskConfiguration): boolean {
		return !value || !value.tasks || Object.keys(value.tasks).length === 0;
	}

691
	export function from(this: void, tasks: TaskDescription[], globals: Globals, context: ParseContext): TaskConfiguration {
E
Erich Gamma 已提交
692
		if (!tasks) {
693
			return undefined;
E
Erich Gamma 已提交
694
		}
695
		let parsedTasks: IStringDictionary<TaskSystem.TaskDescription> = Object.create(null);
J
Johannes Rieken 已提交
696 697
		let defaultBuildTask: { id: string; exact: number; } = { id: null, exact: -1 };
		let defaultTestTask: { id: string; exact: number; } = { id: null, exact: -1 };
E
Erich Gamma 已提交
698 699 700
		tasks.forEach((externalTask) => {
			let taskName = externalTask.taskName;
			if (!taskName) {
701
				context.problemReporter.fatal(nls.localize('ConfigurationParser.noTaskName', 'Error: tasks must provide a taskName property. The task will be ignored.\n{0}\n', JSON.stringify(externalTask, null, 4)));
E
Erich Gamma 已提交
702 703
				return;
			}
704 705 706 707
			let problemMatchers = ProblemMatcherConverter.from(externalTask.problemMatcher, context);
			let command: TaskSystem.CommandConfiguration = externalTask.command !== void 0
				? CommandConfiguration.from(externalTask, context)
				: externalTask.echoCommand !== void 0 ? { echo: !!externalTask.echoCommand } : undefined;
D
Dirk Baeumer 已提交
708
			let identifer = Types.isString(externalTask.identifier) ? externalTask.identifier : taskName;
709 710 711
			let task: TaskSystem.TaskDescription = {
				id: UUID.generateUuid(),
				name: taskName,
D
Dirk Baeumer 已提交
712
				identifier: identifer,
713 714 715 716
				command,
				showOutput: undefined
			};
			if (externalTask.command === void 0 && Types.isStringArray(externalTask.args)) {
E
Erich Gamma 已提交
717 718
				task.args = externalTask.args.slice();
			}
719
			if (externalTask.isWatching !== void 0) {
720
				task.isBackground = !!externalTask.isWatching;
E
Erich Gamma 已提交
721
			}
722 723 724 725
			if (externalTask.isBackground !== void 0) {
				task.isBackground = !!externalTask.isBackground;
			}
			if (externalTask.promptOnClose !== void 0) {
D
Dirk Baeumer 已提交
726 727
				task.promptOnClose = !!externalTask.promptOnClose;
			}
E
Erich Gamma 已提交
728 729 730
			if (Types.isString(externalTask.showOutput)) {
				task.showOutput = TaskSystem.ShowOutput.fromString(externalTask.showOutput);
			}
731
			if (externalTask.command !== void 0) {
732 733 734
				// if the task has its own command then we suppress the
				// task name by default.
				task.suppressTaskName = true;
735 736
			} else if (externalTask.suppressTaskName !== void 0) {
				task.suppressTaskName = !!externalTask.suppressTaskName;
E
Erich Gamma 已提交
737
			}
D
Dirk Baeumer 已提交
738 739 740 741 742 743
			if (externalTask.dependsOn !== void 0) {
				if (Types.isString(externalTask.dependsOn)) {
					task.dependsOn = [externalTask.dependsOn];
				} else if (Types.isStringArray(externalTask.dependsOn)) {
					task.dependsOn = externalTask.dependsOn.slice();
				}
744
			}
E
Erich Gamma 已提交
745 746 747
			if (problemMatchers) {
				task.problemMatchers = problemMatchers;
			}
748 749
			mergeGlobals(task, globals);
			fillDefaults(task);
D
Dirk Baeumer 已提交
750
			let addTask: boolean = true;
751 752
			if (context.isTermnial && task.command && task.command.name && task.command.isShellCommand && task.command.args && task.command.args.length > 0) {
				if (hasUnescapedSpaces(task.command.name) || task.command.args.some(hasUnescapedSpaces)) {
753
					context.problemReporter.warn(nls.localize('taskConfiguration.shellArgs', 'Warning: the task \'{0}\' is a shell command and either the command name or one of its arguments has unescaped spaces. To ensure correct command line quoting please merge args into the command.', task.name));
754
				}
755
			}
D
Dirk Baeumer 已提交
756 757
			if (context.isTermnial) {
				if ((task.command === void 0 || task.command.name === void 0) && (task.dependsOn === void 0 || task.dependsOn.length === 0)) {
758
					context.problemReporter.error(nls.localize(
759
						'taskConfiguration.noCommandOrDependsOn', 'Error: the task \'{0}\' neither specifies a command or a dependsOn property. The task will be ignored. Its definition is:\n{1}',
D
Dirk Baeumer 已提交
760 761 762 763 764 765
						task.name, JSON.stringify(externalTask, undefined, 4)
					));
					addTask = false;
				}
			} else {
				if (task.command === void 0 || task.command.name === void 0) {
766
					context.problemReporter.warn(nls.localize(
767
						'taskConfiguration.noCommand', 'Error: the task \'{0}\' doesn\'t define a command. The task will be ignored. Its definition is:\n{1}',
D
Dirk Baeumer 已提交
768 769 770 771
						task.name, JSON.stringify(externalTask, undefined, 4)
					));
					addTask = false;
				}
E
Erich Gamma 已提交
772
			}
D
Dirk Baeumer 已提交
773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788
			if (addTask) {
				parsedTasks[task.id] = task;
				if (!Types.isUndefined(externalTask.isBuildCommand) && externalTask.isBuildCommand && defaultBuildTask.exact < 2) {
					defaultBuildTask.id = task.id;
					defaultBuildTask.exact = 2;
				} else if (taskName === 'build' && defaultBuildTask.exact < 2) {
					defaultBuildTask.id = task.id;
					defaultBuildTask.exact = 1;
				}
				if (!Types.isUndefined(externalTask.isTestCommand) && externalTask.isTestCommand && defaultTestTask.exact < 2) {
					defaultTestTask.id = task.id;
					defaultTestTask.exact = 2;
				} else if (taskName === 'test' && defaultTestTask.exact < 2) {
					defaultTestTask.id = task.id;
					defaultTestTask.exact = 1;
				}
E
Erich Gamma 已提交
789 790
			}
		});
791
		let buildTask: string;
E
Erich Gamma 已提交
792
		if (defaultBuildTask.exact > 0) {
793
			buildTask = defaultBuildTask.id;
E
Erich Gamma 已提交
794
		}
795
		let testTask: string;
E
Erich Gamma 已提交
796
		if (defaultTestTask.exact > 0) {
797
			testTask = defaultTestTask.id;
E
Erich Gamma 已提交
798
		}
799 800
		let result = { tasks: parsedTasks, buildTask, testTask };
		return isEmpty(result) ? undefined : result;
E
Erich Gamma 已提交
801 802
	}

803
	export function merge(target: TaskConfiguration, source: TaskConfiguration): TaskConfiguration {
804 805 806 807 808 809 810
		if (isEmpty(source)) {
			return target;
		}
		if (isEmpty(target)) {
			return source;
		}

811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830
		if (source.tasks) {
			// Tasks are keyed by ID but we need to merge by name
			let targetNames: IStringDictionary<string> = Object.create(null);
			Object.keys(target.tasks).forEach(key => {
				let task = target.tasks[key];
				targetNames[task.name] = task.id;
			});

			let sourceNames: IStringDictionary<string> = Object.create(null);
			Object.keys(source.tasks).forEach(key => {
				let task = source.tasks[key];
				sourceNames[task.name] = task.id;
			});

			Object.keys(sourceNames).forEach(taskName => {
				let targetId = targetNames[taskName];
				let sourceId = sourceNames[taskName];
				// Same name exists globally
				if (targetId) {
					delete target.tasks[targetId];
E
Erich Gamma 已提交
831
				}
832
				target.tasks[sourceId] = source.tasks[sourceId];
E
Erich Gamma 已提交
833 834
			});
		}
835 836 837 838 839 840
		fillProperty(target, source, 'buildTask');
		fillProperty(target, source, 'testTask');
		return target;
	}

	export function mergeGlobals(task: TaskSystem.TaskDescription, globals: Globals): void {
D
Dirk Baeumer 已提交
841 842 843 844 845 846 847 848 849 850 851 852 853
		// We only merge a command from a global definition if there is no dependsOn
		if (task.dependsOn === void 0) {
			if (CommandConfiguration.isEmpty(task.command) && !CommandConfiguration.isEmpty(globals.command) && globals.command.name !== void 0) {
				task.command = globals.command;
			}
			if (CommandConfiguration.onlyEcho(task.command)) {
				// The globals can have a echo set which would override the local echo
				// Saves the need of a additional fill method. But might be necessary
				// at some point.
				let oldEcho = task.command.echo;
				CommandConfiguration.merge(task.command, globals.command);
				task.command.echo = oldEcho;
			}
854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887
		}
		// promptOnClose is inferred from isBackground if available
		if (task.promptOnClose === void 0 && task.isBackground === void 0 && globals.promptOnClose !== void 0) {
			task.promptOnClose = globals.promptOnClose;
		}
		if (task.suppressTaskName === void 0 && globals.suppressTaskName !== void 0) {
			task.suppressTaskName = globals.suppressTaskName;
		}
		if (task.showOutput === void 0 && globals.showOutput !== void 0) {
			task.showOutput = globals.showOutput;
		}
	}

	export function fillDefaults(task: TaskSystem.TaskDescription): void {
		CommandConfiguration.fillDefaults(task.command);
		if (task.args === void 0 && task.command === void 0) {
			task.args = EMPTY_ARRAY;
		}
		if (task.suppressTaskName === void 0) {
			task.suppressTaskName = false;
		}
		if (task.promptOnClose === void 0) {
			task.promptOnClose = task.isBackground !== void 0 ? !task.isBackground : true;
		}
		if (task.isBackground === void 0) {
			task.isBackground = false;
		}
		if (task.showOutput === void 0) {
			task.showOutput = TaskSystem.ShowOutput.Always;
		}
		if (task.problemMatchers === void 0) {
			task.problemMatchers = EMPTY_ARRAY;
		}
	}
888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909

	function hasUnescapedSpaces(value: string): boolean {
		if (Platform.isWindows) {
			if (value.length >= 2 && value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') {
				return false;
			}
			return value.indexOf(' ') !== -1;
		} else {
			if (value.length >= 2 && ((value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') || (value.charAt(0) === '\'' && value.charAt(value.length - 1) === '\''))) {
				return false;
			}
			for (let i = 0; i < value.length; i++) {
				let ch = value.charAt(i);
				if (ch === ' ') {
					if (i === 0 || value.charAt(i) !== '\\') {
						return true;
					}
				}
			}
			return false;
		}
	}
910 911 912 913 914 915 916 917 918 919
}

interface Globals {
	command?: TaskSystem.CommandConfiguration;
	promptOnClose?: boolean;
	suppressTaskName?: boolean;
	showOutput?: TaskSystem.ShowOutput;
}

namespace Globals {
920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943

	export function from(config: ExternalTaskRunnerConfiguration, context: ParseContext): Globals {
		let result = fromBase(config, context);
		let osGlobals: Globals = undefined;
		if (config.windows && Platform.platform === Platform.Platform.Windows) {
			osGlobals = fromBase(config.windows, context);
		} else if (config.osx && Platform.platform === Platform.Platform.Mac) {
			osGlobals = fromBase(config.osx, context);
		} else if (config.linux && Platform.platform === Platform.Platform.Linux) {
			osGlobals = fromBase(config.linux, context);
		}
		if (osGlobals) {
			result = Globals.merge(result, osGlobals);
		}
		Globals.fillDefaults(result);
		let command = CommandConfiguration.from(config, context);
		if (command) {
			result.command = command;
		}
		Globals.freeze(result);
		return result;
	}

	export function fromBase(this: void, config: BaseTaskRunnerConfiguration, context: ParseContext): Globals {
944 945 946 947 948 949 950 951 952 953
		let result: Globals = {};
		if (Types.isString(config.showOutput)) {
			result.showOutput = TaskSystem.ShowOutput.fromString(config.showOutput);
		}
		if (config.suppressTaskName !== void 0) {
			result.suppressTaskName = !!config.suppressTaskName;
		}
		if (config.promptOnClose !== void 0) {
			result.promptOnClose = !!config.promptOnClose;
		}
E
Erich Gamma 已提交
954 955 956
		return result;
	}

957 958 959 960 961 962 963
	export function isEmpty(value: Globals): boolean {
		return !value || value.command === void 0 && value.promptOnClose === void 0 && value.showOutput === void 0 && value.suppressTaskName === void 0;
	}

	export function merge(target: Globals, source: Globals): Globals {
		if (isEmpty(source)) {
			return target;
E
Erich Gamma 已提交
964
		}
965 966 967 968 969 970 971
		if (isEmpty(target)) {
			return source;
		}
		mergeProperty(target, source, 'promptOnClose');
		mergeProperty(target, source, 'suppressTaskName');
		mergeProperty(target, source, 'showOutput');
		return target;
E
Erich Gamma 已提交
972 973
	}

974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996
	export function fillDefaults(value: Globals): void {
		if (!value) {
			return;
		}
		if (value.suppressTaskName === void 0) {
			value.suppressTaskName = false;
		}
		if (value.showOutput === void 0) {
			value.showOutput = TaskSystem.ShowOutput.Always;
		}
		if (value.promptOnClose === void 0) {
			value.promptOnClose = true;
		}
	}

	export function freeze(value: Globals): void {
		Object.freeze(value);
		if (value.command) {
			CommandConfiguration.freeze(value.command);
		}
	}
}

997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021
export enum ExecutionEngine {
	Unknown = 0,
	Terminal = 1,
	OutputPanel = 2
}

export namespace ExecutionEngine {

	export function from(config: ExternalTaskRunnerConfiguration): ExecutionEngine {
		return isTerminalConfig(config)
			? ExecutionEngine.Terminal
			: isRunnerConfig(config)
				? ExecutionEngine.OutputPanel
				: ExecutionEngine.Unknown;
	}

	function isRunnerConfig(config: ExternalTaskRunnerConfiguration): boolean {
		return (!config._runner || config._runner === 'program') && (config.version === '0.1.0' || !config.version);
	}

	function isTerminalConfig(config: ExternalTaskRunnerConfiguration): boolean {
		return config._runner === 'terminal' || config.version === '2.0.0';
	}
}

1022 1023 1024
export interface ParseResult {
	validationStatus: ValidationStatus;
	configuration: TaskSystem.TaskRunnerConfiguration;
1025
	engine: ExecutionEngine;
1026 1027
}

1028
export interface IProblemReporter extends IProblemReporterBase {
1029
	clearOutput(): void;
1030 1031 1032 1033
}

class ConfigurationParser {

1034
	private problemReporter: IProblemReporter;
1035

1036 1037
	constructor(problemReporter: IProblemReporter) {
		this.problemReporter = problemReporter;
1038 1039 1040
	}

	public run(fileConfig: ExternalTaskRunnerConfiguration): ParseResult {
1041
		let engine = ExecutionEngine.from(fileConfig);
1042
		if (engine === ExecutionEngine.Terminal) {
1043
			this.problemReporter.clearOutput();
1044
		}
1045
		let context: ParseContext = { problemReporter: this.problemReporter, namedProblemMatchers: undefined, isTermnial: engine === ExecutionEngine.Terminal };
1046
		return {
1047
			validationStatus: this.problemReporter.status,
1048
			configuration: this.createTaskRunnerConfiguration(fileConfig, context),
1049
			engine
1050 1051 1052 1053
		};
	}

	private createTaskRunnerConfiguration(fileConfig: ExternalTaskRunnerConfiguration, context: ParseContext): TaskSystem.TaskRunnerConfiguration {
1054
		let globals = Globals.from(fileConfig, context);
1055
		if (this.problemReporter.status.isFatal()) {
1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070
			return undefined;
		}
		context.namedProblemMatchers = ProblemMatcherConverter.namedFrom(fileConfig.declares, context);
		let globalTasks: TaskDescription.TaskConfiguration;
		if (fileConfig.windows && Platform.platform === Platform.Platform.Windows) {
			globalTasks = TaskDescription.from(fileConfig.windows.tasks, globals, context);
		} else if (fileConfig.osx && Platform.platform === Platform.Platform.Mac) {
			globalTasks = TaskDescription.from(fileConfig.osx.tasks, globals, context);
		} else if (fileConfig.linux && Platform.platform === Platform.Platform.Linux) {
			globalTasks = TaskDescription.from(fileConfig.linux.tasks, globals, context);
		}

		let taskConfig: TaskDescription.TaskConfiguration;
		if (fileConfig.tasks) {
			taskConfig = TaskDescription.from(fileConfig.tasks, globals, context);
1071 1072 1073 1074
		}
		taskConfig = TaskDescription.merge(taskConfig, globalTasks);

		if (TaskDescription.isEmpty(taskConfig)) {
1075 1076 1077 1078 1079 1080 1081 1082
			let tasks: IStringDictionary<TaskSystem.TaskDescription> = Object.create(null);
			let buildTask: string;
			if (globals.command && globals.command.name) {
				let matchers: ProblemMatcher[] = ProblemMatcherConverter.from(fileConfig.problemMatcher, context);;
				let isBackground = fileConfig.isBackground ? !!fileConfig.isBackground : fileConfig.isWatching ? !!fileConfig.isWatching : undefined;
				let task: TaskSystem.TaskDescription = {
					id: UUID.generateUuid(),
					name: globals.command.name,
D
Dirk Baeumer 已提交
1083
					identifier: globals.command.name,
1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094
					command: undefined,
					isBackground: isBackground,
					showOutput: undefined,
					suppressTaskName: true, // this must be true since we infer the task from the global data.
					problemMatchers: matchers
				};
				TaskDescription.mergeGlobals(task, globals);
				TaskDescription.fillDefaults(task);
				tasks[task.id] = task;
				buildTask = task.id;
			}
1095

1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107
			taskConfig = {
				tasks: tasks,
				buildTask
			};
		}

		return {
			tasks: taskConfig.tasks,
			buildTasks: taskConfig.buildTask ? [taskConfig.buildTask] : [],
			testTasks: taskConfig.testTask ? [taskConfig.testTask] : []
		};
	}
E
Erich Gamma 已提交
1108 1109
}

1110
export function parse(configuration: ExternalTaskRunnerConfiguration, logger: IProblemReporter): ParseResult {
E
Erich Gamma 已提交
1111 1112
	return (new ConfigurationParser(logger)).run(configuration);
}