bpmnlint 11.14.0 → 11.15.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bpmnlint",
3
3
  "description": "Validate your BPMN diagrams based on configurable lint rules",
4
- "version": "11.14.0",
4
+ "version": "11.15.0",
5
5
  "main": "lib/index.js",
6
6
  "keywords": [
7
7
  "bpmnlint",
@@ -27,11 +27,12 @@
27
27
  },
28
28
  "license": "MIT",
29
29
  "scripts": {
30
- "all": "run-s lint lint:types test-coverage",
30
+ "all": "run-s lint lint:types test-coverage test:perf",
31
31
  "lint": "eslint .",
32
32
  "lint:types": "tsc",
33
33
  "dev": "npm test -- --watch",
34
- "test": "mocha --exclude \"test/integration/*/**\" \"test/**/*.*js\"",
34
+ "test": "mocha --exclude \"test/integration/*/**\" --exclude \"test/performance/**\" \"test/**/*.*js\"",
35
+ "test:perf": "mocha \"test/performance/**/*.*js\"",
35
36
  "test-coverage": "nyc --reporter=lcov --reporter=html npm test",
36
37
  "generate-docs-images": "npx @bpmn-io/bpmnlint-generate-docs-images ."
37
38
  },
@@ -35,12 +35,12 @@ module.exports = function() {
35
35
  // (2) Filter BPMN elements without visual representation
36
36
  const visualBpmnElements = bpmnElements.filter(hasVisualRepresentation);
37
37
 
38
- // (3) Construct array of BPMNDI references
39
- const diBpmnReferences = getAllDiBpmnReferences(node);
38
+ // (3) Construct set of BPMNDI references
39
+ const referencedBpmnElementIds = getReferencedBpmnElementIds(node);
40
40
 
41
41
  // (4) Report elements without BPMNDI
42
42
  visualBpmnElements.forEach((element) => {
43
- if (diBpmnReferences.indexOf(element.id) === -1) {
43
+ if (!referencedBpmnElementIds.has(element.id)) {
44
44
  reporter.report(element.id, 'Element is missing bpmndi');
45
45
  }
46
46
  });
@@ -105,16 +105,17 @@ function getAllBpmnElements(rootElements) {
105
105
  }
106
106
 
107
107
  /**
108
- * Get all BPMN elements within a bpmn:Definitions node
108
+ * Get the ids of all BPMN elements referenced by BPMNDI elements
109
+ * within a bpmn:Definitions node
109
110
  *
110
111
  * @param {ModdleElement} definitionsNode - A moddleElement representing the
111
112
  * bpmn:Definitions element
112
113
  *
113
- * @return {string[]} ids of all BPMNDI element part of
114
- * this bpmn:Definitions node
114
+ * @return {Set<string>} ids of all BPMN elements referenced by BPMNDI
115
+ * elements part of this bpmn:Definitions node
115
116
  */
116
- function getAllDiBpmnReferences(definitionsNode) {
117
- return flatten(
117
+ function getReferencedBpmnElementIds(definitionsNode) {
118
+ return new Set(flatten(
118
119
  definitionsNode.get('diagrams').map((diagram) => {
119
120
 
120
121
  const diElements = diagram.plane.planeElement || [];
@@ -124,7 +125,7 @@ function getAllDiBpmnReferences(definitionsNode) {
124
125
  return element.bpmnElement?.id;
125
126
  });
126
127
  })
127
- );
128
+ ));
128
129
  }
129
130
 
130
131
  /**
@@ -6,6 +6,18 @@ const {
6
6
  annotateRule
7
7
  } = require('./helper');
8
8
 
9
+ /**
10
+ * @typedef { import('../lib/types.js').ModdleElement } ModdleElement
11
+ *
12
+ * @typedef { {
13
+ * element: ModdleElement,
14
+ * left: number,
15
+ * right: number,
16
+ * top: number,
17
+ * bottom: number
18
+ * } } Shape
19
+ */
20
+
9
21
 
10
22
  /**
11
23
  * Rule that checks if two elements overlap except:
@@ -99,36 +111,88 @@ function checkProcess(node, elementsToReport, elementsOutsideToReport, diObjects
99
111
  }
100
112
 
101
113
  /**
114
+ * Check elements for overlap.
115
+ *
116
+ * Sweeps the shapes of the elements from left to right, so that every shape
117
+ * is only compared with the shapes that start before it ends.
118
+ *
102
119
  * @param {Array} elements
103
120
  * @param {Set} elementsToReport
121
+ * @param {Map} diObjects
104
122
  */
105
123
  function checkElementsArray(elements, elementsToReport, diObjects) {
106
- for (let i = 0; i < elements.length - 1; i++) {
107
- const element = elements[i];
124
+ const shapes = getShapes(elements, diObjects).sort((a, b) => a.left - b.left);
108
125
 
109
- for (let j = i + 1; j < elements.length; j++) {
110
- const element2 = elements[j];
126
+ const overlapping = new Set();
111
127
 
112
- // ignore if Boundary events overlap their host
113
- // but still check if they overlap other elements
114
- if (element.attachedToRef === element2 || element2.attachedToRef === element) {
115
- continue;
116
- }
128
+ for (let i = 0; i < shapes.length; i++) {
129
+ const shape = shapes[i];
117
130
 
118
- const bounds1 = diObjects.get(element)?.bounds;
119
- const bounds2 = diObjects.get(element2)?.bounds;
131
+ // shapes starting right of this one cannot overlap it
132
+ for (let j = i + 1; j < shapes.length && shapes[j].left <= shape.right; j++) {
133
+ const otherShape = shapes[j];
120
134
 
121
- // ignore if an element doesn't have bounds
122
- if (!bounds1 || !bounds2) {
135
+ if (!isCollision(shape, otherShape)) {
123
136
  continue;
124
137
  }
125
138
 
126
- if (isCollision(bounds1, bounds2)) {
127
- elementsToReport.add(element);
128
- elementsToReport.add(element2);
139
+ // ignore if Boundary events overlap their host
140
+ // but still check if they overlap other elements
141
+ if (shape.element.attachedToRef === otherShape.element || otherShape.element.attachedToRef === shape.element) {
142
+ continue;
129
143
  }
144
+
145
+ overlapping.add(shape.element);
146
+ overlapping.add(otherShape.element);
130
147
  }
131
148
  }
149
+
150
+ // report in element order, independent of the order overlaps are detected
151
+ elements.forEach(element => {
152
+ if (overlapping.has(element)) {
153
+ elementsToReport.add(element);
154
+ }
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Get the shapes of all elements with valid bounds, the only elements
160
+ * that can overlap.
161
+ *
162
+ * @param {Array} elements
163
+ * @param {Map} diObjects
164
+ *
165
+ * @return {Shape[]}
166
+ */
167
+ function getShapes(elements, diObjects) {
168
+ const shapes = [];
169
+
170
+ elements.forEach(element => {
171
+ const bounds = diObjects.get(element)?.bounds;
172
+
173
+ if (!isValidShapeElement(bounds)) {
174
+ return;
175
+ }
176
+
177
+ const left = bounds.x;
178
+ const right = bounds.x + bounds.width;
179
+
180
+ // shapes with a NaN left or right edge never collide,
181
+ // and a NaN left edge would break the sweep
182
+ if (Number.isNaN(left) || Number.isNaN(right)) {
183
+ return;
184
+ }
185
+
186
+ shapes.push({
187
+ element,
188
+ left,
189
+ right,
190
+ top: bounds.y,
191
+ bottom: bounds.y + bounds.height
192
+ });
193
+ });
194
+
195
+ return shapes;
132
196
  }
133
197
 
134
198
  /**
@@ -148,14 +212,15 @@ function isOutsideParentBoundary(childBounds, parentBounds) {
148
212
 
149
213
  /**
150
214
  * Check if two rectangle shapes collides
215
+ *
216
+ * @param {Shape} firstShape
217
+ * @param {Shape} secondShape
218
+ *
219
+ * @return {boolean}
151
220
  */
152
- function isCollision(firstBounds, secondBounds) {
153
- if (!isValidShapeElement(firstBounds) || !isValidShapeElement(secondBounds)) {
154
- return false;
155
- }
156
-
157
- const collisionX = firstBounds.x + firstBounds.width >= secondBounds.x && secondBounds.x + secondBounds.width >= firstBounds.x;
158
- const collisionY = firstBounds.y + firstBounds.height >= secondBounds.y && secondBounds.y + secondBounds.height >= firstBounds.y;
221
+ function isCollision(firstShape, secondShape) {
222
+ const collisionX = firstShape.right >= secondShape.left && secondShape.right >= firstShape.left;
223
+ const collisionY = firstShape.bottom >= secondShape.top && secondShape.bottom >= firstShape.top;
159
224
 
160
225
  // collision on both axis
161
226
  return collisionX && collisionY;