Skip to content

Commit

Permalink
feat: add link-event rule
Browse files Browse the repository at this point in the history
Verifies correct usage of link events:

* name must be specified
* only a single pair of link {throw/catch} (no fork/join)
* no linking across scopes

Related to camunda/camunda-modeler#3532
  • Loading branch information
nikku committed Jan 30, 2024
1 parent 5a52c5f commit 87216de
Show file tree
Hide file tree
Showing 8 changed files with 438 additions and 42 deletions.
1 change: 1 addition & 0 deletions config/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const allRules = [
'event-sub-process-typed-start-event',
'fake-join',
'label-required',
'link-event',
'no-bpmndi',
'no-complex-gateway',
'no-disconnected',
Expand Down
1 change: 1 addition & 0 deletions config/recommended.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module.exports = {
'event-sub-process-typed-start-event': 'error',
'fake-join': 'warn',
'label-required': 'error',
'link-event': 'error',
'no-bpmndi': 'error',
'no-complex-gateway': 'error',
'no-disconnected': 'error',
Expand Down
100 changes: 100 additions & 0 deletions rules/link-event.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
const {
groupBy
} = require('min-dash');

const {
is
} = require('bpmnlint-utils');


/**
* A rule that verifies that link events are properly used.
*
* This implies:
*
* * for every link throw there exists a link catch within
* the same scope, and vice versa
* * there exists only a single pair of [ throw, catch ] links
* with a given name, per scope
* * link events have a name
*
*/
module.exports = function() {

function check(node, reporter) {

if (!is(node, 'bpmn:FlowElementsContainer')) {
return;
}

const links = (node.flowElements || []).filter(isLinkEvent);

for (const link of links) {
if (!link.name) {
reporter.report(link.id, 'Link event is missing name');
}
}

const names = groupBy(links, (link) => link.name);

for (const [ name, events ] of Object.entries(names)) {

// ignore unnamed (validated earlier)
if (!name) {
continue;
}

// missing catch or throw event
if (events.length === 1) {
const event = events[0];

reporter.report(event.id, `Link ${isThrowEvent(event) ? 'catch' : 'throw' } event with name <${ name }> missing in scope`);
}

const throwEvents = events.filter(isThrowEvent);

if (throwEvents.length > 1) {
for (const event of throwEvents) {
reporter.report(event.id, `Duplicate link throw event with name <${name}> in scope`);
}
}

const catchEvents = events.filter(isCatchEvent);

if (catchEvents.length > 1) {
for (const event of catchEvents) {
reporter.report(event.id, `Duplicate link catch event with name <${name}> in scope`);
}
}
}

}

return {
check
};
};


// helpers /////////////////

function isLinkEvent(node) {

var eventDefinitions = node.eventDefinitions || [];

if (!is(node, 'bpmn:Event')) {
return false;
}

return eventDefinitions.some(
definition => is(definition, 'bpmn:LinkEventDefinition')
);
}

function isThrowEvent(node) {
return is(node, 'bpmn:ThrowEvent');
}

function isCatchEvent(node) {
return is(node, 'bpmn:CatchEvent');
}
89 changes: 47 additions & 42 deletions test/integration/compilation/test/bpmnlintrc.expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const rules = {
"event-sub-process-typed-start-event": "error",
"fake-join": "warn",
"label-required": "error",
"link-event": "error",
"no-bpmndi": "error",
"no-complex-gateway": "error",
"no-disconnected": "warn",
Expand Down Expand Up @@ -90,86 +91,90 @@ import rule_4 from 'bpmnlint/rules/label-required';

cache['bpmnlint/label-required'] = rule_4;

import rule_5 from 'bpmnlint/rules/no-bpmndi';
import rule_5 from 'bpmnlint/rules/link-event';

cache['bpmnlint/no-bpmndi'] = rule_5;
cache['bpmnlint/link-event'] = rule_5;

import rule_6 from 'bpmnlint/rules/no-complex-gateway';
import rule_6 from 'bpmnlint/rules/no-bpmndi';

cache['bpmnlint/no-complex-gateway'] = rule_6;
cache['bpmnlint/no-bpmndi'] = rule_6;

import rule_7 from 'bpmnlint/rules/no-disconnected';
import rule_7 from 'bpmnlint/rules/no-complex-gateway';

cache['bpmnlint/no-disconnected'] = rule_7;
cache['bpmnlint/no-complex-gateway'] = rule_7;

import rule_8 from 'bpmnlint/rules/no-duplicate-sequence-flows';
import rule_8 from 'bpmnlint/rules/no-disconnected';

cache['bpmnlint/no-duplicate-sequence-flows'] = rule_8;
cache['bpmnlint/no-disconnected'] = rule_8;

import rule_9 from 'bpmnlint/rules/no-gateway-join-fork';
import rule_9 from 'bpmnlint/rules/no-duplicate-sequence-flows';

cache['bpmnlint/no-gateway-join-fork'] = rule_9;
cache['bpmnlint/no-duplicate-sequence-flows'] = rule_9;

import rule_10 from 'bpmnlint/rules/no-implicit-split';
import rule_10 from 'bpmnlint/rules/no-gateway-join-fork';

cache['bpmnlint/no-implicit-split'] = rule_10;
cache['bpmnlint/no-gateway-join-fork'] = rule_10;

import rule_11 from 'bpmnlint/rules/no-implicit-end';
import rule_11 from 'bpmnlint/rules/no-implicit-split';

cache['bpmnlint/no-implicit-end'] = rule_11;
cache['bpmnlint/no-implicit-split'] = rule_11;

import rule_12 from 'bpmnlint/rules/no-implicit-start';
import rule_12 from 'bpmnlint/rules/no-implicit-end';

cache['bpmnlint/no-implicit-start'] = rule_12;
cache['bpmnlint/no-implicit-end'] = rule_12;

import rule_13 from 'bpmnlint/rules/no-inclusive-gateway';
import rule_13 from 'bpmnlint/rules/no-implicit-start';

cache['bpmnlint/no-inclusive-gateway'] = rule_13;
cache['bpmnlint/no-implicit-start'] = rule_13;

import rule_14 from 'bpmnlint/rules/no-overlapping-elements';
import rule_14 from 'bpmnlint/rules/no-inclusive-gateway';

cache['bpmnlint/no-overlapping-elements'] = rule_14;
cache['bpmnlint/no-inclusive-gateway'] = rule_14;

import rule_15 from 'bpmnlint/rules/single-blank-start-event';
import rule_15 from 'bpmnlint/rules/no-overlapping-elements';

cache['bpmnlint/single-blank-start-event'] = rule_15;
cache['bpmnlint/no-overlapping-elements'] = rule_15;

import rule_16 from 'bpmnlint/rules/single-event-definition';
import rule_16 from 'bpmnlint/rules/single-blank-start-event';

cache['bpmnlint/single-event-definition'] = rule_16;
cache['bpmnlint/single-blank-start-event'] = rule_16;

import rule_17 from 'bpmnlint/rules/start-event-required';
import rule_17 from 'bpmnlint/rules/single-event-definition';

cache['bpmnlint/start-event-required'] = rule_17;
cache['bpmnlint/single-event-definition'] = rule_17;

import rule_18 from 'bpmnlint/rules/sub-process-blank-start-event';
import rule_18 from 'bpmnlint/rules/start-event-required';

cache['bpmnlint/sub-process-blank-start-event'] = rule_18;
cache['bpmnlint/start-event-required'] = rule_18;

import rule_19 from 'bpmnlint/rules/superfluous-gateway';
import rule_19 from 'bpmnlint/rules/sub-process-blank-start-event';

cache['bpmnlint/superfluous-gateway'] = rule_19;
cache['bpmnlint/sub-process-blank-start-event'] = rule_19;

import rule_20 from 'bpmnlint/rules/superfluous-termination';
import rule_20 from 'bpmnlint/rules/superfluous-gateway';

cache['bpmnlint/superfluous-termination'] = rule_20;
cache['bpmnlint/superfluous-gateway'] = rule_20;

import rule_21 from 'bpmnlint-plugin-test/rules/no-label-foo';
import rule_21 from 'bpmnlint/rules/superfluous-termination';

cache['bpmnlint-plugin-test/no-label-foo'] = rule_21;
cache['bpmnlint/superfluous-termination'] = rule_21;

import rule_22 from 'bpmnlint-plugin-exported/src/foo';
import rule_22 from 'bpmnlint-plugin-test/rules/no-label-foo';

cache['bpmnlint-plugin-exported/foo'] = rule_22;
cache['bpmnlint-plugin-test/no-label-foo'] = rule_22;

import rule_23 from 'bpmnlint-plugin-exported/src/bar';
import rule_23 from 'bpmnlint-plugin-exported/src/foo';

cache['bpmnlint-plugin-exported/bar'] = rule_23;
cache['bpmnlint-plugin-exported/foo'] = rule_23;

import rule_24 from 'bpmnlint-plugin-exported/rules/baz';
import rule_24 from 'bpmnlint-plugin-exported/src/bar';

cache['bpmnlint-plugin-exported/baz'] = rule_24;
cache['bpmnlint-plugin-exported/bar'] = rule_24;

import rule_25 from 'bpmnlint-plugin-exported/src/foo';
import rule_25 from 'bpmnlint-plugin-exported/rules/baz';

cache['bpmnlint-plugin-exported/foo-absolute'] = rule_25;
cache['bpmnlint-plugin-exported/baz'] = rule_25;

import rule_26 from 'bpmnlint-plugin-exported/src/foo';

cache['bpmnlint-plugin-exported/foo-absolute'] = rule_26;
82 changes: 82 additions & 0 deletions test/rules/link-event.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import RuleTester from '../../lib/testers/rule-tester.js';

import rule from '../../rules/link-event.js';

import {
readModdle
} from '../../lib/testers/helper.js';

import { stubCJS } from '../helper.mjs';

const {
__dirname
} = stubCJS(import.meta.url);


RuleTester.verify('link-event', rule, {
valid: [
{
moddleElement: readModdle(__dirname + '/link-event/valid.bpmn')
},
{
moddleElement: readModdle(__dirname + '/link-event/valid-collaboration.bpmn')
}
],
invalid: [
{
moddleElement: readModdle(__dirname + '/link-event/invalid.bpmn'),
report: [
{
'id': 'THROW_NO_NAME',
'message': 'Link event is missing name',
'category': 'error'
},
{
'id': 'CATCH_NO_NAME',
'message': 'Link event is missing name',
'category': 'error'
},
{
'id': 'NO_CATCH',
'message': 'Link catch event with name <NO_CATCH> missing in scope',
'category': 'error'
},
{
'id': 'NO_THROW',
'message': 'Link throw event with name <NO_THROW> missing in scope',
'category': 'error'
},
{
'id': 'SCOPE_BOUNDARY_THROW',
'message': 'Link catch event with name <SCOPE_BOUNDARY> missing in scope',
'category': 'error'
},
{
'id': 'DUPLICATE_NAME_THROW_1',
'message': 'Duplicate link throw event with name <DUPLICATE_NAME> in scope',
'category': 'error'
},
{
'id': 'DUPLICATE_NAME_THROW_2',
'message': 'Duplicate link throw event with name <DUPLICATE_NAME> in scope',
'category': 'error'
},
{
'id': 'DUPLICATE_NAME_CATCH_1',
'message': 'Duplicate link catch event with name <DUPLICATE_NAME> in scope',
'category': 'error'
},
{
'id': 'DUPLICATE_NAME_CATCH_2',
'message': 'Duplicate link catch event with name <DUPLICATE_NAME> in scope',
'category': 'error'
},
{
'id': 'SCOPE_BOUNDARY_CATCH',
'message': 'Link throw event with name <SCOPE_BOUNDARY> missing in scope',
'category': 'error'
}
]
}
]
});
Loading

0 comments on commit 87216de

Please sign in to comment.