diff --git a/config/all.js b/config/all.js
index 1a54780..e18d76c 100644
--- a/config/all.js
+++ b/config/all.js
@@ -1,4 +1,5 @@
const allRules = [
+ 'ad-hoc-sub-process',
'conditional-flows',
'end-event-required',
'event-sub-process-typed-start-event',
diff --git a/config/recommended.js b/config/recommended.js
index d9fbe9a..d8cc9a1 100644
--- a/config/recommended.js
+++ b/config/recommended.js
@@ -1,5 +1,6 @@
module.exports = {
rules: {
+ 'ad-hoc-sub-process': 'error',
'conditional-flows': 'error',
'end-event-required': 'error',
'event-sub-process-typed-start-event': 'error',
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 342ada4..30537a1 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -2,6 +2,7 @@
This library implements the following lint rules:
+* [Ad-Hoc Sub-Process](./ad-hoc-sub-process.md)
* [Conditional Flows](./conditional-flows.md)
* [End Event Required](./end-event-required.md)
* [Event Sub-Process Typed Start Event](./event-sub-process-typed-start-event.md)
diff --git a/docs/rules/ad-hoc-sub-process.md b/docs/rules/ad-hoc-sub-process.md
new file mode 100644
index 0000000..1ce397f
--- /dev/null
+++ b/docs/rules/ad-hoc-sub-process.md
@@ -0,0 +1,20 @@
+# Ad-hoc Sub-Process (ad-hoc-sub-process)
+
+Ensure that an Ad-Hoc Sub-Process is valid according to the BPMN specification:
+
+- Must not contain start or end events.
+- Every intermediate event must have an outgoing sequence flow.
+
+
+Example of __incorrect__ usage for this rule:
+
+
+
+Cf. [`ad-hoc-sub-process-incorrect.bpmn`](./examples/no-complex-gateway-incorrect.bpmn).
+
+
+Example of __correct__ usage for this rule:
+
+
+
+Cf. [`ad-hoc-sub-process-correct.bpmn`](./examples/no-complex-gateway-correct.bpmn).
diff --git a/docs/rules/examples/ad-hoc-sub-process-correct.bpmn b/docs/rules/examples/ad-hoc-sub-process-correct.bpmn
new file mode 100644
index 0000000..a6958f1
--- /dev/null
+++ b/docs/rules/examples/ad-hoc-sub-process-correct.bpmn
@@ -0,0 +1,58 @@
+
+
+
+
+
+ Flow_0njsews
+
+
+ Flow_0njsews
+
+
+
+ Flow_1ykoger
+
+
+
+
+
+
+ Flow_1ykoger
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/rules/examples/ad-hoc-sub-process-correct.png b/docs/rules/examples/ad-hoc-sub-process-correct.png
new file mode 100644
index 0000000..fc7d79a
Binary files /dev/null and b/docs/rules/examples/ad-hoc-sub-process-correct.png differ
diff --git a/docs/rules/examples/ad-hoc-sub-process-incorrect.bpmn b/docs/rules/examples/ad-hoc-sub-process-incorrect.bpmn
new file mode 100644
index 0000000..197b540
--- /dev/null
+++ b/docs/rules/examples/ad-hoc-sub-process-incorrect.bpmn
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+ Flow_0ej0cl8
+
+
+ Flow_0ej0cl8
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/rules/examples/ad-hoc-sub-process-incorrect.png b/docs/rules/examples/ad-hoc-sub-process-incorrect.png
new file mode 100644
index 0000000..64887d0
Binary files /dev/null and b/docs/rules/examples/ad-hoc-sub-process-incorrect.png differ
diff --git a/rules/ad-hoc-sub-process.js b/rules/ad-hoc-sub-process.js
new file mode 100644
index 0000000..791dc71
--- /dev/null
+++ b/rules/ad-hoc-sub-process.js
@@ -0,0 +1,45 @@
+const {
+ is,
+ isAny
+} = require('bpmnlint-utils');
+
+/**
+ * A rule that ensures that an Ad Hoc Sub Process is valid according to the BPMN spec:
+ *
+ * - No start or end events
+ * - Every intermediate event has an outgoing sequence flow
+ */
+module.exports = function() {
+
+ function check(node, reporter) {
+
+ if (!is(node, 'bpmn:AdHocSubProcess')) {
+ return;
+ }
+
+ const flowElements = node.flowElements || [];
+
+ flowElements.forEach(function(flowElement) {
+
+ if (is(flowElement, 'bpmn:StartEvent')) {
+ reporter.report(flowElement.id, 'A is not allowed in ');
+ }
+
+ if (is(flowElement, 'bpmn:EndEvent')) {
+ reporter.report(flowElement.id, 'An is not allowed in ');
+ }
+
+ if (isAny(flowElement, [ 'bpmn:IntermediateCatchEvent', 'bpmn:IntermediateThrowEvent' ])) {
+ if (!flowElement.outgoing || flowElement.outgoing.length === 0) {
+ reporter.report(flowElement.id, 'An intermediate event inside must have an outgoing sequence flow');
+ }
+ }
+
+ });
+ }
+
+ return {
+ check
+ };
+
+};
\ 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 a53770e..0144c83 100644
--- a/test/integration/compilation/test/bpmnlintrc.expected.js
+++ b/test/integration/compilation/test/bpmnlintrc.expected.js
@@ -29,6 +29,7 @@ Resolver.prototype.resolveConfig = function(pkg, configName) {
const resolver = new Resolver();
const rules = {
+ "ad-hoc-sub-process": "error",
"conditional-flows": "error",
"end-event-required": "error",
"event-sub-process-typed-start-event": "error",
@@ -72,114 +73,118 @@ export { resolver, config };
export default bundle;
-import rule_0 from 'bpmnlint/rules/conditional-flows';
+import rule_0 from 'bpmnlint/rules/ad-hoc-sub-process';
-cache['bpmnlint/conditional-flows'] = rule_0;
+cache['bpmnlint/ad-hoc-sub-process'] = rule_0;
-import rule_1 from 'bpmnlint/rules/end-event-required';
+import rule_1 from 'bpmnlint/rules/conditional-flows';
-cache['bpmnlint/end-event-required'] = rule_1;
+cache['bpmnlint/conditional-flows'] = rule_1;
-import rule_2 from 'bpmnlint/rules/event-sub-process-typed-start-event';
+import rule_2 from 'bpmnlint/rules/end-event-required';
-cache['bpmnlint/event-sub-process-typed-start-event'] = rule_2;
+cache['bpmnlint/end-event-required'] = rule_2;
-import rule_3 from 'bpmnlint/rules/fake-join';
+import rule_3 from 'bpmnlint/rules/event-sub-process-typed-start-event';
-cache['bpmnlint/fake-join'] = rule_3;
+cache['bpmnlint/event-sub-process-typed-start-event'] = rule_3;
-import rule_4 from 'bpmnlint/rules/global';
+import rule_4 from 'bpmnlint/rules/fake-join';
-cache['bpmnlint/global'] = rule_4;
+cache['bpmnlint/fake-join'] = rule_4;
-import rule_5 from 'bpmnlint/rules/label-required';
+import rule_5 from 'bpmnlint/rules/global';
-cache['bpmnlint/label-required'] = rule_5;
+cache['bpmnlint/global'] = rule_5;
-import rule_6 from 'bpmnlint/rules/link-event';
+import rule_6 from 'bpmnlint/rules/label-required';
-cache['bpmnlint/link-event'] = rule_6;
+cache['bpmnlint/label-required'] = rule_6;
-import rule_7 from 'bpmnlint/rules/no-bpmndi';
+import rule_7 from 'bpmnlint/rules/link-event';
-cache['bpmnlint/no-bpmndi'] = rule_7;
+cache['bpmnlint/link-event'] = rule_7;
-import rule_8 from 'bpmnlint/rules/no-complex-gateway';
+import rule_8 from 'bpmnlint/rules/no-bpmndi';
-cache['bpmnlint/no-complex-gateway'] = rule_8;
+cache['bpmnlint/no-bpmndi'] = rule_8;
-import rule_9 from 'bpmnlint/rules/no-disconnected';
+import rule_9 from 'bpmnlint/rules/no-complex-gateway';
-cache['bpmnlint/no-disconnected'] = rule_9;
+cache['bpmnlint/no-complex-gateway'] = rule_9;
-import rule_10 from 'bpmnlint/rules/no-duplicate-sequence-flows';
+import rule_10 from 'bpmnlint/rules/no-disconnected';
-cache['bpmnlint/no-duplicate-sequence-flows'] = rule_10;
+cache['bpmnlint/no-disconnected'] = rule_10;
-import rule_11 from 'bpmnlint/rules/no-gateway-join-fork';
+import rule_11 from 'bpmnlint/rules/no-duplicate-sequence-flows';
-cache['bpmnlint/no-gateway-join-fork'] = rule_11;
+cache['bpmnlint/no-duplicate-sequence-flows'] = rule_11;
-import rule_12 from 'bpmnlint/rules/no-implicit-split';
+import rule_12 from 'bpmnlint/rules/no-gateway-join-fork';
-cache['bpmnlint/no-implicit-split'] = rule_12;
+cache['bpmnlint/no-gateway-join-fork'] = rule_12;
-import rule_13 from 'bpmnlint/rules/no-implicit-end';
+import rule_13 from 'bpmnlint/rules/no-implicit-split';
-cache['bpmnlint/no-implicit-end'] = rule_13;
+cache['bpmnlint/no-implicit-split'] = rule_13;
-import rule_14 from 'bpmnlint/rules/no-implicit-start';
+import rule_14 from 'bpmnlint/rules/no-implicit-end';
-cache['bpmnlint/no-implicit-start'] = rule_14;
+cache['bpmnlint/no-implicit-end'] = rule_14;
-import rule_15 from 'bpmnlint/rules/no-inclusive-gateway';
+import rule_15 from 'bpmnlint/rules/no-implicit-start';
-cache['bpmnlint/no-inclusive-gateway'] = rule_15;
+cache['bpmnlint/no-implicit-start'] = rule_15;
-import rule_16 from 'bpmnlint/rules/no-overlapping-elements';
+import rule_16 from 'bpmnlint/rules/no-inclusive-gateway';
-cache['bpmnlint/no-overlapping-elements'] = rule_16;
+cache['bpmnlint/no-inclusive-gateway'] = rule_16;
-import rule_17 from 'bpmnlint/rules/single-blank-start-event';
+import rule_17 from 'bpmnlint/rules/no-overlapping-elements';
-cache['bpmnlint/single-blank-start-event'] = rule_17;
+cache['bpmnlint/no-overlapping-elements'] = rule_17;
-import rule_18 from 'bpmnlint/rules/single-event-definition';
+import rule_18 from 'bpmnlint/rules/single-blank-start-event';
-cache['bpmnlint/single-event-definition'] = rule_18;
+cache['bpmnlint/single-blank-start-event'] = rule_18;
-import rule_19 from 'bpmnlint/rules/start-event-required';
+import rule_19 from 'bpmnlint/rules/single-event-definition';
-cache['bpmnlint/start-event-required'] = rule_19;
+cache['bpmnlint/single-event-definition'] = rule_19;
-import rule_20 from 'bpmnlint/rules/sub-process-blank-start-event';
+import rule_20 from 'bpmnlint/rules/start-event-required';
-cache['bpmnlint/sub-process-blank-start-event'] = rule_20;
+cache['bpmnlint/start-event-required'] = rule_20;
-import rule_21 from 'bpmnlint/rules/superfluous-gateway';
+import rule_21 from 'bpmnlint/rules/sub-process-blank-start-event';
-cache['bpmnlint/superfluous-gateway'] = rule_21;
+cache['bpmnlint/sub-process-blank-start-event'] = rule_21;
-import rule_22 from 'bpmnlint/rules/superfluous-termination';
+import rule_22 from 'bpmnlint/rules/superfluous-gateway';
-cache['bpmnlint/superfluous-termination'] = rule_22;
+cache['bpmnlint/superfluous-gateway'] = rule_22;
-import rule_23 from 'bpmnlint-plugin-test/rules/no-label-foo';
+import rule_23 from 'bpmnlint/rules/superfluous-termination';
-cache['bpmnlint-plugin-test/no-label-foo'] = rule_23;
+cache['bpmnlint/superfluous-termination'] = rule_23;
-import rule_24 from 'bpmnlint-plugin-exported/src/foo';
+import rule_24 from 'bpmnlint-plugin-test/rules/no-label-foo';
-cache['bpmnlint-plugin-exported/foo'] = rule_24;
+cache['bpmnlint-plugin-test/no-label-foo'] = rule_24;
-import rule_25 from 'bpmnlint-plugin-exported/src/bar';
+import rule_25 from 'bpmnlint-plugin-exported/src/foo';
-cache['bpmnlint-plugin-exported/bar'] = rule_25;
+cache['bpmnlint-plugin-exported/foo'] = rule_25;
-import rule_26 from 'bpmnlint-plugin-exported/rules/baz';
+import rule_26 from 'bpmnlint-plugin-exported/src/bar';
-cache['bpmnlint-plugin-exported/baz'] = rule_26;
+cache['bpmnlint-plugin-exported/bar'] = rule_26;
-import rule_27 from 'bpmnlint-plugin-exported/src/foo';
+import rule_27 from 'bpmnlint-plugin-exported/rules/baz';
-cache['bpmnlint-plugin-exported/foo-absolute'] = rule_27;
\ No newline at end of file
+cache['bpmnlint-plugin-exported/baz'] = rule_27;
+
+import rule_28 from 'bpmnlint-plugin-exported/src/foo';
+
+cache['bpmnlint-plugin-exported/foo-absolute'] = rule_28;
\ No newline at end of file
diff --git a/test/rules/ad-hoc-sub-process.mjs b/test/rules/ad-hoc-sub-process.mjs
new file mode 100644
index 0000000..06221fd
--- /dev/null
+++ b/test/rules/ad-hoc-sub-process.mjs
@@ -0,0 +1,50 @@
+import RuleTester from '../../lib/testers/rule-tester.js';
+
+import rule from '../../rules/ad-hoc-sub-process.js';
+
+import {
+ readModdle
+} from '../../lib/testers/helper.js';
+
+import { stubCJS } from '../helper.mjs';
+
+const {
+ __dirname
+} = stubCJS(import.meta.url);
+
+
+RuleTester.verify('ad-hoc-sub-process', rule, {
+ valid: [
+ {
+ moddleElement: readModdle(__dirname + '/ad-hoc-sub-process/valid.bpmn')
+ }
+ ],
+ invalid: [
+ {
+ moddleElement: readModdle(__dirname + '/ad-hoc-sub-process/invalid-start-end.bpmn'),
+ report: [
+ {
+ id: 'StartEvent',
+ message: 'A is not allowed in '
+ },
+ {
+ id: 'EndEvent',
+ message: 'An is not allowed in '
+ }
+ ]
+ },
+ {
+ moddleElement: readModdle(__dirname + '/ad-hoc-sub-process/invalid-intermediate.bpmn'),
+ report: [
+ {
+ id: 'ThrowEvent',
+ message: 'An intermediate event inside must have an outgoing sequence flow'
+ },
+ {
+ id: 'CatchEvent',
+ message: 'An intermediate event inside must have an outgoing sequence flow'
+ }
+ ]
+ }
+ ]
+});
\ No newline at end of file
diff --git a/test/rules/ad-hoc-sub-process/invalid-intermediate.bpmn b/test/rules/ad-hoc-sub-process/invalid-intermediate.bpmn
new file mode 100644
index 0000000..416bf56
--- /dev/null
+++ b/test/rules/ad-hoc-sub-process/invalid-intermediate.bpmn
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/rules/ad-hoc-sub-process/invalid-start-end.bpmn b/test/rules/ad-hoc-sub-process/invalid-start-end.bpmn
new file mode 100644
index 0000000..05f2b90
--- /dev/null
+++ b/test/rules/ad-hoc-sub-process/invalid-start-end.bpmn
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/rules/ad-hoc-sub-process/valid.bpmn b/test/rules/ad-hoc-sub-process/valid.bpmn
new file mode 100644
index 0000000..1a98ade
--- /dev/null
+++ b/test/rules/ad-hoc-sub-process/valid.bpmn
@@ -0,0 +1,53 @@
+
+
+
+
+
+ Flow_1h8yyeu
+
+
+
+ Flow_0wahprq
+
+
+ Flow_1h8yyeu
+
+
+ Flow_0wahprq
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+