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/docs/rules/examples/link-event-correct.bpmn b/docs/rules/examples/link-event-correct.bpmn new file mode 100644 index 0000000..7a1697c --- /dev/null +++ b/docs/rules/examples/link-event-correct.bpmn @@ -0,0 +1,51 @@ + + + + + Flow_0w6huzu + + + + Flow_0w6huzu + + + + Flow_1yhu2w5 + + + Flow_1yhu2w5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/rules/examples/link-event-correct.png b/docs/rules/examples/link-event-correct.png new file mode 100644 index 0000000..98282ca Binary files /dev/null and b/docs/rules/examples/link-event-correct.png differ diff --git a/docs/rules/examples/link-event-incorrect.bpmn b/docs/rules/examples/link-event-incorrect.bpmn new file mode 100644 index 0000000..4d6f7d8 --- /dev/null +++ b/docs/rules/examples/link-event-incorrect.bpmn @@ -0,0 +1,78 @@ + + + + + Flow_0w6huzu + + + + Flow_0w6huzu + + + + + Flow_1v2vhxj + + + + Flow_1v2vhxj + + + + + + Flow_1m6xxsl + + + + Flow_1m6xxsl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/rules/examples/link-event-incorrect.png b/docs/rules/examples/link-event-incorrect.png new file mode 100644 index 0000000..1bb6adc Binary files /dev/null and b/docs/rules/examples/link-event-incorrect.png differ diff --git a/docs/rules/link-event.md b/docs/rules/link-event.md new file mode 100644 index 0000000..1644b69 --- /dev/null +++ b/docs/rules/link-event.md @@ -0,0 +1,20 @@ +# Link Event (link-event) + +A rule that checks that link events are used in accordance with the BPMN specification: + +* A link event must be named +* For every link throw event there exists a matching catch event in the same scope + + +Example of __incorrect__ usage for this rule: + +![Incorrect usage example](./examples/link-event-incorrect.png) + +Cf. [`link-event-incorrect.bpmn`](./examples/link-event-incorrect.bpmn). + + +Example of __correct__ usage for this rule: + +![Correct usage example](./examples/link-event-correct.png) + +Cf. [`link-event-correct.bpmn`](./examples/link-event-correct.bpmn). diff --git a/package.json b/package.json index 5dcc1ac..0ae92cc 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "license": "MIT", "scripts": { "all": "run-s lint test-coverage", - "lint": "eslint .", + "lint": "eslint . --ext js,mjs", "dev": "npm test -- --watch", "test": "mocha --exclude 'test/integration/*/**' 'test/**/*.*js'", "test-coverage": "nyc --reporter=lcov --reporter=html npm test", 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +