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.
Files changed (58) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +39 -39
  3. package/index.js +242 -237
  4. package/package.json +53 -53
  5. package/rules/camunda-cloud/called-element.js +42 -42
  6. package/rules/camunda-cloud/collapsed-subprocess.js +40 -40
  7. package/rules/camunda-cloud/connector-properties/config.js +90 -90
  8. package/rules/camunda-cloud/connector-properties/index.js +47 -47
  9. package/rules/camunda-cloud/duplicate-execution-listeners.js +33 -33
  10. package/rules/camunda-cloud/duplicate-task-headers.js +58 -58
  11. package/rules/camunda-cloud/element-type/config.js +66 -66
  12. package/rules/camunda-cloud/element-type/index.js +133 -133
  13. package/rules/camunda-cloud/error-reference.js +71 -71
  14. package/rules/camunda-cloud/escalation-boundary-event-attached-to-ref.js +48 -48
  15. package/rules/camunda-cloud/escalation-reference.js +66 -66
  16. package/rules/camunda-cloud/event-based-gateway-target.js +38 -38
  17. package/rules/camunda-cloud/executable-process.js +61 -61
  18. package/rules/camunda-cloud/execution-listener.js +34 -34
  19. package/rules/camunda-cloud/feel.js +82 -82
  20. package/rules/camunda-cloud/implementation/config.js +16 -16
  21. package/rules/camunda-cloud/implementation/index.js +218 -218
  22. package/rules/camunda-cloud/inclusive-gateway.js +35 -35
  23. package/rules/camunda-cloud/link-event.js +142 -142
  24. package/rules/camunda-cloud/loop-characteristics.js +66 -66
  25. package/rules/camunda-cloud/message-reference.js +60 -60
  26. package/rules/camunda-cloud/no-binding-type.js +43 -43
  27. package/rules/camunda-cloud/no-candidate-users.js +38 -38
  28. package/rules/camunda-cloud/no-execution-listeners.js +21 -21
  29. package/rules/camunda-cloud/no-expression.js +173 -173
  30. package/rules/camunda-cloud/no-loop.js +316 -316
  31. package/rules/camunda-cloud/no-multiple-none-start-events.js +41 -41
  32. package/rules/camunda-cloud/no-priority-definition.js +19 -0
  33. package/rules/camunda-cloud/no-propagate-all-parent-variables.js +44 -44
  34. package/rules/camunda-cloud/no-signal-event-sub-process.js +45 -45
  35. package/rules/camunda-cloud/no-task-schedule.js +18 -18
  36. package/rules/camunda-cloud/no-template.js +23 -23
  37. package/rules/camunda-cloud/no-zeebe-properties.js +18 -18
  38. package/rules/camunda-cloud/no-zeebe-user-task.js +27 -27
  39. package/rules/camunda-cloud/priority-definition.js +62 -0
  40. package/rules/camunda-cloud/secrets.js +119 -119
  41. package/rules/camunda-cloud/sequence-flow-condition.js +56 -56
  42. package/rules/camunda-cloud/signal-reference.js +64 -64
  43. package/rules/camunda-cloud/start-event-form.js +97 -97
  44. package/rules/camunda-cloud/subscription.js +65 -65
  45. package/rules/camunda-cloud/task-schedule.js +67 -67
  46. package/rules/camunda-cloud/timer/config.js +46 -46
  47. package/rules/camunda-cloud/timer/index.js +183 -183
  48. package/rules/camunda-cloud/user-task-definition.js +24 -24
  49. package/rules/camunda-cloud/user-task-form.js +142 -142
  50. package/rules/camunda-cloud/wait-for-completion.js +46 -46
  51. package/rules/camunda-platform/history-time-to-live.js +19 -19
  52. package/rules/utils/cron.js +95 -95
  53. package/rules/utils/element.js +533 -533
  54. package/rules/utils/error-types.js +25 -25
  55. package/rules/utils/iso8601.js +52 -52
  56. package/rules/utils/reporter.js +37 -37
  57. package/rules/utils/rule.js +46 -46
  58. 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
  }