From 87216de02ac2154c3de813b1b452ad2adc203d32 Mon Sep 17 00:00:00 2001 From: Nico Rehwaldt Date: Thu, 25 Jan 2024 15:01:14 +0100 Subject: [PATCH] feat: add `link-event` rule 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 https://github.com/camunda/camunda-modeler/issues/3532 --- config/all.js | 1 + config/recommended.js | 1 + rules/link-event.js | 100 +++++++++++++++++ .../compilation/test/bpmnlintrc.expected.js | 89 +++++++-------- test/rules/link-event.mjs | 82 ++++++++++++++ test/rules/link-event/invalid.bpmn | 101 ++++++++++++++++++ .../rules/link-event/valid-collaboration.bpmn | 56 ++++++++++ test/rules/link-event/valid.bpmn | 50 +++++++++ 8 files changed, 438 insertions(+), 42 deletions(-) create mode 100644 rules/link-event.js create mode 100644 test/rules/link-event.mjs create mode 100644 test/rules/link-event/invalid.bpmn create mode 100644 test/rules/link-event/valid-collaboration.bpmn create mode 100644 test/rules/link-event/valid.bpmn diff --git a/config/all.js b/config/all.js index c70709d..6a8890d 100644 --- a/config/all.js +++ b/config/all.js @@ -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', diff --git a/config/recommended.js b/config/recommended.js index 4f2e778..5eee56a 100644 --- a/config/recommended.js +++ b/config/recommended.js @@ -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', diff --git a/rules/link-event.js b/rules/link-event.js new file mode 100644 index 0000000..7934a59 --- /dev/null +++ b/rules/link-event.js @@ -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'); +} \ No newline at end of file diff --git a/test/integration/compilation/test/bpmnlintrc.expected.js b/test/integration/compilation/test/bpmnlintrc.expected.js index c88860b..4d6f171 100644 --- a/test/integration/compilation/test/bpmnlintrc.expected.js +++ b/test/integration/compilation/test/bpmnlintrc.expected.js @@ -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", @@ -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; \ No newline at end of file +cache['bpmnlint-plugin-exported/baz'] = rule_25; + +import rule_26 from 'bpmnlint-plugin-exported/src/foo'; + +cache['bpmnlint-plugin-exported/foo-absolute'] = rule_26; \ No newline at end of file diff --git a/test/rules/link-event.mjs b/test/rules/link-event.mjs new file mode 100644 index 0000000..4aabf40 --- /dev/null +++ b/test/rules/link-event.mjs @@ -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 missing in scope', + 'category': 'error' + }, + { + 'id': 'NO_THROW', + 'message': 'Link throw event with name missing in scope', + 'category': 'error' + }, + { + 'id': 'SCOPE_BOUNDARY_THROW', + 'message': 'Link catch event with name missing in scope', + 'category': 'error' + }, + { + 'id': 'DUPLICATE_NAME_THROW_1', + 'message': 'Duplicate link throw event with name in scope', + 'category': 'error' + }, + { + 'id': 'DUPLICATE_NAME_THROW_2', + 'message': 'Duplicate link throw event with name in scope', + 'category': 'error' + }, + { + 'id': 'DUPLICATE_NAME_CATCH_1', + 'message': 'Duplicate link catch event with name in scope', + 'category': 'error' + }, + { + 'id': 'DUPLICATE_NAME_CATCH_2', + 'message': 'Duplicate link catch event with name in scope', + 'category': 'error' + }, + { + 'id': 'SCOPE_BOUNDARY_CATCH', + 'message': 'Link throw event with name missing in scope', + 'category': 'error' + } + ] + } + ] +}); \ No newline at end of file diff --git a/test/rules/link-event/invalid.bpmn b/test/rules/link-event/invalid.bpmn new file mode 100644 index 0000000..e2dcdcc --- /dev/null +++ b/test/rules/link-event/invalid.bpmn @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/rules/link-event/valid-collaboration.bpmn b/test/rules/link-event/valid-collaboration.bpmn new file mode 100644 index 0000000..1df013e --- /dev/null +++ b/test/rules/link-event/valid-collaboration.bpmn @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/rules/link-event/valid.bpmn b/test/rules/link-event/valid.bpmn new file mode 100644 index 0000000..47ea7a1 --- /dev/null +++ b/test/rules/link-event/valid.bpmn @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +