bpmnlint-plugin-camunda-compat 2.23.0 → 2.24.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/LICENSE +20 -20
- package/README.md +39 -39
- package/index.js +242 -237
- package/package.json +53 -53
- package/rules/camunda-cloud/called-element.js +42 -42
- package/rules/camunda-cloud/collapsed-subprocess.js +40 -40
- package/rules/camunda-cloud/connector-properties/config.js +90 -90
- package/rules/camunda-cloud/connector-properties/index.js +47 -47
- package/rules/camunda-cloud/duplicate-execution-listeners.js +33 -33
- package/rules/camunda-cloud/duplicate-task-headers.js +58 -58
- package/rules/camunda-cloud/element-type/config.js +66 -66
- package/rules/camunda-cloud/element-type/index.js +133 -133
- package/rules/camunda-cloud/error-reference.js +71 -71
- package/rules/camunda-cloud/escalation-boundary-event-attached-to-ref.js +48 -48
- package/rules/camunda-cloud/escalation-reference.js +66 -66
- package/rules/camunda-cloud/event-based-gateway-target.js +38 -38
- package/rules/camunda-cloud/executable-process.js +61 -61
- package/rules/camunda-cloud/execution-listener.js +34 -34
- package/rules/camunda-cloud/feel.js +82 -82
- package/rules/camunda-cloud/implementation/config.js +16 -16
- package/rules/camunda-cloud/implementation/index.js +218 -218
- package/rules/camunda-cloud/inclusive-gateway.js +35 -35
- package/rules/camunda-cloud/link-event.js +142 -142
- package/rules/camunda-cloud/loop-characteristics.js +66 -66
- package/rules/camunda-cloud/message-reference.js +60 -60
- package/rules/camunda-cloud/no-binding-type.js +43 -43
- package/rules/camunda-cloud/no-candidate-users.js +38 -38
- package/rules/camunda-cloud/no-execution-listeners.js +21 -21
- package/rules/camunda-cloud/no-expression.js +173 -173
- package/rules/camunda-cloud/no-loop.js +316 -316
- package/rules/camunda-cloud/no-multiple-none-start-events.js +41 -41
- package/rules/camunda-cloud/no-priority-definition.js +19 -0
- package/rules/camunda-cloud/no-propagate-all-parent-variables.js +44 -44
- package/rules/camunda-cloud/no-signal-event-sub-process.js +45 -45
- package/rules/camunda-cloud/no-task-schedule.js +18 -18
- package/rules/camunda-cloud/no-template.js +23 -23
- package/rules/camunda-cloud/no-zeebe-properties.js +18 -18
- package/rules/camunda-cloud/no-zeebe-user-task.js +27 -27
- package/rules/camunda-cloud/priority-definition.js +62 -0
- package/rules/camunda-cloud/secrets.js +119 -119
- package/rules/camunda-cloud/sequence-flow-condition.js +56 -56
- package/rules/camunda-cloud/signal-reference.js +64 -64
- package/rules/camunda-cloud/start-event-form.js +97 -97
- package/rules/camunda-cloud/subscription.js +65 -65
- package/rules/camunda-cloud/task-schedule.js +67 -67
- package/rules/camunda-cloud/timer/config.js +46 -46
- package/rules/camunda-cloud/timer/index.js +183 -183
- package/rules/camunda-cloud/user-task-definition.js +24 -24
- package/rules/camunda-cloud/user-task-form.js +142 -142
- package/rules/camunda-cloud/wait-for-completion.js +46 -46
- package/rules/camunda-platform/history-time-to-live.js +19 -19
- package/rules/utils/cron.js +95 -95
- package/rules/utils/element.js +533 -533
- package/rules/utils/error-types.js +25 -25
- package/rules/utils/iso8601.js +52 -52
- package/rules/utils/reporter.js +37 -37
- package/rules/utils/rule.js +46 -46
- package/rules/utils/version.js +4 -4
@@ -1,317 +1,317 @@
|
|
1
|
-
const { isString } = require('min-dash');
|
2
|
-
|
3
|
-
const { is } = require('bpmnlint-utils');
|
4
|
-
|
5
|
-
const {
|
6
|
-
findExtensionElement,
|
7
|
-
findParent,
|
8
|
-
isAnyExactly
|
9
|
-
} = require('../utils/element');
|
10
|
-
|
11
|
-
const { reportErrors } = require('../utils/reporter');
|
12
|
-
|
13
|
-
const { ERROR_TYPES } = require('../utils/error-types');
|
14
|
-
|
15
|
-
const { skipInNonExecutableProcess } = require('../utils/rule');
|
16
|
-
|
17
|
-
/**
|
18
|
-
* @typedef {import('bpmn-moddle').BaseElement} ModdleElement
|
19
|
-
**/
|
20
|
-
|
21
|
-
const LOOP_REQUIRED_ELEMENT_TYPES = [
|
22
|
-
'bpmn:CallActivity',
|
23
|
-
'bpmn:ManualTask',
|
24
|
-
'bpmn:Task'
|
25
|
-
];
|
26
|
-
|
27
|
-
const LOOP_ELEMENT_TYPES = [
|
28
|
-
...LOOP_REQUIRED_ELEMENT_TYPES,
|
29
|
-
'bpmn:StartEvent',
|
30
|
-
'bpmn:EndEvent',
|
31
|
-
'bpmn:ManualTask',
|
32
|
-
'bpmn:ExclusiveGateway',
|
33
|
-
'bpmn:InclusiveGateway',
|
34
|
-
'bpmn:ParallelGateway',
|
35
|
-
'bpmn:SubProcess',
|
36
|
-
'bpmn:Task'
|
37
|
-
];
|
38
|
-
|
39
|
-
module.exports = skipInNonExecutableProcess(function() {
|
40
|
-
function check(node, reporter) {
|
41
|
-
if (!is(node, 'bpmn:Process')) {
|
42
|
-
return;
|
43
|
-
}
|
44
|
-
|
45
|
-
// 1. Remove all elements that can be part of an infinite loop
|
46
|
-
const relevantNodes = getFlowElements(node)
|
47
|
-
.filter(flowElement => {
|
48
|
-
return isAnyExactly(flowElement, LOOP_ELEMENT_TYPES);
|
49
|
-
});
|
50
|
-
|
51
|
-
// 2. Remove all non-required elements. This produces a graph that only contains the required elements,
|
52
|
-
// with annotated edges that preserve the original path.
|
53
|
-
// Any loop found within the simplified graph is a valid loop, as all Vertices in the graph are `LOOP_REQUIRED_ELEMENT_TYPES`.
|
54
|
-
const minimalGraph = simplifyGraph(relevantNodes);
|
55
|
-
|
56
|
-
// 3. Use breadth-first search to find loops in the simplified Graph.
|
57
|
-
const errors = findLoops(minimalGraph, node);
|
58
|
-
|
59
|
-
if (errors) {
|
60
|
-
reportErrors(node, reporter, errors);
|
61
|
-
}
|
62
|
-
}
|
63
|
-
|
64
|
-
return {
|
65
|
-
check
|
66
|
-
};
|
67
|
-
});
|
68
|
-
|
69
|
-
/**
|
70
|
-
* @typedef {Object} GraphNode
|
71
|
-
* @property {ModdleElement} element the bpmn element this node represents
|
72
|
-
* @property {Map<ModdleElement, Array<ModdleElement>>} incoming Maps the target node with the shortest path to it
|
73
|
-
* @property {Map<ModdleElement, Array<ModdleElement>>} outgoing Maps the source node with the shortest path to it
|
74
|
-
*/
|
75
|
-
|
76
|
-
/**
|
77
|
-
* Simplifies the graph by removing all non-`LOOP_REQUIRED_ELEMENT_TYPES` elements and connecting incoming and outgoing nodes directly.
|
78
|
-
* Annotates the edges with the original path. Uses breadth-first search to find paths.
|
79
|
-
*
|
80
|
-
* @param {Array<ModdleElement>} flowElements
|
81
|
-
* @returns {Map<ModdleElement, GraphNode>}
|
82
|
-
*/
|
83
|
-
function simplifyGraph(flowElements) {
|
84
|
-
|
85
|
-
// Transform Array<ModdleElement> into Map<ModdleElement, GraphNode>
|
86
|
-
const graph = elementsToGraph(flowElements);
|
87
|
-
|
88
|
-
breadthFirstSearch(graph, (node) => {
|
89
|
-
const { element, outgoing } = node;
|
90
|
-
|
91
|
-
// Remove non-required element and connect incoming and outgoing nodes directly
|
92
|
-
if (!isAnyExactly(element, LOOP_REQUIRED_ELEMENT_TYPES)) {
|
93
|
-
connectNodes(graph, node);
|
94
|
-
}
|
95
|
-
|
96
|
-
return Array.from(outgoing.keys(), key => graph.get(key));
|
97
|
-
});
|
98
|
-
|
99
|
-
// Clean up all references to removed elements
|
100
|
-
graph.forEach(({ incoming, outgoing }) => {
|
101
|
-
incoming.forEach((_, key) => {
|
102
|
-
if (!graph.has(key)) {
|
103
|
-
incoming.delete(key);
|
104
|
-
}
|
105
|
-
});
|
106
|
-
|
107
|
-
outgoing.forEach((_, key) => {
|
108
|
-
if (!graph.has(key)) {
|
109
|
-
outgoing.delete(key);
|
110
|
-
}
|
111
|
-
});
|
112
|
-
});
|
113
|
-
|
114
|
-
return graph;
|
115
|
-
}
|
116
|
-
|
117
|
-
|
118
|
-
/**
|
119
|
-
* Uses breadth-first search to find loops in the graph and generate errors.
|
120
|
-
*
|
121
|
-
* @param {Map<ModdleElement, GraphNode>} graph The simplified graph containing only required elements
|
122
|
-
* @param {ModdleElement} root used for reporting the errors
|
123
|
-
* @returns {Array<Object>} errors
|
124
|
-
*/
|
125
|
-
function findLoops(graph, root) {
|
126
|
-
const errors = [];
|
127
|
-
|
128
|
-
// Traverse graph using breadth-first search, remembering the path. If we find a loop, report it.
|
129
|
-
breadthFirstSearch(graph, (node) => {
|
130
|
-
const { element, outgoing, path = [] } = node;
|
131
|
-
|
132
|
-
const nextElements = [ ];
|
133
|
-
outgoing.forEach((connectionPath, nextElement) => {
|
134
|
-
const newPath = [ ...path, element, ...connectionPath ];
|
135
|
-
|
136
|
-
// We already visited this node, we found a loop
|
137
|
-
if (newPath.includes(nextElement)) {
|
138
|
-
errors.push(handleLoop(newPath, nextElement, root));
|
139
|
-
} else {
|
140
|
-
const nextNode = graph.get(nextElement);
|
141
|
-
nextNode.path = nextNode.path || newPath;
|
142
|
-
nextElements.push(nextNode);
|
143
|
-
}
|
144
|
-
});
|
145
|
-
|
146
|
-
return nextElements;
|
147
|
-
});
|
148
|
-
|
149
|
-
return errors.filter(Boolean);
|
150
|
-
}
|
151
|
-
|
152
|
-
const handleLoop = (path, currentNode, root) => {
|
153
|
-
const loop = path.slice(path.indexOf(currentNode));
|
154
|
-
|
155
|
-
if (isIgnoredLoop(loop)) {
|
156
|
-
return null;
|
157
|
-
}
|
158
|
-
|
159
|
-
return {
|
160
|
-
message: `Loop detected: ${ loop.map(({ id }) => id).join(' -> ') } -> ${ currentNode.id }`,
|
161
|
-
path: null,
|
162
|
-
data: {
|
163
|
-
type: ERROR_TYPES.LOOP_NOT_ALLOWED,
|
164
|
-
node: root,
|
165
|
-
parentNode: null,
|
166
|
-
elements: loop.map(({ id }) => id)
|
167
|
-
}
|
168
|
-
};
|
169
|
-
};
|
170
|
-
|
171
|
-
function getFlowElements(node) {
|
172
|
-
return node.get('flowElements').reduce((flowElements, flowElement) => {
|
173
|
-
if (is(flowElement, 'bpmn:FlowElementsContainer')) {
|
174
|
-
return [ ...flowElements, flowElement, ...getFlowElements(flowElement) ];
|
175
|
-
}
|
176
|
-
|
177
|
-
return [ ...flowElements, flowElement ];
|
178
|
-
}, []);
|
179
|
-
}
|
180
|
-
|
181
|
-
function getNextFlowElements(flowElement) {
|
182
|
-
if (is(flowElement, 'bpmn:CallActivity')) {
|
183
|
-
const calledElement = findExtensionElement(flowElement, 'zeebe:CalledElement');
|
184
|
-
|
185
|
-
if (calledElement) {
|
186
|
-
const processId = calledElement.get('processId');
|
187
|
-
|
188
|
-
if (isString(processId) && !isFeel(processId)) {
|
189
|
-
const process = findParent(flowElement, 'bpmn:Process');
|
190
|
-
|
191
|
-
if (process && process.get('id') === processId) {
|
192
|
-
return process.get('flowElements').filter(flowElement => is(flowElement, 'bpmn:StartEvent'));
|
193
|
-
}
|
194
|
-
}
|
195
|
-
}
|
196
|
-
} else if (is(flowElement, 'bpmn:SubProcess')) {
|
197
|
-
return flowElement
|
198
|
-
.get('flowElements').filter(flowElement => is(flowElement, 'bpmn:StartEvent'));
|
199
|
-
} else if (is(flowElement, 'bpmn:EndEvent')) {
|
200
|
-
const parent = flowElement.$parent;
|
201
|
-
|
202
|
-
if (is(parent, 'bpmn:SubProcess')) {
|
203
|
-
flowElement = parent;
|
204
|
-
}
|
205
|
-
}
|
206
|
-
|
207
|
-
return flowElement
|
208
|
-
.get('outgoing').filter(outgoing => is(outgoing, 'bpmn:SequenceFlow')).map(sequenceFlow => sequenceFlow.get('targetRef'));
|
209
|
-
}
|
210
|
-
|
211
|
-
function isIgnoredLoop(elements) {
|
212
|
-
return !elements.some(element => isAnyExactly(element, LOOP_REQUIRED_ELEMENT_TYPES));
|
213
|
-
}
|
214
|
-
|
215
|
-
function isFeel(value) {
|
216
|
-
return isString(value) && value.startsWith('=');
|
217
|
-
}
|
218
|
-
|
219
|
-
const getOrSet = (map, key, defaultValue) => {
|
220
|
-
if (!map.has(key)) {
|
221
|
-
map.set(key, defaultValue);
|
222
|
-
}
|
223
|
-
|
224
|
-
return map.get(key);
|
225
|
-
};
|
226
|
-
|
227
|
-
const setIfAbsent = (map, key, value) => {
|
228
|
-
map.has(key) || map.set(key, value);
|
229
|
-
};
|
230
|
-
|
231
|
-
/**
|
232
|
-
* Transform Array of flow elements into a Graph structure, adding implicit connections (e.g. SubProcess -> StartEvent)
|
233
|
-
* via `getNextFlowElements`.
|
234
|
-
*
|
235
|
-
* @param {Array<ModdleElement>} flowElements
|
236
|
-
* @returns Map<ModdleElement, GraphNode>
|
237
|
-
*/
|
238
|
-
function elementsToGraph(flowElements) {
|
239
|
-
return flowElements.reduce((currentMap, element) => {
|
240
|
-
const currentNode = getOrSet(currentMap, element, {
|
241
|
-
element,
|
242
|
-
incoming: new Map(),
|
243
|
-
outgoing: new Map(),
|
244
|
-
});
|
245
|
-
|
246
|
-
const nextFlowElements = getNextFlowElements(element);
|
247
|
-
|
248
|
-
nextFlowElements.forEach(nextElement => {
|
249
|
-
const nextNode = getOrSet(currentMap, nextElement, {
|
250
|
-
element: nextElement,
|
251
|
-
incoming: new Map(),
|
252
|
-
outgoing: new Map(),
|
253
|
-
});
|
254
|
-
|
255
|
-
nextNode.incoming.set(element, []);
|
256
|
-
currentNode.outgoing.set(nextElement, []);
|
257
|
-
});
|
258
|
-
|
259
|
-
return currentMap;
|
260
|
-
}, new Map());
|
261
|
-
}
|
262
|
-
|
263
|
-
/**
|
264
|
-
* Connects incoming and outgoing nodes directly, add current node to the path and remove node from graph.
|
265
|
-
*/
|
266
|
-
function connectNodes(graph, node) {
|
267
|
-
const { element, incoming, outgoing } = node;
|
268
|
-
|
269
|
-
incoming.forEach((fromPath, fromKey) => {
|
270
|
-
outgoing.forEach((toPath, toKey) => {
|
271
|
-
const fromNode = graph.get(fromKey);
|
272
|
-
const toNode = graph.get(toKey);
|
273
|
-
|
274
|
-
if (!fromNode || !toNode) {
|
275
|
-
return;
|
276
|
-
}
|
277
|
-
|
278
|
-
// We only care about the shortest path, so we don't need to update the path if it's already set
|
279
|
-
setIfAbsent(fromNode.outgoing, toKey, [ ...fromPath, element, ...toPath ]);
|
280
|
-
setIfAbsent(toNode.incoming, fromKey, [ ...fromPath, element, ...toPath ]);
|
281
|
-
});
|
282
|
-
});
|
283
|
-
|
284
|
-
graph.delete(element);
|
285
|
-
}
|
286
|
-
|
287
|
-
/**
|
288
|
-
* Iterates over all nodes in the graph using breadth-first search.
|
289
|
-
*
|
290
|
-
* @param {Map<ModdleElement, GraphNode>} graph
|
291
|
-
* @param {Function} iterationCallback
|
292
|
-
*/
|
293
|
-
function breadthFirstSearch(graph, iterationCallback) {
|
294
|
-
const unvisited = new Set(graph.values());
|
295
|
-
|
296
|
-
while (unvisited.size) {
|
297
|
-
let firstElement = unvisited.values().next().value;
|
298
|
-
unvisited.delete(firstElement);
|
299
|
-
|
300
|
-
const elementsToVisit = [ firstElement ];
|
301
|
-
|
302
|
-
while (elementsToVisit.length) {
|
303
|
-
const node = elementsToVisit.shift();
|
304
|
-
|
305
|
-
const nextElements = iterationCallback(node);
|
306
|
-
|
307
|
-
nextElements.forEach(nextElement => {
|
308
|
-
if (!unvisited.has(nextElement)) {
|
309
|
-
return;
|
310
|
-
}
|
311
|
-
|
312
|
-
unvisited.delete(nextElement);
|
313
|
-
elementsToVisit.push(nextElement);
|
314
|
-
});
|
315
|
-
}
|
316
|
-
}
|
1
|
+
const { isString } = require('min-dash');
|
2
|
+
|
3
|
+
const { is } = require('bpmnlint-utils');
|
4
|
+
|
5
|
+
const {
|
6
|
+
findExtensionElement,
|
7
|
+
findParent,
|
8
|
+
isAnyExactly
|
9
|
+
} = require('../utils/element');
|
10
|
+
|
11
|
+
const { reportErrors } = require('../utils/reporter');
|
12
|
+
|
13
|
+
const { ERROR_TYPES } = require('../utils/error-types');
|
14
|
+
|
15
|
+
const { skipInNonExecutableProcess } = require('../utils/rule');
|
16
|
+
|
17
|
+
/**
|
18
|
+
* @typedef {import('bpmn-moddle').BaseElement} ModdleElement
|
19
|
+
**/
|
20
|
+
|
21
|
+
const LOOP_REQUIRED_ELEMENT_TYPES = [
|
22
|
+
'bpmn:CallActivity',
|
23
|
+
'bpmn:ManualTask',
|
24
|
+
'bpmn:Task'
|
25
|
+
];
|
26
|
+
|
27
|
+
const LOOP_ELEMENT_TYPES = [
|
28
|
+
...LOOP_REQUIRED_ELEMENT_TYPES,
|
29
|
+
'bpmn:StartEvent',
|
30
|
+
'bpmn:EndEvent',
|
31
|
+
'bpmn:ManualTask',
|
32
|
+
'bpmn:ExclusiveGateway',
|
33
|
+
'bpmn:InclusiveGateway',
|
34
|
+
'bpmn:ParallelGateway',
|
35
|
+
'bpmn:SubProcess',
|
36
|
+
'bpmn:Task'
|
37
|
+
];
|
38
|
+
|
39
|
+
module.exports = skipInNonExecutableProcess(function() {
|
40
|
+
function check(node, reporter) {
|
41
|
+
if (!is(node, 'bpmn:Process')) {
|
42
|
+
return;
|
43
|
+
}
|
44
|
+
|
45
|
+
// 1. Remove all elements that can be part of an infinite loop
|
46
|
+
const relevantNodes = getFlowElements(node)
|
47
|
+
.filter(flowElement => {
|
48
|
+
return isAnyExactly(flowElement, LOOP_ELEMENT_TYPES);
|
49
|
+
});
|
50
|
+
|
51
|
+
// 2. Remove all non-required elements. This produces a graph that only contains the required elements,
|
52
|
+
// with annotated edges that preserve the original path.
|
53
|
+
// Any loop found within the simplified graph is a valid loop, as all Vertices in the graph are `LOOP_REQUIRED_ELEMENT_TYPES`.
|
54
|
+
const minimalGraph = simplifyGraph(relevantNodes);
|
55
|
+
|
56
|
+
// 3. Use breadth-first search to find loops in the simplified Graph.
|
57
|
+
const errors = findLoops(minimalGraph, node);
|
58
|
+
|
59
|
+
if (errors) {
|
60
|
+
reportErrors(node, reporter, errors);
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
return {
|
65
|
+
check
|
66
|
+
};
|
67
|
+
});
|
68
|
+
|
69
|
+
/**
|
70
|
+
* @typedef {Object} GraphNode
|
71
|
+
* @property {ModdleElement} element the bpmn element this node represents
|
72
|
+
* @property {Map<ModdleElement, Array<ModdleElement>>} incoming Maps the target node with the shortest path to it
|
73
|
+
* @property {Map<ModdleElement, Array<ModdleElement>>} outgoing Maps the source node with the shortest path to it
|
74
|
+
*/
|
75
|
+
|
76
|
+
/**
|
77
|
+
* Simplifies the graph by removing all non-`LOOP_REQUIRED_ELEMENT_TYPES` elements and connecting incoming and outgoing nodes directly.
|
78
|
+
* Annotates the edges with the original path. Uses breadth-first search to find paths.
|
79
|
+
*
|
80
|
+
* @param {Array<ModdleElement>} flowElements
|
81
|
+
* @returns {Map<ModdleElement, GraphNode>}
|
82
|
+
*/
|
83
|
+
function simplifyGraph(flowElements) {
|
84
|
+
|
85
|
+
// Transform Array<ModdleElement> into Map<ModdleElement, GraphNode>
|
86
|
+
const graph = elementsToGraph(flowElements);
|
87
|
+
|
88
|
+
breadthFirstSearch(graph, (node) => {
|
89
|
+
const { element, outgoing } = node;
|
90
|
+
|
91
|
+
// Remove non-required element and connect incoming and outgoing nodes directly
|
92
|
+
if (!isAnyExactly(element, LOOP_REQUIRED_ELEMENT_TYPES)) {
|
93
|
+
connectNodes(graph, node);
|
94
|
+
}
|
95
|
+
|
96
|
+
return Array.from(outgoing.keys(), key => graph.get(key));
|
97
|
+
});
|
98
|
+
|
99
|
+
// Clean up all references to removed elements
|
100
|
+
graph.forEach(({ incoming, outgoing }) => {
|
101
|
+
incoming.forEach((_, key) => {
|
102
|
+
if (!graph.has(key)) {
|
103
|
+
incoming.delete(key);
|
104
|
+
}
|
105
|
+
});
|
106
|
+
|
107
|
+
outgoing.forEach((_, key) => {
|
108
|
+
if (!graph.has(key)) {
|
109
|
+
outgoing.delete(key);
|
110
|
+
}
|
111
|
+
});
|
112
|
+
});
|
113
|
+
|
114
|
+
return graph;
|
115
|
+
}
|
116
|
+
|
117
|
+
|
118
|
+
/**
|
119
|
+
* Uses breadth-first search to find loops in the graph and generate errors.
|
120
|
+
*
|
121
|
+
* @param {Map<ModdleElement, GraphNode>} graph The simplified graph containing only required elements
|
122
|
+
* @param {ModdleElement} root used for reporting the errors
|
123
|
+
* @returns {Array<Object>} errors
|
124
|
+
*/
|
125
|
+
function findLoops(graph, root) {
|
126
|
+
const errors = [];
|
127
|
+
|
128
|
+
// Traverse graph using breadth-first search, remembering the path. If we find a loop, report it.
|
129
|
+
breadthFirstSearch(graph, (node) => {
|
130
|
+
const { element, outgoing, path = [] } = node;
|
131
|
+
|
132
|
+
const nextElements = [ ];
|
133
|
+
outgoing.forEach((connectionPath, nextElement) => {
|
134
|
+
const newPath = [ ...path, element, ...connectionPath ];
|
135
|
+
|
136
|
+
// We already visited this node, we found a loop
|
137
|
+
if (newPath.includes(nextElement)) {
|
138
|
+
errors.push(handleLoop(newPath, nextElement, root));
|
139
|
+
} else {
|
140
|
+
const nextNode = graph.get(nextElement);
|
141
|
+
nextNode.path = nextNode.path || newPath;
|
142
|
+
nextElements.push(nextNode);
|
143
|
+
}
|
144
|
+
});
|
145
|
+
|
146
|
+
return nextElements;
|
147
|
+
});
|
148
|
+
|
149
|
+
return errors.filter(Boolean);
|
150
|
+
}
|
151
|
+
|
152
|
+
const handleLoop = (path, currentNode, root) => {
|
153
|
+
const loop = path.slice(path.indexOf(currentNode));
|
154
|
+
|
155
|
+
if (isIgnoredLoop(loop)) {
|
156
|
+
return null;
|
157
|
+
}
|
158
|
+
|
159
|
+
return {
|
160
|
+
message: `Loop detected: ${ loop.map(({ id }) => id).join(' -> ') } -> ${ currentNode.id }`,
|
161
|
+
path: null,
|
162
|
+
data: {
|
163
|
+
type: ERROR_TYPES.LOOP_NOT_ALLOWED,
|
164
|
+
node: root,
|
165
|
+
parentNode: null,
|
166
|
+
elements: loop.map(({ id }) => id)
|
167
|
+
}
|
168
|
+
};
|
169
|
+
};
|
170
|
+
|
171
|
+
function getFlowElements(node) {
|
172
|
+
return node.get('flowElements').reduce((flowElements, flowElement) => {
|
173
|
+
if (is(flowElement, 'bpmn:FlowElementsContainer')) {
|
174
|
+
return [ ...flowElements, flowElement, ...getFlowElements(flowElement) ];
|
175
|
+
}
|
176
|
+
|
177
|
+
return [ ...flowElements, flowElement ];
|
178
|
+
}, []);
|
179
|
+
}
|
180
|
+
|
181
|
+
function getNextFlowElements(flowElement) {
|
182
|
+
if (is(flowElement, 'bpmn:CallActivity')) {
|
183
|
+
const calledElement = findExtensionElement(flowElement, 'zeebe:CalledElement');
|
184
|
+
|
185
|
+
if (calledElement) {
|
186
|
+
const processId = calledElement.get('processId');
|
187
|
+
|
188
|
+
if (isString(processId) && !isFeel(processId)) {
|
189
|
+
const process = findParent(flowElement, 'bpmn:Process');
|
190
|
+
|
191
|
+
if (process && process.get('id') === processId) {
|
192
|
+
return process.get('flowElements').filter(flowElement => is(flowElement, 'bpmn:StartEvent'));
|
193
|
+
}
|
194
|
+
}
|
195
|
+
}
|
196
|
+
} else if (is(flowElement, 'bpmn:SubProcess')) {
|
197
|
+
return flowElement
|
198
|
+
.get('flowElements').filter(flowElement => is(flowElement, 'bpmn:StartEvent'));
|
199
|
+
} else if (is(flowElement, 'bpmn:EndEvent')) {
|
200
|
+
const parent = flowElement.$parent;
|
201
|
+
|
202
|
+
if (is(parent, 'bpmn:SubProcess')) {
|
203
|
+
flowElement = parent;
|
204
|
+
}
|
205
|
+
}
|
206
|
+
|
207
|
+
return flowElement
|
208
|
+
.get('outgoing').filter(outgoing => is(outgoing, 'bpmn:SequenceFlow')).map(sequenceFlow => sequenceFlow.get('targetRef'));
|
209
|
+
}
|
210
|
+
|
211
|
+
function isIgnoredLoop(elements) {
|
212
|
+
return !elements.some(element => isAnyExactly(element, LOOP_REQUIRED_ELEMENT_TYPES));
|
213
|
+
}
|
214
|
+
|
215
|
+
function isFeel(value) {
|
216
|
+
return isString(value) && value.startsWith('=');
|
217
|
+
}
|
218
|
+
|
219
|
+
const getOrSet = (map, key, defaultValue) => {
|
220
|
+
if (!map.has(key)) {
|
221
|
+
map.set(key, defaultValue);
|
222
|
+
}
|
223
|
+
|
224
|
+
return map.get(key);
|
225
|
+
};
|
226
|
+
|
227
|
+
const setIfAbsent = (map, key, value) => {
|
228
|
+
map.has(key) || map.set(key, value);
|
229
|
+
};
|
230
|
+
|
231
|
+
/**
|
232
|
+
* Transform Array of flow elements into a Graph structure, adding implicit connections (e.g. SubProcess -> StartEvent)
|
233
|
+
* via `getNextFlowElements`.
|
234
|
+
*
|
235
|
+
* @param {Array<ModdleElement>} flowElements
|
236
|
+
* @returns Map<ModdleElement, GraphNode>
|
237
|
+
*/
|
238
|
+
function elementsToGraph(flowElements) {
|
239
|
+
return flowElements.reduce((currentMap, element) => {
|
240
|
+
const currentNode = getOrSet(currentMap, element, {
|
241
|
+
element,
|
242
|
+
incoming: new Map(),
|
243
|
+
outgoing: new Map(),
|
244
|
+
});
|
245
|
+
|
246
|
+
const nextFlowElements = getNextFlowElements(element);
|
247
|
+
|
248
|
+
nextFlowElements.forEach(nextElement => {
|
249
|
+
const nextNode = getOrSet(currentMap, nextElement, {
|
250
|
+
element: nextElement,
|
251
|
+
incoming: new Map(),
|
252
|
+
outgoing: new Map(),
|
253
|
+
});
|
254
|
+
|
255
|
+
nextNode.incoming.set(element, []);
|
256
|
+
currentNode.outgoing.set(nextElement, []);
|
257
|
+
});
|
258
|
+
|
259
|
+
return currentMap;
|
260
|
+
}, new Map());
|
261
|
+
}
|
262
|
+
|
263
|
+
/**
|
264
|
+
* Connects incoming and outgoing nodes directly, add current node to the path and remove node from graph.
|
265
|
+
*/
|
266
|
+
function connectNodes(graph, node) {
|
267
|
+
const { element, incoming, outgoing } = node;
|
268
|
+
|
269
|
+
incoming.forEach((fromPath, fromKey) => {
|
270
|
+
outgoing.forEach((toPath, toKey) => {
|
271
|
+
const fromNode = graph.get(fromKey);
|
272
|
+
const toNode = graph.get(toKey);
|
273
|
+
|
274
|
+
if (!fromNode || !toNode) {
|
275
|
+
return;
|
276
|
+
}
|
277
|
+
|
278
|
+
// We only care about the shortest path, so we don't need to update the path if it's already set
|
279
|
+
setIfAbsent(fromNode.outgoing, toKey, [ ...fromPath, element, ...toPath ]);
|
280
|
+
setIfAbsent(toNode.incoming, fromKey, [ ...fromPath, element, ...toPath ]);
|
281
|
+
});
|
282
|
+
});
|
283
|
+
|
284
|
+
graph.delete(element);
|
285
|
+
}
|
286
|
+
|
287
|
+
/**
|
288
|
+
* Iterates over all nodes in the graph using breadth-first search.
|
289
|
+
*
|
290
|
+
* @param {Map<ModdleElement, GraphNode>} graph
|
291
|
+
* @param {Function} iterationCallback
|
292
|
+
*/
|
293
|
+
function breadthFirstSearch(graph, iterationCallback) {
|
294
|
+
const unvisited = new Set(graph.values());
|
295
|
+
|
296
|
+
while (unvisited.size) {
|
297
|
+
let firstElement = unvisited.values().next().value;
|
298
|
+
unvisited.delete(firstElement);
|
299
|
+
|
300
|
+
const elementsToVisit = [ firstElement ];
|
301
|
+
|
302
|
+
while (elementsToVisit.length) {
|
303
|
+
const node = elementsToVisit.shift();
|
304
|
+
|
305
|
+
const nextElements = iterationCallback(node);
|
306
|
+
|
307
|
+
nextElements.forEach(nextElement => {
|
308
|
+
if (!unvisited.has(nextElement)) {
|
309
|
+
return;
|
310
|
+
}
|
311
|
+
|
312
|
+
unvisited.delete(nextElement);
|
313
|
+
elementsToVisit.push(nextElement);
|
314
|
+
});
|
315
|
+
}
|
316
|
+
}
|
317
317
|
}
|