diff --git a/packages/task/src/browser/task-configurations.ts b/packages/task/src/browser/task-configurations.ts index 074a4d1a533e0..8aba389829935 100644 --- a/packages/task/src/browser/task-configurations.ts +++ b/packages/task/src/browser/task-configurations.ts @@ -14,11 +14,13 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import * as Ajv from 'ajv'; import { inject, injectable, postConstruct } from 'inversify'; import { ContributedTaskConfiguration, TaskConfiguration, TaskCustomization, TaskDefinition } from '../common'; import { TaskDefinitionRegistry } from './task-definition-registry'; import { ProvidedTaskConfigurations } from './provided-task-configurations'; import { TaskConfigurationManager } from './task-configuration-manager'; +import { TaskSchemaUpdater } from './task-schema-updater'; import { Disposable, DisposableCollection, ResourceProvider } from '@theia/core/lib/common'; import URI from '@theia/core/lib/common/uri'; import { FileChange, FileChangeType } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; @@ -81,6 +83,9 @@ export class TaskConfigurations implements Disposable { @inject(TaskConfigurationManager) protected readonly taskConfigurationManager: TaskConfigurationManager; + @inject(TaskSchemaUpdater) + protected readonly taskSchemaUpdater: TaskSchemaUpdater; + constructor() { this.toDispose.push(Disposable.create(() => { this.tasksMap.clear(); @@ -125,9 +130,11 @@ export class TaskConfigurations implements Disposable { } /** - * returns the list of known tasks, which includes: + * returns a collection of known tasks, which includes: * - all the configured tasks in `tasks.json`, and - * - the customized detected tasks + * - the customized detected tasks. + * + * The invalid task configs are not returned. */ async getTasks(): Promise { const configuredTasks = Array.from(this.tasksMap.values()).reduce((acc, labelConfigMap) => acc.concat(Array.from(labelConfigMap.values())), [] as TaskConfiguration[]); @@ -143,6 +150,22 @@ export class TaskConfigurations implements Disposable { return [...configuredTasks, ...detectedTasksAsConfigured]; } + /** + * returns a collection of invalid task configs as per the task schema defined in Theia. + */ + getInvalidTaskConfigurations(): (TaskCustomization | TaskConfiguration)[] { + const invalidTaskConfigs: (TaskCustomization | TaskConfiguration)[] = []; + for (const taskConfigs of this.rawTaskConfigurations.values()) { + for (const taskConfig of taskConfigs) { + const isValid = this.isTaskConfigValid(taskConfig); + if (!isValid) { + invalidTaskConfigs.push(taskConfig); + } + } + } + return invalidTaskConfigs; + } + /** returns the task configuration for a given label or undefined if none */ getTask(rootFolderPath: string, taskLabel: string): TaskConfiguration | undefined { const labelConfigMap = this.tasksMap.get(rootFolderPath); @@ -358,6 +381,10 @@ export class TaskConfigurations implements Disposable { for (const [rootFolder, taskConfigs] of this.rawTaskConfigurations.entries()) { for (const taskConfig of taskConfigs) { + const isValid = this.isTaskConfigValid(taskConfig); + if (!isValid) { + continue; + } if (this.isDetectedTask(taskConfig)) { addCustomization(rootFolder, taskConfig); } else { @@ -370,6 +397,17 @@ export class TaskConfigurations implements Disposable { this.tasksMap = newTaskMap; } + /** + * Returns `true` if the given task configuration is valid as per the task schema defined in Theia + * or contributed by Theia extensions and plugins, `false` otherwise. + */ + private isTaskConfigValid(task: TaskCustomization): boolean { + const schema = this.taskSchemaUpdater.getTaskSchema(); + const ajv = new Ajv(); + const validateSchema = ajv.compile(schema); + return !!validateSchema({ tasks: [task] }); + } + /** * Updates the task config in the `tasks.json`. * The task config, together with updates, will be written into the `tasks.json` if it is not found in the file. diff --git a/packages/task/src/browser/task-schema-updater.ts b/packages/task/src/browser/task-schema-updater.ts index e056fcc438602..eb712dad52c62 100644 --- a/packages/task/src/browser/task-schema-updater.ts +++ b/packages/task/src/browser/task-schema-updater.ts @@ -13,6 +13,13 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +// This file is inspired by VSCode and partially copied from https://github.com/Microsoft/vscode/blob/1.33.1/src/vs/workbench/contrib/tasks/common/problemMatcher.ts +// 'problemMatcher.ts' copyright: +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + import { injectable, inject, postConstruct } from 'inversify'; import { JsonSchemaStore } from '@theia/core/lib/browser/json-schema-store'; import { InMemoryResources, deepClone } from '@theia/core/lib/common'; @@ -56,7 +63,7 @@ export class TaskSchemaUpdater { update(): void { const taskSchemaUri = new URI(taskSchemaId); - taskConfigurationSchema.oneOf = [processTaskConfigurationSchema, ...customizedDetectedTasks, ...customSchemas]; + taskConfigurationSchema.anyOf = [processTaskConfigurationSchema, ...customizedDetectedTasks, ...customSchemas]; const schemaContent = this.getStrigifiedTaskSchema(); try { @@ -256,27 +263,7 @@ const commandAndArgs = { args: commandArgSchema, options: commandOptionsSchema }; -const problemMatcher = { - oneOf: [ - { - type: 'string', - description: 'Name of the problem matcher to parse the output of the task', - enum: problemMatcherNames - }, - { - type: 'object', - description: 'User defined problem matcher(s) to parse the output of the task', - }, - { - type: 'array', - description: 'Name(s) of the problem matcher(s) to parse the output of the task', - items: { - type: 'string', - enum: problemMatcherNames - } - } - ] -}; + const group = { oneOf: [ { @@ -316,6 +303,225 @@ const group = { description: 'Defines to which execution group this task belongs to. It supports "build" to add it to the build group and "test" to add it to the test group.' }; +const problemPattern: IJSONSchema = { + default: { + regexp: '^([^\\\\s].*)\\\\((\\\\d+,\\\\d+)\\\\):\\\\s*(.*)$', + file: 1, + location: 2, + message: 3 + }, + type: 'object', + properties: { + regexp: { + type: 'string', + description: 'The regular expression to find an error, warning or info in the output.' + }, + kind: { + type: 'string', + description: 'whether the pattern matches a location (file and line) or only a file.' + }, + file: { + type: 'integer', + description: 'The match group index of the filename. If omitted 1 is used.' + }, + location: { + type: 'integer', + description: 'The match group index of the problem\'s location. Valid location patterns are: (line), (line,column) and (startLine,startColumn,endLine,endColumn). If omitted (line,column) is assumed.' + }, + line: { + type: 'integer', + description: 'The match group index of the problem\'s line. Defaults to 2' + }, + column: { + type: 'integer', + description: 'The match group index of the problem\'s line character. Defaults to 3' + }, + endLine: { + type: 'integer', + description: 'The match group index of the problem\'s end line. Defaults to undefined' + }, + endColumn: { + type: 'integer', + description: 'The match group index of the problem\'s end line character. Defaults to undefined' + }, + severity: { + type: 'integer', + description: 'The match group index of the problem\'s severity. Defaults to undefined' + }, + code: { + type: 'integer', + description: 'The match group index of the problem\'s code. Defaults to undefined' + }, + message: { + type: 'integer', + description: 'The match group index of the message. If omitted it defaults to 4 if location is specified. Otherwise it defaults to 5.' + }, + loop: { + type: 'boolean', + description: 'In a multi line matcher loop indicated whether this pattern is executed in a loop as long as it matches. Can only specified on a last pattern in a multi line pattern.' + } + } +}; + +const multiLineProblemPattern: IJSONSchema = { + type: 'array', + items: problemPattern +}; + +const watchingPattern: IJSONSchema = { + type: 'object', + additionalProperties: false, + properties: { + regexp: { + type: 'string', + description: 'The regular expression to detect the begin or end of a background task.' + }, + file: { + type: 'integer', + description: 'The match group index of the filename. Can be omitted.' + }, + } +}; + +const patternType: IJSONSchema = { + anyOf: [ + { + type: 'string', + description: 'The name of a contributed or predefined pattern' + }, + problemPattern, + multiLineProblemPattern + ], + description: 'A problem pattern or the name of a contributed or predefined problem pattern. Can be omitted if base is specified.' +}; + +const problemMatcherObject: IJSONSchema = { + type: 'object', + properties: { + base: { + type: 'string', + description: 'The name of a base problem matcher to use.' + }, + owner: { + type: 'string', + description: 'The owner of the problem inside Code. Can be omitted if base is specified. Defaults to \'external\' if omitted and base is not specified.' + }, + source: { + type: 'string', + description: 'A human-readable string describing the source of this diagnostic, e.g. \'typescript\' or \'super lint\'.' + }, + severity: { + type: 'string', + enum: ['error', 'warning', 'info'], + description: 'The default severity for captures problems. Is used if the pattern doesn\'t define a match group for severity.' + }, + applyTo: { + type: 'string', + enum: ['allDocuments', 'openDocuments', 'closedDocuments'], + description: 'Controls if a problem reported on a text document is applied only to open, closed or all documents.' + }, + pattern: patternType, + fileLocation: { + oneOf: [ + { + type: 'string', + enum: ['absolute', 'relative', 'autoDetect'] + }, + { + type: 'array', + items: { + type: 'string' + } + } + ], + description: 'Defines how file names reported in a problem pattern should be interpreted.' + }, + background: { + type: 'object', + additionalProperties: false, + description: 'Patterns to track the begin and end of a matcher active on a background task.', + properties: { + activeOnStart: { + type: 'boolean', + description: 'If set to true the background monitor is in active mode when the task starts. This is equals of issuing a line that matches the beginsPattern' + }, + beginsPattern: { + oneOf: [ + { + type: 'string' + }, + watchingPattern + ], + description: 'If matched in the output the start of a background task is signaled.' + }, + endsPattern: { + oneOf: [ + { + type: 'string' + }, + watchingPattern + ], + description: 'If matched in the output the end of a background task is signaled.' + } + } + }, + watching: { + type: 'object', + additionalProperties: false, + deprecationMessage: 'The watching property is deprecated. Use background instead.', + description: 'Patterns to track the begin and end of a watching matcher.', + properties: { + activeOnStart: { + type: 'boolean', + description: 'If set to true the watcher is in active mode when the task starts. This is equals of issuing a line that matches the beginPattern' + }, + beginsPattern: { + oneOf: [ + { + type: 'string' + }, + watchingPattern + ], + description: 'If matched in the output the start of a watching task is signaled.' + }, + endsPattern: { + oneOf: [ + { + type: 'string' + }, + watchingPattern + ], + description: 'If matched in the output the end of a watching task is signaled.' + } + } + } + } +}; + +const problemMatcher = { + anyOf: [ + { + type: 'string', + description: 'Name of the problem matcher to parse the output of the task', + enum: problemMatcherNames + }, + { + type: 'array', + description: 'Name(s) of the problem matcher(s) to parse the output of the task', + items: { + type: 'string', + enum: problemMatcherNames + } + }, + problemMatcherObject, + { + type: 'array', + description: 'User defined problem matcher(s) to parse the output of the task', + items: problemMatcherObject + } + ] +}; + const processTaskConfigurationSchema: IJSONSchema = { type: 'object', required: ['type', 'label', 'command'], @@ -349,5 +555,5 @@ const customSchemas: IJSONSchema[] = []; const taskConfigurationSchema: IJSONSchema = { $id: taskSchemaId, - oneOf: [processTaskConfigurationSchema, ...customizedDetectedTasks, ...customSchemas] + anyOf: [processTaskConfigurationSchema, ...customizedDetectedTasks, ...customSchemas] }; diff --git a/packages/task/src/browser/task-service.ts b/packages/task/src/browser/task-service.ts index f15507869ca6b..f40189232de51 100644 --- a/packages/task/src/browser/task-service.ts +++ b/packages/task/src/browser/task-service.ts @@ -14,7 +14,6 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import * as Ajv from 'ajv'; import { ApplicationShell, FrontendApplication, WidgetManager } from '@theia/core/lib/browser'; import { open, OpenerService } from '@theia/core/lib/browser/opener-service'; import { ILogger, CommandService } from '@theia/core/lib/common'; @@ -244,15 +243,7 @@ export class TaskService implements TaskConfigurationClient { /** Returns an array of the valid task configurations which are configured in tasks.json files */ async getConfiguredTasks(): Promise { - const taskConfigs = await this.taskConfigurations.getTasks(); - let invalidTaskConfig: TaskConfiguration | undefined; - const validTaskConfigs = taskConfigs.filter(t => { - const isValid = this.isTaskConfigValid(t); - if (!isValid) { - invalidTaskConfig = t; - } - return isValid; - }); + const invalidTaskConfig = this.taskConfigurations.getInvalidTaskConfigurations()[0]; if (invalidTaskConfig) { const widget = await this.widgetManager.getOrCreateWidget(PROBLEMS_WIDGET_ID); const isProblemsWidgetVisible = widget && widget.isVisible; @@ -280,18 +271,9 @@ export class TaskService implements TaskConfigurationClient { this.messageService.warn(warningMessage); } } - return validTaskConfigs; - } - /** - * Returns `true` if the given task configuration is valid as per the task schema defined in Theia - * or contributed by Theia extensions and plugins, `false` otherwise. - */ - isTaskConfigValid(task: TaskConfiguration): boolean { - const schema = this.taskSchemaUpdater.getTaskSchema(); - const ajv = new Ajv(); - const validateSchema = ajv.compile(schema); - return !!validateSchema({ tasks: [task] }); + const validTaskConfigs = await this.taskConfigurations.getTasks(); + return validTaskConfigs; } /** Returns an array of the task configurations which are provided by the extensions. */ diff --git a/packages/task/src/common/problem-matcher-protocol.ts b/packages/task/src/common/problem-matcher-protocol.ts index 92d2ecc7dd236..ec42f5b970b76 100644 --- a/packages/task/src/common/problem-matcher-protocol.ts +++ b/packages/task/src/common/problem-matcher-protocol.ts @@ -167,9 +167,9 @@ export namespace ProblemPattern { message: value.message, location: value.location, line: value.line, - character: value.character, + character: value.column || value.character, endLine: value.endLine, - endCharacter: value.endCharacter, + endCharacter: value.endColumn || value.endCharacter, code: value.code, severity: value.severity, loop: value.loop diff --git a/packages/task/src/common/task-protocol.ts b/packages/task/src/common/task-protocol.ts index a131f3fa03429..c623b723f9f35 100644 --- a/packages/task/src/common/task-protocol.ts +++ b/packages/task/src/common/task-protocol.ts @@ -189,8 +189,10 @@ export interface ProblemPatternContribution { location?: number; line?: number; character?: number; + column?: number; endLine?: number; endCharacter?: number; + endColumn?: number; code?: number; severity?: number; loop?: boolean;