bpmnlint 11.14.1 → 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.1",
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
  },
@@ -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;