@camunda/linting 0.6.1 → 0.7.1

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,17 +1,16 @@
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
 
11
10
  import modelerModdle from 'modeler-moddle/resources/modeler.json';
12
11
  import zeebeModdle from 'zeebe-bpmn-moddle/resources/zeebe.json';
13
12
 
14
- import { getErrorMessage } from './utils/error-messages';
13
+ import { getErrorMessage, getExecutionPlatformLabel } from './utils/error-messages';
15
14
  import { getEntryIds } from './utils/properties-panel';
16
15
 
17
16
  const moddle = new BpmnModdle({
@@ -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 ];
48
-
49
- if (!config) {
50
- return [];
51
- }
50
+ const config = this._createConfig(configName);
52
51
 
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,50 +128,42 @@ 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
- };
131
+ function toLowerCase(string) {
132
+ return string.toLowerCase();
101
133
  }
102
134
 
103
- async function importRules({ rules }) {
104
- let importedRules = {};
135
+ function toSemverMinor(executionPlatformVersion) {
136
+ return executionPlatformVersion.split('.').slice(0, 2).join('.');
137
+ }
105
138
 
106
- for (let key of Object.keys(rules)) {
107
- const importedRule = require(`bpmnlint-plugin-camunda-compat/rules/${ key }`);
139
+ async function createCache(configName) {
140
+ let config = require('bpmnlint-plugin-camunda-compat').configs[ configName ];
108
141
 
109
- importedRules = {
110
- ...importedRules,
111
- [ `rule:bpmnlint-plugin-camunda-compat/${ key }` ]: importedRule
142
+ if (!config) {
143
+ config = {
144
+ rules: {}
112
145
  };
113
146
  }
114
147
 
115
- return importedRules;
148
+ const rules = await requireRules(config);
149
+
150
+ return {
151
+ [ `config:bpmnlint-plugin-camunda-compat/${ configName }` ]: config,
152
+ ...rules
153
+ };
116
154
  }
117
155
 
118
- const executionPlatformLabels = {
119
- 'Camunda Cloud': {
120
- '1.0': 'Camunda 8 (Zeebe 1.0)',
121
- '1.1': 'Camunda 8 (Zeebe 1.1)',
122
- '1.2': 'Camunda 8 (Zeebe 1.2)',
123
- '1.3': 'Camunda 8 (Zeebe 1.3)',
124
- '8.0': 'Camunda 8'
125
- }
126
- };
156
+ function requireRules({ rules }) {
157
+ let requiredRules = {};
127
158
 
128
- function getExecutionPlatformLabel(executionPlatform, executionPlatformVersion) {
129
- return executionPlatformLabels[ executionPlatform ] && executionPlatformLabels[ executionPlatform ][ executionPlatformVersion ];
130
- }
159
+ for (let ruleName of Object.keys(rules)) {
160
+ const requiredRule = require(`bpmnlint-plugin-camunda-compat/rules/${ ruleName }`);
131
161
 
132
- function toLowerCase(string) {
133
- return string.toLowerCase();
134
- }
162
+ requiredRules = {
163
+ ...requiredRules,
164
+ [ `rule:bpmnlint-plugin-camunda-compat/${ ruleName }` ]: requiredRule
165
+ };
166
+ }
135
167
 
136
- function toSemverMinor(executionPlatformVersion) {
137
- return executionPlatformVersion.split('.').slice(0, 2).join('.');
168
+ return requiredRules;
138
169
  }
@@ -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,7 +28,9 @@ 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
 
@@ -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,9 +58,38 @@ 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
 
68
+ const executionPlatformLabels = {
69
+ 'Camunda Cloud': {
70
+ 'defaultPlatformName': 'Camunda',
71
+ '1.0': 'Camunda 8 (Zeebe 1.0)',
72
+ '1.1': 'Camunda 8 (Zeebe 1.1)',
73
+ '1.2': 'Camunda 8 (Zeebe 1.2)',
74
+ '1.3': 'Camunda 8 (Zeebe 1.3)',
75
+ '8.0': 'Camunda 8'
76
+ }
77
+ };
78
+
79
+ export function getExecutionPlatformLabel(executionPlatform, executionPlatformVersion) {
80
+ const translatedLabel = executionPlatformLabels[ executionPlatform ] && executionPlatformLabels[ executionPlatform ][ executionPlatformVersion ];
81
+
82
+ if (translatedLabel) {
83
+ return translatedLabel;
84
+ }
85
+
86
+ if (executionPlatformLabels[executionPlatform] && executionPlatformLabels[executionPlatform]['defaultPlatformName']) {
87
+ executionPlatform = executionPlatformLabels[executionPlatform]['defaultPlatformName'];
88
+ }
89
+
90
+ return `${ executionPlatform } ${ executionPlatformVersion }`;
91
+ }
92
+
57
93
  function getIndefiniteArticle(type) {
58
94
  if ([
59
95
  'Error',
@@ -293,6 +329,24 @@ function getPropertyRequiredErrorMessage(report, executionPlatformLabel) {
293
329
  return `${ getIndefiniteArticle(typeString) } <${ typeString }> must have a defined <Condition expression> or be the default <Sequence Flow>`;
294
330
  }
295
331
 
332
+ if (is(node, 'bpmn:TimerEventDefinition')
333
+ && isArray(requiredProperty)
334
+ && TIMER_PROPERTIES.some(property => requiredProperty.includes(property))
335
+ ) {
336
+ return `${ getIndefiniteArticle(typeString) } <${ typeString }> must have a defined <Timer type>`;
337
+ }
338
+
339
+ if (is(node, 'bpmn:TimerEventDefinition') && requiredProperty === 'timeDuration') {
340
+ return `${ getIndefiniteArticle(typeString) } <${ typeString }> must have a defined <Timer duration>`;
341
+ }
342
+
343
+ if (is(node, 'bpmn:FormalExpression')
344
+ && requiredProperty === 'body'
345
+ && TIMER_PROPERTIES.includes(secondLast(report.path))
346
+ ) {
347
+ return `${ getIndefiniteArticle(typeString) } <${ typeString }> must have a defined <Timer value>`;
348
+ }
349
+
296
350
  return message;
297
351
  }
298
352
 
@@ -315,4 +369,43 @@ function getPropertyTypeNotAllowedErrorMessage(report, executionPlatformLabel) {
315
369
  }
316
370
 
317
371
  return message;
318
- }
372
+ }
373
+
374
+ function getPropertyValueNotAllowedErrorMessage(report, executionPlatformLabel) {
375
+ const {
376
+ error
377
+ } = report;
378
+
379
+ const {
380
+ node,
381
+ parentNode,
382
+ property
383
+ } = error;
384
+
385
+ const typeString = getTypeString(parentNode || node);
386
+
387
+ if (is(node, 'bpmn:FormalExpression')
388
+ && property === 'body'
389
+ && secondLast(report.path) === 'timeCycle'
390
+ ) {
391
+ 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)`;
392
+ }
393
+
394
+ if (is(node, 'bpmn:FormalExpression')
395
+ && property === 'body'
396
+ && secondLast(report.path) === 'timeDate'
397
+ ) {
398
+ return `${ getIndefiniteArticle(typeString) } <${ typeString }> <Time date> should be an expression, or an ISO 8601 date`;
399
+ }
400
+
401
+ if (is(node, 'bpmn:FormalExpression')
402
+ && property === 'body'
403
+ && secondLast(report.path) === 'timeDuration'
404
+ ) {
405
+ return `${ getIndefiniteArticle(typeString) } <${ typeString }> <Time duration> should be an expression, or an ISO 8601 interval`;
406
+ }
407
+ }
408
+
409
+ function secondLast(array) {
410
+ return array[array.length - 2];
411
+ }
@@ -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.1",
3
+ "version": "0.7.1",
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",