Skip to content

Commit

Permalink
support having multiple user-defined problem matchers in tasks.json
Browse files Browse the repository at this point in the history
- resolved eclipse-theia#6567: With this change Theia support users defining a
collection of problem matchers in tasks.json
- added json schemas to validate the user-defined problem matchers in
tasks.json
- fixed the bug with task's schema to allow configured tasks and
customized detected tasks in the same tasks.json

Signed-off-by: Liang Huang <[email protected]>
  • Loading branch information
Liang Huang authored and akosyakov committed Feb 24, 2020
1 parent 1fa1159 commit 7a63577
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 48 deletions.
42 changes: 40 additions & 2 deletions packages/task/src/browser/task-configurations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<TaskConfiguration[]> {
const configuredTasks = Array.from(this.tasksMap.values()).reduce((acc, labelConfigMap) => acc.concat(Array.from(labelConfigMap.values())), [] as TaskConfiguration[]);
Expand All @@ -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);
Expand Down Expand Up @@ -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 {
Expand All @@ -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.
Expand Down
252 changes: 229 additions & 23 deletions packages/task/src/browser/task-schema-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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: [
{
Expand Down Expand Up @@ -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'],
Expand Down Expand Up @@ -349,5 +555,5 @@ const customSchemas: IJSONSchema[] = [];

const taskConfigurationSchema: IJSONSchema = {
$id: taskSchemaId,
oneOf: [processTaskConfigurationSchema, ...customizedDetectedTasks, ...customSchemas]
anyOf: [processTaskConfigurationSchema, ...customizedDetectedTasks, ...customSchemas]
};
Loading

0 comments on commit 7a63577

Please sign in to comment.