bpmnlint-plugin-camunda-compat 2.6.2 → 2.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/index.js CHANGED
@@ -13,6 +13,7 @@ const camundaCloud10Rules = withConfig({
13
13
  'message-reference': 'error',
14
14
  'no-candidate-users': 'error',
15
15
  'no-expression': 'error',
16
+ 'no-loop': 'error',
16
17
  'no-multiple-none-start-events': 'error',
17
18
  'no-propagate-all-parent-variables': 'error',
18
19
  'no-task-schedule': 'error',
@@ -93,6 +94,7 @@ const rules = {
93
94
  'message-reference': './rules/camunda-cloud/message-reference',
94
95
  'no-candidate-users': './rules/camunda-cloud/no-candidate-users',
95
96
  'no-expression': './rules/camunda-cloud/no-expression',
97
+ 'no-loop': './rules/camunda-cloud/no-loop',
96
98
  'no-multiple-none-start-events': './rules/camunda-cloud/no-multiple-none-start-events',
97
99
  'no-propagate-all-parent-variables': './rules/camunda-cloud/no-propagate-all-parent-variables',
98
100
  'no-signal-event-sub-process': './rules/camunda-cloud/no-signal-event-sub-process',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bpmnlint-plugin-camunda-compat",
3
- "version": "2.6.2",
3
+ "version": "2.7.0",
4
4
  "description": "A bpmnlint plug-in for Camunda Platform compatibility",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -89,7 +89,7 @@ module.exports = skipInNonExecutableProcess(function() {
89
89
 
90
90
  // check for missing link catch & throw event names
91
91
  if (isLinkEvent(node)) {
92
- const linkEventDefinition = getEventDefinition(node, 'bpmn:LinkEventDefinition');
92
+ const linkEventDefinition = getEventDefinition(node);
93
93
 
94
94
  const errors = hasProperties(linkEventDefinition, {
95
95
  name: {
@@ -109,15 +109,19 @@ module.exports = skipInNonExecutableProcess(function() {
109
109
  });
110
110
 
111
111
  function isLinkEvent(element) {
112
+ const eventDefinition = getEventDefinition(element);
113
+
112
114
  return isAny(element, [
113
115
  'bpmn:IntermediateCatchEvent',
114
116
  'bpmn:IntermediateThrowEvent'
115
- ]) && getEventDefinition(element, 'bpmn:LinkEventDefinition');
117
+ ]) && eventDefinition && is(eventDefinition, 'bpmn:LinkEventDefinition');
116
118
  }
117
119
 
118
120
  function isLinkCatchEvent(element) {
121
+ const eventDefinition = getEventDefinition(element);
122
+
119
123
  return is(element, 'bpmn:IntermediateCatchEvent')
120
- && getEventDefinition(element, 'bpmn:LinkEventDefinition');
124
+ && eventDefinition && is(eventDefinition, 'bpmn:LinkEventDefinition');
121
125
  }
122
126
 
123
127
  function getLinkCatchEvents(flowElementsContainer) {
@@ -0,0 +1,146 @@
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
+ const LOOP_REQUIRED_ELEMENT_TYPES = [
18
+ 'bpmn:CallActivity',
19
+ 'bpmn:ManualTask',
20
+ 'bpmn:Task'
21
+ ];
22
+
23
+ const LOOP_ELEMENT_TYPES = [
24
+ ...LOOP_REQUIRED_ELEMENT_TYPES,
25
+ 'bpmn:StartEvent',
26
+ 'bpmn:EndEvent',
27
+ 'bpmn:ManualTask',
28
+ 'bpmn:ExclusiveGateway',
29
+ 'bpmn:InclusiveGateway',
30
+ 'bpmn:ParallelGateway',
31
+ 'bpmn:SubProcess',
32
+ 'bpmn:Task'
33
+ ];
34
+
35
+ module.exports = skipInNonExecutableProcess(function() {
36
+ function check(node, reporter) {
37
+ if (!is(node, 'bpmn:Process')) {
38
+ return;
39
+ }
40
+
41
+ const error = getFlowElements(node)
42
+ .filter(flowElement => {
43
+ return isAnyExactly(flowElement, LOOP_ELEMENT_TYPES);
44
+ })
45
+ .reduce((error, flowElement) => {
46
+ return error || findLoop(flowElement, node);
47
+ }, null);
48
+
49
+ if (error) {
50
+ reportErrors(node, reporter, error);
51
+ }
52
+ }
53
+
54
+ return {
55
+ check
56
+ };
57
+ });
58
+
59
+ function findLoop(flowElement, parentElement, visitedFlowElements = []) {
60
+
61
+ // (1.1) is not a loop
62
+ if (!isAnyExactly(flowElement, LOOP_ELEMENT_TYPES)) {
63
+ return null;
64
+ }
65
+
66
+ const nextFlowElements = getNextFlowElements(flowElement);
67
+
68
+ // (1.2) is not a loop
69
+ if (!nextFlowElements.length) {
70
+ return null;
71
+ }
72
+
73
+ // (2) may be a loop
74
+ if (!visitedFlowElements.includes(flowElement)) {
75
+ return nextFlowElements.reduce((error, nextFlowElement) => {
76
+ return error || findLoop(nextFlowElement, parentElement, [ ...visitedFlowElements, flowElement ]);
77
+ }, null);
78
+ }
79
+
80
+ // (3) is a loop but ignored
81
+ if (isIgnoredLoop(visitedFlowElements)) {
82
+ return null;
83
+ }
84
+
85
+ const elements = visitedFlowElements.slice(visitedFlowElements.indexOf(flowElement));
86
+
87
+ // (4) is a loop
88
+ return {
89
+ message: `Loop detected: ${ elements.map(({ id }) => id).join(' -> ') } -> ${ flowElement.id }`,
90
+ path: null,
91
+ data: {
92
+ type: ERROR_TYPES.LOOP_NOT_ALLOWED,
93
+ node: parentElement,
94
+ parentNode: null,
95
+ elements: elements.map(({ id }) => id)
96
+ }
97
+ };
98
+ }
99
+
100
+ function getFlowElements(node) {
101
+ return node.get('flowElements').reduce((flowElements, flowElement) => {
102
+ if (is(flowElement, 'bpmn:FlowElementsContainer')) {
103
+ return [ ...flowElements, ...getFlowElements(flowElement) ];
104
+ }
105
+
106
+ return [ ...flowElements, flowElement ];
107
+ }, []);
108
+ }
109
+
110
+ function getNextFlowElements(flowElement) {
111
+ if (is(flowElement, 'bpmn:CallActivity')) {
112
+ const calledElement = findExtensionElement(flowElement, 'zeebe:CalledElement');
113
+
114
+ if (calledElement) {
115
+ const processId = calledElement.get('processId');
116
+
117
+ if (isString(processId) && !isFeel(processId)) {
118
+ const process = findParent(flowElement, 'bpmn:Process');
119
+
120
+ if (process && process.get('id') === processId) {
121
+ return process.get('flowElements').filter(flowElement => is(flowElement, 'bpmn:StartEvent'));
122
+ }
123
+ }
124
+ }
125
+ } else if (is(flowElement, 'bpmn:SubProcess')) {
126
+ return flowElement
127
+ .get('flowElements').filter(flowElement => is(flowElement, 'bpmn:StartEvent'));
128
+ } else if (is(flowElement, 'bpmn:EndEvent')) {
129
+ const parent = flowElement.$parent;
130
+
131
+ if (is(parent, 'bpmn:SubProcess')) {
132
+ flowElement = parent;
133
+ }
134
+ }
135
+
136
+ return flowElement
137
+ .get('outgoing').filter(outgoing => is(outgoing, 'bpmn:SequenceFlow')).map(sequenceFlow => sequenceFlow.get('targetRef'));
138
+ }
139
+
140
+ function isIgnoredLoop(elements) {
141
+ return !elements.some(element => isAnyExactly(element, LOOP_REQUIRED_ELEMENT_TYPES));
142
+ }
143
+
144
+ function isFeel(value) {
145
+ return isString(value) && value.startsWith('=');
146
+ }
@@ -95,9 +95,13 @@ function validateSubscription(node) {
95
95
  }
96
96
 
97
97
  function getReport(propertyName, node, parentNode) {
98
+ const path = getPath(node, parentNode);
99
+
98
100
  return {
99
101
  message: `Property <${ propertyName }> is not a valid secret`,
100
- path: [ ...getPath(node, parentNode), propertyName ],
102
+ path: path
103
+ ? [ ...getPath(node, parentNode), propertyName ]
104
+ : [ propertyName ],
101
105
  data: {
102
106
  type: ERROR_TYPES.SECRET_EXPRESSION_INVALID,
103
107
  node,
@@ -3,6 +3,7 @@ const { is } = require('bpmnlint-utils');
3
3
  const {
4
4
  findExtensionElement,
5
5
  findExtensionElements,
6
+ findParent,
6
7
  hasProperties
7
8
  } = require('../utils/element');
8
9
 
@@ -76,22 +77,4 @@ function findUserTaskForm(node, formKey) {
76
77
  return `camunda-forms:bpmn:${ id }` === formKey;
77
78
  });
78
79
  }
79
- }
80
-
81
- function findParent(node, type) {
82
- if (!node) {
83
- return null;
84
- }
85
-
86
- const parent = node.$parent;
87
-
88
- if (!parent) {
89
- return node;
90
- }
91
-
92
- if (is(parent, type)) {
93
- return parent;
94
- }
95
-
96
- return findParent(parent, type);
97
80
  }
@@ -450,4 +450,24 @@ function addAllowedVersion(data, allowedVersion) {
450
450
  ...data,
451
451
  allowedVersion
452
452
  };
453
- }
453
+ }
454
+
455
+ function findParent(node, type) {
456
+ if (!node) {
457
+ return null;
458
+ }
459
+
460
+ const parent = node.$parent;
461
+
462
+ if (!parent) {
463
+ return node;
464
+ }
465
+
466
+ if (is(parent, type)) {
467
+ return parent;
468
+ }
469
+
470
+ return findParent(parent, type);
471
+ }
472
+
473
+ module.exports.findParent = findParent;
@@ -11,6 +11,7 @@ module.exports.ERROR_TYPES = Object.freeze({
11
11
  EXTENSION_ELEMENT_NOT_ALLOWED: 'camunda.extensionElementNotAllowed',
12
12
  EXTENSION_ELEMENT_REQUIRED: 'camunda.extensionElementRequired',
13
13
  FEEL_EXPRESSION_INVALID: 'camunda.feelExpressionInvalid',
14
+ LOOP_NOT_ALLOWED: 'camunda.loopNotAllowed',
14
15
  ATTACHED_TO_REF_ELEMENT_TYPE_NOT_ALLOWED: 'camunda.attachedToRefElementTypeNotAllowed',
15
16
  PROPERTY_DEPENDENT_REQUIRED: 'camunda.propertyDependentRequired',
16
17
  PROPERTY_NOT_ALLOWED: 'camunda.propertyNotAllowed',