@camunda/linting 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/Linter.js CHANGED
@@ -1,10 +1,9 @@
1
1
  import BpmnModdle from 'bpmn-moddle/dist/index';
2
2
 
3
3
  import { Linter as BpmnLinter } from 'bpmnlint';
4
-
5
4
  import StaticResolver from 'bpmnlint/lib/resolver/static-resolver';
6
5
 
7
- import { configs } from 'bpmnlint-plugin-camunda-compat';
6
+ import Resolver from './Resolver';
8
7
 
9
8
  import { isString } from 'min-dash';
10
9
 
@@ -21,9 +20,13 @@ const moddle = new BpmnModdle({
21
20
 
22
21
  export class Linter {
23
22
  constructor(options = {}) {
24
- const { modeler = 'desktop' } = options;
23
+ const {
24
+ modeler = 'desktop',
25
+ plugins = []
26
+ } = options;
25
27
 
26
28
  this._modeler = modeler;
29
+ this._plugins = plugins;
27
30
  }
28
31
 
29
32
  async lint(contents) {
@@ -44,22 +47,18 @@ export class Linter {
44
47
 
45
48
  const configName = getConfigName(executionPlatform, executionPlatformVersion);
46
49
 
47
- const config = configs[ configName ];
50
+ const config = this._createConfig(configName);
48
51
 
49
- if (!config) {
50
- return [];
51
- }
52
-
53
- const rules = await importRules(config);
52
+ const resolver = await this._createResolver(configName);
54
53
 
55
54
  const linter = new BpmnLinter({
56
- config: prefixRules(config),
57
- resolver: new StaticResolver(rules)
55
+ config,
56
+ resolver
58
57
  });
59
58
 
60
- const allReports = await linter.lint(rootElement);
59
+ const reportsByRule = await linter.lint(rootElement);
61
60
 
62
- return Object.values(allReports).reduce((allReports, reports) => {
61
+ return Object.values(reportsByRule).reduce((allReports, reports) => {
63
62
  return [
64
63
  ...allReports,
65
64
  ...reports.map(report => {
@@ -80,6 +79,46 @@ export class Linter {
80
79
  ];
81
80
  }, []);
82
81
  }
82
+
83
+ _createConfig(configName) {
84
+ const configs = [
85
+ {
86
+ extends: `plugin:bpmnlint-plugin-camunda-compat/${ configName }`
87
+ },
88
+ ...this._plugins.map(({ config }) => config)
89
+ ];
90
+
91
+ return configs.reduce(
92
+ (config, _config) => {
93
+ let { extends: _extends = [], rules = {} } = _config;
94
+
95
+ if (isString(_extends)) {
96
+ _extends = [ _extends ];
97
+ }
98
+
99
+ return {
100
+ extends: [ ...config.extends, ..._extends ],
101
+ rules: {
102
+ ...config.rules,
103
+ ...rules
104
+ }
105
+ };
106
+ },
107
+ {
108
+ extends: [],
109
+ rules: {}
110
+ }
111
+ );
112
+ }
113
+
114
+ async _createResolver(configName) {
115
+ const cache = await createCache(configName);
116
+
117
+ return new Resolver([
118
+ new StaticResolver(cache),
119
+ ...this._plugins.map(({ resolver }) => resolver)
120
+ ]);
121
+ }
83
122
  }
84
123
 
85
124
  function getConfigName(executionPlatform, executionPlatformVersion) {
@@ -89,32 +128,6 @@ function getConfigName(executionPlatform, executionPlatformVersion) {
89
128
  ].join('-');
90
129
  }
91
130
 
92
- function prefixRules({ rules }) {
93
- return {
94
- rules: Object.entries(rules).reduce((rules, [ key, value ]) => {
95
- return {
96
- ...rules,
97
- [ `bpmnlint-plugin-camunda-compat/${ key }` ]: value
98
- };
99
- }, {})
100
- };
101
- }
102
-
103
- async function importRules({ rules }) {
104
- let importedRules = {};
105
-
106
- for (let key of Object.keys(rules)) {
107
- const importedRule = require(`bpmnlint-plugin-camunda-compat/rules/${ key }`);
108
-
109
- importedRules = {
110
- ...importedRules,
111
- [ `rule:bpmnlint-plugin-camunda-compat/${ key }` ]: importedRule
112
- };
113
- }
114
-
115
- return importedRules;
116
- }
117
-
118
131
  const executionPlatformLabels = {
119
132
  'Camunda Cloud': {
120
133
  '1.0': 'Camunda 8 (Zeebe 1.0)',
@@ -135,4 +148,36 @@ function toLowerCase(string) {
135
148
 
136
149
  function toSemverMinor(executionPlatformVersion) {
137
150
  return executionPlatformVersion.split('.').slice(0, 2).join('.');
151
+ }
152
+
153
+ async function createCache(configName) {
154
+ let config = require('bpmnlint-plugin-camunda-compat').configs[ configName ];
155
+
156
+ if (!config) {
157
+ config = {
158
+ rules: {}
159
+ };
160
+ }
161
+
162
+ const rules = await requireRules(config);
163
+
164
+ return {
165
+ [ `config:bpmnlint-plugin-camunda-compat/${ configName }` ]: config,
166
+ ...rules
167
+ };
168
+ }
169
+
170
+ function requireRules({ rules }) {
171
+ let requiredRules = {};
172
+
173
+ for (let ruleName of Object.keys(rules)) {
174
+ const requiredRule = require(`bpmnlint-plugin-camunda-compat/rules/${ ruleName }`);
175
+
176
+ requiredRules = {
177
+ ...requiredRules,
178
+ [ `rule:bpmnlint-plugin-camunda-compat/${ ruleName }` ]: requiredRule
179
+ };
180
+ }
181
+
182
+ return requiredRules;
138
183
  }
@@ -0,0 +1,31 @@
1
+ export default class NestedResolver {
2
+ constructor(resolvers) {
3
+ this.resolvers = resolvers.slice().reverse();
4
+ }
5
+
6
+ resolveRule(pkg, ruleName) {
7
+ for (const resolver of this.resolvers) {
8
+ try {
9
+ return resolver.resolveRule(pkg, ruleName);
10
+ } catch (err) {
11
+
12
+ // ignore
13
+ }
14
+ }
15
+
16
+ throw new Error(`unknown rule <${ pkg }/${ ruleName }>`);
17
+ }
18
+
19
+ resolveConfig(pkg, configName) {
20
+ for (const resolver of this.resolvers) {
21
+ try {
22
+ return resolver.resolveConfig(pkg, configName);
23
+ } catch (err) {
24
+
25
+ // ignore
26
+ }
27
+ }
28
+
29
+ throw new Error(`unknown config <${ pkg }/${ configName }>`);
30
+ }
31
+ }
@@ -28,12 +28,17 @@ export default class Linting {
28
28
  this._canvas.scrollToElement(element);
29
29
  }
30
30
 
31
- this._selection.select(element);
31
+ if (element !== this._canvas.getRootElement()) {
32
+ this._selection.select(element);
33
+ }
32
34
 
33
35
  const { entryId } = propertiesPanel;
34
36
 
35
- this._eventBus.fire('propertiesPanel.showEntry', {
36
- id: entryId
37
+ // TODO(philippfromme): remove timeout once properties panel is fixed
38
+ setTimeout(() => {
39
+ this._eventBus.fire('propertiesPanel.showEntry', {
40
+ id: entryId
41
+ });
37
42
  });
38
43
  }
39
44
 
@@ -9,7 +9,8 @@ const errorSvg = `
9
9
  `;
10
10
 
11
11
  export default class LintingAnnotations {
12
- constructor(elementRegistry, eventBus, overlays) {
12
+ constructor(canvas, elementRegistry, eventBus, overlays) {
13
+ this._canvas = canvas;
13
14
  this._elementRegistry = elementRegistry;
14
15
  this._eventBus = eventBus;
15
16
  this._overlays = overlays;
@@ -35,7 +36,7 @@ export default class LintingAnnotations {
35
36
  Object.entries(reportsByElement).forEach(([ id, reports ]) => {
36
37
  const element = this._elementRegistry.get(id);
37
38
 
38
- if (!element) {
39
+ if (!element || element === this._canvas.getRootElement()) {
39
40
  return;
40
41
  }
41
42
 
@@ -66,6 +67,7 @@ export default class LintingAnnotations {
66
67
  }
67
68
 
68
69
  LintingAnnotations.$inject = [
70
+ 'canvas',
69
71
  'elementRegistry',
70
72
  'eventBus',
71
73
  'overlays'
@@ -1,12 +1,19 @@
1
1
  import { ERROR_TYPES } from 'bpmnlint-plugin-camunda-compat/rules/utils/element';
2
2
 
3
3
  import { is } from 'bpmnlint-utils';
4
- import { every } from 'min-dash';
5
-
6
- import { isArray } from 'min-dash';
4
+ import {
5
+ every,
6
+ isArray
7
+ } from 'min-dash';
7
8
 
8
9
  import { getTypeString } from './types';
9
10
 
11
+ const TIMER_PROPERTIES = [
12
+ 'timeDate',
13
+ 'timeDuration',
14
+ 'timeCycle'
15
+ ];
16
+
10
17
  export function getErrorMessage(report, executionPlatformLabel, modeler = 'desktop') {
11
18
  const {
12
19
  error,
@@ -14,7 +21,7 @@ export function getErrorMessage(report, executionPlatformLabel, modeler = 'deskt
14
21
  } = report;
15
22
 
16
23
  if (!error) {
17
- return report;
24
+ return message;
18
25
  }
19
26
 
20
27
  const { type } = error;
@@ -51,6 +58,10 @@ export function getErrorMessage(report, executionPlatformLabel, modeler = 'deskt
51
58
  return getPropertyValueDuplicatedErrorMessage(report);
52
59
  }
53
60
 
61
+ if (type === ERROR_TYPES.PROPERTY_VALUE_NOT_ALLOWED) {
62
+ return getPropertyValueNotAllowedErrorMessage(report, executionPlatformLabel);
63
+ }
64
+
54
65
  return message;
55
66
  }
56
67
 
@@ -293,6 +304,24 @@ function getPropertyRequiredErrorMessage(report, executionPlatformLabel) {
293
304
  return `${ getIndefiniteArticle(typeString) } <${ typeString }> must have a defined <Condition expression> or be the default <Sequence Flow>`;
294
305
  }
295
306
 
307
+ if (is(node, 'bpmn:TimerEventDefinition')
308
+ && isArray(requiredProperty)
309
+ && TIMER_PROPERTIES.some(property => requiredProperty.includes(property))
310
+ ) {
311
+ return `${ getIndefiniteArticle(typeString) } <${ typeString }> must have a defined <Timer type>`;
312
+ }
313
+
314
+ if (is(node, 'bpmn:TimerEventDefinition') && requiredProperty === 'timeDuration') {
315
+ return `${ getIndefiniteArticle(typeString) } <${ typeString }> must have a defined <Timer duration>`;
316
+ }
317
+
318
+ if (is(node, 'bpmn:FormalExpression')
319
+ && requiredProperty === 'body'
320
+ && TIMER_PROPERTIES.includes(secondLast(report.path))
321
+ ) {
322
+ return `${ getIndefiniteArticle(typeString) } <${ typeString }> must have a defined <Timer value>`;
323
+ }
324
+
296
325
  return message;
297
326
  }
298
327
 
@@ -315,4 +344,43 @@ function getPropertyTypeNotAllowedErrorMessage(report, executionPlatformLabel) {
315
344
  }
316
345
 
317
346
  return message;
318
- }
347
+ }
348
+
349
+ function getPropertyValueNotAllowedErrorMessage(report, executionPlatformLabel) {
350
+ const {
351
+ error
352
+ } = report;
353
+
354
+ const {
355
+ node,
356
+ parentNode,
357
+ property
358
+ } = error;
359
+
360
+ const typeString = getTypeString(parentNode || node);
361
+
362
+ if (is(node, 'bpmn:FormalExpression')
363
+ && property === 'body'
364
+ && secondLast(report.path) === 'timeCycle'
365
+ ) {
366
+ return `${ getIndefiniteArticle(typeString) } <${ typeString }> <Time cycle> should be an expression, an ISO 8601 repeating interval, or a cron expression (cron requires Camunda Platform 8.1 or newer)`;
367
+ }
368
+
369
+ if (is(node, 'bpmn:FormalExpression')
370
+ && property === 'body'
371
+ && secondLast(report.path) === 'timeDate'
372
+ ) {
373
+ return `${ getIndefiniteArticle(typeString) } <${ typeString }> <Time date> should be an expression, or an ISO 8601 date`;
374
+ }
375
+
376
+ if (is(node, 'bpmn:FormalExpression')
377
+ && property === 'body'
378
+ && secondLast(report.path) === 'timeDuration'
379
+ ) {
380
+ return `${ getIndefiniteArticle(typeString) } <${ typeString }> <Time duration> should be an expression, or an ISO 8601 interval`;
381
+ }
382
+ }
383
+
384
+ function secondLast(array) {
385
+ return array[array.length - 2];
386
+ }
@@ -4,6 +4,12 @@ import { is } from 'bpmnlint-utils';
4
4
 
5
5
  import { ERROR_TYPES } from 'bpmnlint-plugin-camunda-compat/rules/utils/error-types';
6
6
 
7
+ const TIMER_PROPERTIES = [
8
+ 'timeDate',
9
+ 'timeDuration',
10
+ 'timeCycle'
11
+ ];
12
+
7
13
  /**
8
14
  * Get errors for a given element.
9
15
  *
@@ -31,7 +37,7 @@ export function getErrors(reports, element) {
31
37
  ...ids.reduce((errors, id) => {
32
38
  return {
33
39
  ...errors,
34
- [ id ]: getErrorMessage(id) || message
40
+ [ id ]: getErrorMessage(id, report) || message
35
41
  };
36
42
  }, {})
37
43
  };
@@ -134,10 +140,28 @@ export function getEntryIds(report) {
134
140
  return [ 'conditionExpression' ];
135
141
  }
136
142
 
143
+ if (isPropertyRequiredError(error, 'timeDuration', 'bpmn:TimerEventDefinition')) {
144
+ return [ 'timerEventDefinitionDurationValue' ];
145
+ }
146
+
147
+ if (TIMER_PROPERTIES.some(property =>
148
+ isOneOfPropertiesRequiredError(error, property , 'bpmn:TimerEventDefinition'))
149
+ ) {
150
+ return [ 'timerEventDefinitionType' ];
151
+ }
152
+
153
+ if (isPropertyRequiredError(error, 'body', 'bpmn:FormalExpression') && TIMER_PROPERTIES.includes(secondLast(report.path))) {
154
+ return hasOnlyDurationTimer(error.parentNode) ? [ 'timerEventDefinitionDurationValue' ] : [ 'timerEventDefinitionValue' ];
155
+ }
156
+
157
+ if (isPropertyValueNotAllowedError(error, 'body', 'bpmn:FormalExpression') && TIMER_PROPERTIES.includes(secondLast(report.path))) {
158
+ return hasOnlyDurationTimer(error.parentNode) ? [ 'timerEventDefinitionDurationValue' ] : [ 'timerEventDefinitionValue' ];
159
+ }
160
+
137
161
  return [];
138
162
  }
139
163
 
140
- export function getErrorMessage(id) {
164
+ export function getErrorMessage(id, report) {
141
165
  if (id === 'businessRuleImplementation') {
142
166
  return 'Implementation must be defined.';
143
167
  }
@@ -182,7 +206,7 @@ export function getErrorMessage(id) {
182
206
  return 'Process ID must be defined.';
183
207
  }
184
208
 
185
- if (id === 'taskDefinitionType') {
209
+ if (id === 'taskDefinitionType' || id === 'timerEventDefinitionType') {
186
210
  return 'Type must be defined.';
187
211
  }
188
212
 
@@ -209,6 +233,31 @@ export function getErrorMessage(id) {
209
233
  if (id === 'conditionExpression') {
210
234
  return 'Condition expression must be defined.';
211
235
  }
236
+
237
+ if (id === 'timerEventDefinitionDurationValue') {
238
+ return report.error.type === ERROR_TYPES.PROPERTY_REQUIRED ?
239
+ 'Duration must be defined.' : 'Must be an expression, or an ISO 8601 interval.';
240
+ }
241
+
242
+ if (id === 'timerEventDefinitionValue') {
243
+ if (report.error.type === ERROR_TYPES.PROPERTY_REQUIRED) {
244
+ return 'Value must be defined.';
245
+ }
246
+
247
+ const property = secondLast(report.path);
248
+
249
+ if (property === 'timeCycle') {
250
+ return 'Must be an expression, an ISO 8601 repeating interval, or a cron expression (cron requires Camunda Platform 8.1 or newer).';
251
+ }
252
+
253
+ if (property === 'timeDate') {
254
+ return 'Must be an expression, or an ISO 8601 date.';
255
+ }
256
+
257
+ if (property === 'timeDuration') {
258
+ return 'Must be an expression, or an ISO 8601 interval.';
259
+ }
260
+ }
212
261
  }
213
262
 
214
263
  function isExtensionElementNotAllowedError(error, extensionElement, type) {
@@ -232,7 +281,13 @@ function isPropertyDependendRequiredError(error, dependendRequiredProperty, type
232
281
 
233
282
  function isPropertyRequiredError(error, requiredProperty, type) {
234
283
  return error.type === ERROR_TYPES.PROPERTY_REQUIRED
235
- && error.requiredProperty === requiredProperty
284
+ && (error.requiredProperty === requiredProperty)
285
+ && (!type || is(error.node, type));
286
+ }
287
+
288
+ function isOneOfPropertiesRequiredError(error, requiredProperty, type) {
289
+ return error.type === ERROR_TYPES.PROPERTY_REQUIRED
290
+ && (isArray(error.requiredProperty) && error.requiredProperty.includes(requiredProperty))
236
291
  && (!type || is(error.node, type));
237
292
  }
238
293
 
@@ -243,6 +298,24 @@ function isPropertyValueDuplicatedError(error, propertiesName, duplicatedPropert
243
298
  && (!type || is(error.node, type));
244
299
  }
245
300
 
301
+ function isPropertyValueNotAllowedError(error, propertyName, type) {
302
+ return error.type === ERROR_TYPES.PROPERTY_VALUE_NOT_ALLOWED
303
+ && error.property === propertyName
304
+ && (!type || is(error.node, type));
305
+ }
306
+
246
307
  function getBusinessObject(element) {
247
308
  return element.businessObject || element;
248
- }
309
+ }
310
+
311
+ function secondLast(array) {
312
+ return array[array.length - 2];
313
+ }
314
+
315
+ function hasOnlyDurationTimer(node) {
316
+ return is(node, 'bpmn:IntermediateCatchEvent') || isInterruptingBoundaryEvent(node);
317
+ }
318
+
319
+ function isInterruptingBoundaryEvent(event) {
320
+ return is(event, 'bpmn:BoundaryEvent') && event.get('cancelActivity') !== false;
321
+ }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@camunda/linting",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Linting for Camunda Platform",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "all": "npm run lint && npm test",
8
+ "dev": "npm run test:watch",
8
9
  "lint": "eslint .",
9
10
  "start": "cross-env SINGLE_START=modeler npm run test:watch",
10
11
  "test": "karma start",
@@ -24,12 +25,12 @@
24
25
  },
25
26
  "license": "MIT",
26
27
  "dependencies": {
27
- "bpmn-moddle": "^7.1.2",
28
- "bpmnlint": "^7.8.0",
29
- "bpmnlint-plugin-camunda-compat": "^0.12.0",
28
+ "bpmn-moddle": "^7.1.3",
29
+ "bpmnlint": "^8.0.0",
30
+ "bpmnlint-plugin-camunda-compat": "^0.13.1",
30
31
  "bpmnlint-utils": "^1.0.2",
31
- "min-dash": "^3.8.1",
32
- "min-dom": "^3.2.1",
32
+ "min-dash": "^4.0.0",
33
+ "min-dom": "^4.0.1",
33
34
  "zeebe-bpmn-moddle": "^0.15.0"
34
35
  },
35
36
  "devDependencies": {
@@ -38,8 +39,8 @@
38
39
  "bpmn-js-properties-panel": "^1.6.1",
39
40
  "chai": "^4.3.6",
40
41
  "cross-env": "^7.0.3",
41
- "eslint": "^4.11.0",
42
- "eslint-plugin-bpmn-io": "^0.4.1",
42
+ "eslint": "^8.23.1",
43
+ "eslint-plugin-bpmn-io": "^0.16.0",
43
44
  "karma": "^6.4.0",
44
45
  "karma-chrome-launcher": "^3.1.1",
45
46
  "karma-debug-launcher": "0.0.5",
@@ -47,7 +48,7 @@
47
48
  "karma-mocha": "^2.0.1",
48
49
  "karma-sinon-chai": "^2.0.2",
49
50
  "karma-webpack": "^5.0.0",
50
- "mocha": "^4.0.1",
51
+ "mocha": "^10.0.0",
51
52
  "mocha-test-container-support": "^0.2.0",
52
53
  "modeler-moddle": "^0.2.0",
53
54
  "puppeteer": "^16.2.0",