bpmnlint-plugin-camunda-compat 2.52.0 → 2.53.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/index.js
CHANGED
|
@@ -36,6 +36,7 @@ const camundaCloud10Rules = withConfig({
|
|
|
36
36
|
'user-task-form': 'error',
|
|
37
37
|
'variable-name': 'error',
|
|
38
38
|
'feel': 'error',
|
|
39
|
+
'feel-compatibility': 'error',
|
|
39
40
|
'bpmnlint/start-event-required': 'error',
|
|
40
41
|
}, { version: '1.0' });
|
|
41
42
|
|
|
@@ -182,6 +183,7 @@ const rules = {
|
|
|
182
183
|
'executable-process': './rules/camunda-cloud/executable-process',
|
|
183
184
|
'execution-listener': './rules/camunda-cloud/execution-listener',
|
|
184
185
|
'feel': './rules/camunda-cloud/feel',
|
|
186
|
+
'feel-compatibility': './rules/camunda-cloud/feel-compatibility',
|
|
185
187
|
'history-time-to-live': './rules/camunda-platform/history-time-to-live',
|
|
186
188
|
'implementation': './rules/camunda-cloud/implementation',
|
|
187
189
|
'inclusive-gateway': './rules/camunda-cloud/inclusive-gateway',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bpmnlint-plugin-camunda-compat",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.53.0",
|
|
4
4
|
"description": "A bpmnlint plug-in for Camunda compatibility",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -37,9 +37,10 @@
|
|
|
37
37
|
"zeebe-bpmn-moddle": "^1.13.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@bpmn-io/feel-
|
|
40
|
+
"@bpmn-io/feel-analyzer": "^0.3.0",
|
|
41
41
|
"@bpmn-io/moddle-utils": "^0.3.0",
|
|
42
|
-
"@
|
|
42
|
+
"@bpmn-io/semver-compat": "^0.1.0",
|
|
43
|
+
"@camunda/feel-builtins": "^1.2.0",
|
|
43
44
|
"bpmnlint-utils": "^1.1.1",
|
|
44
45
|
"min-dash": "^5.0.0",
|
|
45
46
|
"semver-compare": "^1.0.0"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const { is } = require('bpmnlint-utils');
|
|
2
|
+
|
|
3
|
+
const { getPath } = require('@bpmn-io/moddle-utils');
|
|
4
|
+
const { camundaBuiltins, camundaReservedNameBuiltins } = require('@camunda/feel-builtins');
|
|
5
|
+
|
|
6
|
+
const { FeelAnalyzer } = require('@bpmn-io/feel-analyzer');
|
|
7
|
+
const { isCompatible } = require('@bpmn-io/semver-compat');
|
|
8
|
+
|
|
9
|
+
const { reportErrors } = require('../utils/reporter');
|
|
10
|
+
|
|
11
|
+
const { ERROR_TYPES } = require('../utils/error-types');
|
|
12
|
+
|
|
13
|
+
const { skipInNonExecutableProcess } = require('../utils/rule');
|
|
14
|
+
|
|
15
|
+
const { findParentNode } = require('../utils/element');
|
|
16
|
+
|
|
17
|
+
const { isFeelProperty } = require('./utils/feel');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reports calls to FEEL builtins that aren't available in the target Camunda
|
|
21
|
+
* engine (configured via `config.version`).
|
|
22
|
+
*/
|
|
23
|
+
module.exports = skipInNonExecutableProcess(function(config = {}) {
|
|
24
|
+
|
|
25
|
+
const { version } = config;
|
|
26
|
+
const builtins = [ ...camundaBuiltins, ...camundaReservedNameBuiltins ];
|
|
27
|
+
const unavailableByName = getUnavailableBuiltins(builtins, version);
|
|
28
|
+
|
|
29
|
+
const feelAnalyzer = new FeelAnalyzer({
|
|
30
|
+
parserDialect: 'camunda',
|
|
31
|
+
builtins: camundaBuiltins,
|
|
32
|
+
reservedNameBuiltins: camundaReservedNameBuiltins
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function check(node, reporter) {
|
|
36
|
+
if (is(node, 'bpmn:Expression')) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!unavailableByName.size) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const parentNode = findParentNode(node);
|
|
45
|
+
|
|
46
|
+
if (!parentNode) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const errors = [];
|
|
51
|
+
|
|
52
|
+
Object.entries(node).forEach(([ propertyName, propertyValue ]) => {
|
|
53
|
+
if (propertyValue && is(propertyValue, 'bpmn:Expression')) {
|
|
54
|
+
propertyValue = propertyValue.get('body');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!isFeelProperty(node, propertyName, propertyValue)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const expression = propertyValue.substring(1);
|
|
62
|
+
|
|
63
|
+
const { valid, functions = [] } = feelAnalyzer.analyzeExpression(expression);
|
|
64
|
+
|
|
65
|
+
// Skip invalid expressions; the `feel` rule reports the syntax error
|
|
66
|
+
if (!valid) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const unsupported = functions.find(
|
|
71
|
+
({ name, type }) => type === 'builtin' && unavailableByName.has(name)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (unsupported) {
|
|
75
|
+
const builtin = unavailableByName.get(unsupported.name);
|
|
76
|
+
const path = getPath(node, parentNode);
|
|
77
|
+
|
|
78
|
+
errors.push({
|
|
79
|
+
message: `FEEL function <${ builtin.name }> requires Camunda ${ builtin.engines.camunda }`,
|
|
80
|
+
path: path
|
|
81
|
+
? [ ...path, propertyName ]
|
|
82
|
+
: [ propertyName ],
|
|
83
|
+
data: {
|
|
84
|
+
type: ERROR_TYPES.FEEL_EXPRESSION_UNSUPPORTED_FUNCTION,
|
|
85
|
+
node,
|
|
86
|
+
parentNode,
|
|
87
|
+
property: propertyName
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (errors && errors.length) {
|
|
94
|
+
reportErrors(parentNode, reporter, errors);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
check
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Returns a map of builtin names to their definitions for builtins
|
|
105
|
+
* that are incompatible with the provided engine versions.
|
|
106
|
+
*
|
|
107
|
+
* @param { Array<{ name: string, engines?: Record<string, string> }> } builtins
|
|
108
|
+
* @param { string } version
|
|
109
|
+
* @returns { Map<string, { name: string, engines: Record<string, string> }> }
|
|
110
|
+
*/
|
|
111
|
+
function getUnavailableBuiltins(builtins, version) {
|
|
112
|
+
const unavailable = new Map();
|
|
113
|
+
|
|
114
|
+
if (!version) {
|
|
115
|
+
return unavailable;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const builtin of builtins) {
|
|
119
|
+
if (unavailable.has(builtin.name)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (builtin.engines && !isCompatible(builtin.engines, { camunda: version })) {
|
|
124
|
+
unavailable.set(builtin.name, builtin);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return unavailable;
|
|
129
|
+
}
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { is } = require('bpmnlint-utils');
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
is,
|
|
5
|
-
isAny
|
|
6
|
-
} = require('bpmnlint-utils');
|
|
3
|
+
const { getPath } = require('@bpmn-io/moddle-utils');
|
|
7
4
|
|
|
8
|
-
const {
|
|
5
|
+
const { FeelAnalyzer } = require('@bpmn-io/feel-analyzer');
|
|
9
6
|
|
|
10
|
-
const { getPath } = require('@bpmn-io/moddle-utils');
|
|
11
7
|
const { camundaReservedNameBuiltins } = require('@camunda/feel-builtins');
|
|
12
8
|
|
|
13
9
|
const { reportErrors } = require('../utils/reporter');
|
|
@@ -17,22 +13,16 @@ const { ERROR_TYPES } = require('../utils/error-types');
|
|
|
17
13
|
const { skipInNonExecutableProcess } = require('../utils/rule');
|
|
18
14
|
const { annotateRule } = require('../helper');
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
// Properties ignored only for specific element types
|
|
26
|
-
const IGNORED_PROPERTIES_BY_TYPE = {
|
|
27
|
-
'zeebe:Input': [ 'target' ],
|
|
28
|
-
'zeebe:Output': [ 'target' ],
|
|
29
|
-
'zeebe:Header': [ 'key', 'value' ],
|
|
30
|
-
'zeebe:Property': [ 'name', 'value' ],
|
|
31
|
-
'zeebe:CalledDecision': [ 'resultVariable' ],
|
|
32
|
-
'zeebe:Script': [ 'resultVariable' ]
|
|
33
|
-
};
|
|
16
|
+
const { findParentNode } = require('../utils/element');
|
|
17
|
+
|
|
18
|
+
const { isFeelProperty } = require('./utils/feel');
|
|
34
19
|
|
|
35
20
|
module.exports = skipInNonExecutableProcess(function() {
|
|
21
|
+
const feelAnalyzer = new FeelAnalyzer({
|
|
22
|
+
parserDialect: 'camunda',
|
|
23
|
+
reservedNameBuiltins: camundaReservedNameBuiltins
|
|
24
|
+
});
|
|
25
|
+
|
|
36
26
|
function check(node, reporter) {
|
|
37
27
|
if (is(node, 'bpmn:Expression')) {
|
|
38
28
|
return;
|
|
@@ -51,31 +41,27 @@ module.exports = skipInNonExecutableProcess(function() {
|
|
|
51
41
|
propertyValue = propertyValue.get('body');
|
|
52
42
|
}
|
|
53
43
|
|
|
54
|
-
if (isFeelProperty(node, propertyName, propertyValue)) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
builtins: camundaReservedNameBuiltins
|
|
58
|
-
});
|
|
44
|
+
if (!isFeelProperty(node, propertyName, propertyValue)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
59
47
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
);
|
|
78
|
-
}
|
|
48
|
+
const { valid } = feelAnalyzer.analyzeExpression(propertyValue.substring(1));
|
|
49
|
+
|
|
50
|
+
if (!valid) {
|
|
51
|
+
const path = getPath(node, parentNode);
|
|
52
|
+
|
|
53
|
+
errors.push({
|
|
54
|
+
message: `Property <${ propertyName }> is not a valid FEEL expression`,
|
|
55
|
+
path: path
|
|
56
|
+
? [ ...path, propertyName ]
|
|
57
|
+
: [ propertyName ],
|
|
58
|
+
data: {
|
|
59
|
+
type: ERROR_TYPES.FEEL_EXPRESSION_INVALID,
|
|
60
|
+
node,
|
|
61
|
+
parentNode,
|
|
62
|
+
property: propertyName
|
|
63
|
+
}
|
|
64
|
+
});
|
|
79
65
|
}
|
|
80
66
|
});
|
|
81
67
|
|
|
@@ -88,26 +74,3 @@ module.exports = skipInNonExecutableProcess(function() {
|
|
|
88
74
|
check
|
|
89
75
|
});
|
|
90
76
|
});
|
|
91
|
-
|
|
92
|
-
const isFeelProperty = (node, propertyName, value) => {
|
|
93
|
-
return !isIgnoredProperty(node, propertyName) && isString(value) && value.startsWith('=');
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const isIgnoredProperty = (node, propertyName) => {
|
|
97
|
-
if (propertyName.startsWith('$') || IGNORED_PROPERTIES.includes(propertyName)) {
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const nodeType = node.$type;
|
|
102
|
-
const ignoredForType = IGNORED_PROPERTIES_BY_TYPE[nodeType];
|
|
103
|
-
|
|
104
|
-
return ignoredForType && ignoredForType.includes(propertyName);
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const findParentNode = node => {
|
|
108
|
-
while (node && !isAny(node, [ 'bpmn:FlowElement', 'bpmn:FlowElementsContainer' ])) {
|
|
109
|
-
node = node.$parent;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return node;
|
|
113
|
-
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { isString } = require('min-dash');
|
|
2
|
+
|
|
3
|
+
// Properties ignored globally
|
|
4
|
+
const IGNORED_PROPERTIES = [
|
|
5
|
+
'name'
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
// Properties ignored only for specific element types
|
|
9
|
+
const IGNORED_PROPERTIES_BY_TYPE = {
|
|
10
|
+
'zeebe:Input': [ 'target' ],
|
|
11
|
+
'zeebe:Output': [ 'target' ],
|
|
12
|
+
'zeebe:Header': [ 'key', 'value' ],
|
|
13
|
+
'zeebe:Property': [ 'name', 'value' ],
|
|
14
|
+
'zeebe:CalledDecision': [ 'resultVariable' ],
|
|
15
|
+
'zeebe:Script': [ 'resultVariable' ]
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const isIgnoredProperty = (node, propertyName) => {
|
|
19
|
+
if (propertyName.startsWith('$') || IGNORED_PROPERTIES.includes(propertyName)) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nodeType = node.$type;
|
|
24
|
+
const ignoredForType = IGNORED_PROPERTIES_BY_TYPE[nodeType];
|
|
25
|
+
|
|
26
|
+
return ignoredForType && ignoredForType.includes(propertyName);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const isFeelProperty = (node, propertyName, value) => {
|
|
30
|
+
return !isIgnoredProperty(node, propertyName) && isString(value) && value.startsWith('=');
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
isFeelProperty
|
|
35
|
+
};
|
package/rules/utils/element.js
CHANGED
|
@@ -525,6 +525,16 @@ function findParent(node, type) {
|
|
|
525
525
|
|
|
526
526
|
module.exports.findParent = findParent;
|
|
527
527
|
|
|
528
|
+
function findParentNode(node) {
|
|
529
|
+
while (node && !isAny(node, [ 'bpmn:FlowElement', 'bpmn:FlowElementsContainer' ])) {
|
|
530
|
+
node = node.$parent;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return node;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
module.exports.findParentNode = findParentNode;
|
|
537
|
+
|
|
528
538
|
function isEmptyString(value) {
|
|
529
539
|
return isString(value) && value.trim() === '';
|
|
530
540
|
}
|
|
@@ -13,6 +13,7 @@ module.exports.ERROR_TYPES = Object.freeze({
|
|
|
13
13
|
EXTENSION_ELEMENT_NOT_ALLOWED: 'camunda.extensionElementNotAllowed',
|
|
14
14
|
EXTENSION_ELEMENT_REQUIRED: 'camunda.extensionElementRequired',
|
|
15
15
|
FEEL_EXPRESSION_INVALID: 'camunda.feelExpressionInvalid',
|
|
16
|
+
FEEL_EXPRESSION_UNSUPPORTED_FUNCTION: 'camunda.feelExpressionUnsupportedFunction',
|
|
16
17
|
LOOP_NOT_ALLOWED: 'camunda.loopNotAllowed',
|
|
17
18
|
ATTACHED_TO_REF_ELEMENT_TYPE_NOT_ALLOWED: 'camunda.attachedToRefElementTypeNotAllowed',
|
|
18
19
|
PROPERTY_DEPENDENT_REQUIRED: 'camunda.propertyDependentRequired',
|