UNPKG

19.2 kBJavaScriptView Raw
1import { assign, groupBy, reduce } from 'min-dash';
2import { domify, query, remove } from 'min-dom';
3
4/**
5 * Traverse a moddle tree, depth first from top to bottom
6 * and call the passed visitor fn.
7 *
8 * @param {ModdleElement} element
9 * @param {Function} fn
10 */
11var traverse = function traverse(element, fn) {
12 fn(element);
13
14 var descriptor = element.$descriptor;
15
16 if (descriptor.isGeneric) {
17 return;
18 }
19
20 var containedProperties = descriptor.properties.filter(p => {
21 return !p.isAttr && !p.isReference && p.type !== 'String';
22 });
23
24 containedProperties.forEach(p => {
25 if (p.name in element) {
26 const propertyValue = element[p.name];
27
28 if (p.isMany) {
29 propertyValue.forEach(child => {
30 traverse(child, fn);
31 });
32 } else {
33 traverse(propertyValue, fn);
34 }
35 }
36 });
37};
38
39class Reporter {
40 constructor({ moddleRoot, rule }) {
41 this.rule = rule;
42 this.moddleRoot = moddleRoot;
43 this.messages = [];
44 this.report = this.report.bind(this);
45 }
46
47 report(id, message) {
48 this.messages.push({ id, message });
49 }
50}
51
52var testRule = function testRule({ moddleRoot, rule }) {
53 const reporter = new Reporter({ rule, moddleRoot });
54 traverse(moddleRoot, node => rule.check(node, reporter));
55 return reporter.messages;
56};
57
58const categoryMap = {
59 0: 'off',
60 1: 'warn',
61 2: 'error'
62};
63
64
65function Linter(options = {}) {
66
67 const {
68 config,
69 resolver
70 } = options;
71
72 if (typeof resolver === 'undefined') {
73 throw new Error('must provide <options.resolver>');
74 }
75
76 this.config = config;
77 this.resolver = resolver;
78
79 this.cachedRules = {};
80 this.cachedConfigs = {};
81}
82
83
84var linter = Linter;
85
86/**
87 * Applies a rule on the moddleRoot and adds reports to the finalReport
88 *
89 * @param {ModdleElement} moddleRoot
90 *
91 * @param {Object} ruleDefinition.name
92 * @param {Object} ruleDefinition.config
93 * @param {Object} ruleDefinition.category
94 * @param {Rule} ruleDefinition.rule
95 *
96 * @return {Array<ValidationErrors>} rule reports
97 */
98Linter.prototype.applyRule = function applyRule(moddleRoot, ruleDefinition) {
99
100 const {
101 config,
102 rule,
103 category,
104 name
105 } = ruleDefinition;
106
107 try {
108
109 const reports = testRule({
110 moddleRoot,
111 rule,
112 config
113 });
114
115 return reports.map(function(report) {
116 return {
117 ...report,
118 category
119 };
120 });
121 } catch (e) {
122 console.error('rule <' + name + '> failed with error: ', e);
123
124 return [
125 {
126 message: 'Rule error: ' + e.message,
127 category: 'error'
128 }
129 ];
130 }
131
132};
133
134
135Linter.prototype.resolveRule = function(name) {
136
137 const {
138 pkg,
139 ruleName
140 } = this.parseRuleName(name);
141
142 const id = `${pkg}-${ruleName}`;
143
144 const rule = this.cachedRules[id];
145
146 if (rule) {
147 return Promise.resolve(rule);
148 }
149
150 return Promise.resolve(this.resolver.resolveRule(pkg, ruleName)).then((ruleFactory) => {
151
152 if (!ruleFactory) {
153 throw new Error(`unknown rule <${name}>`);
154 }
155
156 const rule = this.cachedRules[id] = ruleFactory();
157
158 return rule;
159 });
160};
161
162Linter.prototype.resolveConfig = function(name) {
163
164 const {
165 pkg,
166 configName
167 } = this.parseConfigName(name);
168
169 const id = `${pkg}-${configName}`;
170
171 const config = this.cachedConfigs[id];
172
173 if (config) {
174 return Promise.resolve(config);
175 }
176
177 return Promise.resolve(this.resolver.resolveConfig(pkg, configName)).then((config) => {
178
179 if (!config) {
180 throw new Error(`unknown config <${name}>`);
181 }
182
183 const actualConfig = this.cachedConfigs[id] = normalizeConfig(config, pkg);
184
185 return actualConfig;
186 });
187};
188
189/**
190 * Take a linter config and return list of resolved rules.
191 *
192 * @param {Object} config
193 *
194 * @return {Array<RuleDefinition>}
195 */
196Linter.prototype.resolveRules = function(config) {
197
198 return this.resolveConfiguredRules(config).then((rulesConfig) => {
199
200 // parse rule values
201 const parsedRules = Object.entries(rulesConfig).map(([ name, value ]) => {
202 const {
203 category,
204 config
205 } = this.parseRuleValue(value);
206
207 return {
208 name,
209 category,
210 config
211 };
212 });
213
214 // filter only for enabled rules
215 const enabledRules = parsedRules.filter(definition => definition.category !== 'off');
216
217 // load enabled rules
218 const loaders = enabledRules.map((definition) => {
219
220 const {
221 name
222 } = definition;
223
224 return this.resolveRule(name).then(function(rule) {
225 return {
226 ...definition,
227 rule
228 };
229 });
230 });
231
232 return Promise.all(loaders);
233 });
234};
235
236
237Linter.prototype.resolveConfiguredRules = function(config) {
238
239 let parents = config.extends;
240
241 if (typeof parents === 'string') {
242 parents = [ parents ];
243 }
244
245 if (typeof parents === 'undefined') {
246 parents = [];
247 }
248
249 return Promise.all(
250 parents.map((configName) => {
251 return this.resolveConfig(configName).then((config) => {
252 return this.resolveConfiguredRules(config);
253 });
254 })
255 ).then((inheritedRules) => {
256
257 const overrideRules = normalizeConfig(config, 'bpmnlint').rules;
258
259 const rules = [ ...inheritedRules, overrideRules ].reduce((rules, currentRules) => {
260 return {
261 ...rules,
262 ...currentRules
263 };
264 }, {});
265
266 return rules;
267 });
268};
269
270
271/**
272 * Lint the given model root, using the specified linter config.
273 *
274 * @param {ModdleElement} moddleRoot
275 * @param {Object} [config] the bpmnlint configuration to use
276 *
277 * @return {Object} lint results, keyed by category names
278 */
279Linter.prototype.lint = function(moddleRoot, config) {
280
281 config = config || this.config;
282
283 // load rules
284 return this.resolveRules(config).then((ruleDefinitions) => {
285
286 const allReports = {};
287
288 ruleDefinitions.forEach((ruleDefinition) => {
289
290 const {
291 name
292 } = ruleDefinition;
293
294 const reports = this.applyRule(moddleRoot, ruleDefinition);
295
296 if (reports.length) {
297 allReports[name] = reports;
298 }
299 });
300
301 return allReports;
302 });
303};
304
305
306Linter.prototype.parseRuleValue = function(value) {
307
308 let category;
309 let config;
310
311 if (Array.isArray(value)) {
312 category = value[0];
313 config = value[1];
314 } else {
315 category = value;
316 config = {};
317 }
318
319 // normalize rule flag to <error> and <warn> which
320 // may be upper case or a number at this point
321 if (typeof category === 'string') {
322 category = category.toLowerCase();
323 }
324
325 category = categoryMap[category] || category;
326
327 return {
328 config,
329 category
330 };
331};
332
333Linter.prototype.parseRuleName = function(name) {
334
335 const slashIdx = name.indexOf('/');
336
337 // resolve rule as built-in, if unprefixed
338 if (slashIdx === -1) {
339 return {
340 pkg: 'bpmnlint',
341 ruleName: name
342 };
343 }
344
345 const pkg = name.substring(0, slashIdx);
346 const ruleName = name.substring(slashIdx + 1);
347
348 if (pkg === 'bpmnlint') {
349 return {
350 pkg: 'bpmnlint',
351 ruleName
352 };
353 } else {
354 return {
355 pkg: 'bpmnlint-plugin-' + pkg,
356 ruleName
357 };
358 }
359};
360
361
362Linter.prototype.parseConfigName = function(name) {
363
364 const localMatch = /^bpmnlint:(.*)$/.exec(name);
365
366 if (localMatch) {
367 return {
368 pkg: 'bpmnlint',
369 configName: localMatch[1]
370 };
371 }
372
373 const pluginMatch = /^plugin:([^/]+)\/(.+)$/.exec(name);
374
375 if (!pluginMatch) {
376 throw new Error(`invalid config name <${ name }>`);
377 }
378
379 return {
380 pkg: 'bpmnlint-plugin-' + pluginMatch[1],
381 configName: pluginMatch[2]
382 };
383};
384
385
386// helpers ///////////////////////////
387
388/**
389 * Validate and return validated config.
390 *
391 * @param {Object} config
392 * @param {String} pkg
393 *
394 * @return {Object} validated config
395 */
396function normalizeConfig(config, pkg) {
397
398 const rules = config.rules || {};
399
400 const validatedRules = Object.keys(rules).reduce((normalizedRules, name) => {
401
402 const value = rules[name];
403
404 // prefix local rule definition
405 if (name.indexOf('bpmnlint/') === 0) {
406 name = name.substring('bpmnlint/'.length);
407 }
408
409 normalizedRules[name] = value;
410
411 return normalizedRules;
412 }, {});
413
414 return {
415 ...config,
416 rules: validatedRules
417 };
418}
419
420var ErrorSvg = "<svg width=\"12\" height=\"12\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 352 512\"><path fill=\"currentColor\" d=\"M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z\"></path></svg>";
421
422var WarningSvg = "<svg width=\"12\" height=\"12\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 576 512\"><path fill=\"currentColor\" d=\"M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z\"></path></svg>";
423
424var SuccessSvg = "<svg width=\"12\" height=\"12\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\"><path fill=\"currentColor\" d=\"M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z\"></path></svg>";
425
426var OFFSET_TOP = -5,
427 OFFSET_RIGHT = -5;
428
429var LOW_PRIORITY = 500;
430
431var emptyConfig = {
432 resolver: {
433 resolveRule: function() {
434 return null;
435 }
436 },
437 config: {}
438};
439
440
441function Linting(
442 config, bpmnjs, canvas,
443 elementRegistry, eventBus, overlays
444) {
445
446 if ('bpmnlint' in config) {
447 this._linterConfig = config.bpmnlint;
448 }
449
450 if (!this._linterConfig || !this._linterConfig.config || !this._linterConfig.resolver) {
451
452 console.warn(
453 '[bpmn-js-bpmnlint] You did not configure any lint rules to use. ' +
454 'Ensure you bundle and include your rules via the `linting.bpmnlint` option. ' +
455 'See https://github.com/bpmn-io/bpmn-js-bpmnlint#configure-lint-rules'
456 );
457
458 this._linterConfig = emptyConfig;
459 }
460
461 this._bpmnjs = bpmnjs;
462 this._canvas = canvas;
463 this._elementRegistry = elementRegistry;
464 this._eventBus = eventBus;
465 this._overlays = overlays;
466
467 this._issues = null;
468 this._lintingActive = false;
469
470 this._overlayIds = {};
471
472 var self = this;
473
474 eventBus.on('elements.changed', LOW_PRIORITY, function(e) {
475 if (self.lintingActive()) {
476 self.update();
477 }
478 });
479
480 eventBus.on('diagram.clear', function() {
481 self.clearIssues();
482 });
483
484 this._init();
485}
486Linting.prototype._init = function() {
487 var self = this;
488
489 var button = this._button = domify(
490 '<button class="bpmn-js-bpmnlint-button inactive"></button>'
491 );
492
493 this.updateButton();
494
495 button.addEventListener('click', function() {
496 self.toggleLinting();
497 });
498
499 this._canvas.getContainer().appendChild(button);
500};
501
502Linting.prototype.lintingActive = function() {
503 return this._lintingActive;
504};
505
506Linting.prototype.formatIssues = function(issues) {
507
508 const reports = reduce(issues, function(reports, ruleReports, rule) {
509
510 return reports.concat(ruleReports.map(function(report) {
511 report.rule = rule;
512
513 return report;
514 }));
515
516 }, []);
517
518 return groupBy(reports, function(report) {
519 return report.id;
520 });
521};
522
523Linting.prototype.toggleLinting = function() {
524 if (this.lintingActive()) {
525 this.deactivateLinting();
526 } else {
527 this.activateLinting();
528 }
529};
530
531Linting.prototype.activateLinting = function() {
532 var self = this;
533
534 this.setActive(true);
535
536 this.lint()
537 .then(function(issues) {
538 self._issues = self.formatIssues(issues);
539
540 self.createIssues(self._issues);
541
542 self.updateButton();
543 });
544};
545
546Linting.prototype.deactivateLinting = function() {
547 this.setActive(false);
548
549 this.clearIssues();
550
551 this.updateButton();
552};
553
554Linting.prototype.setActive = function(active) {
555 this._lintingActive = active;
556
557 this._eventBus.fire('linting.toggle', { active: active });
558};
559
560/**
561 * Update overlays. Always lint and check wether overlays need update or not.
562 */
563Linting.prototype.update = function() {
564 var self = this;
565
566 this.lint()
567 .then(function(newIssues) {
568 newIssues = self.formatIssues(newIssues);
569
570 var remove$$1 = {},
571 update = {},
572 add = {};
573
574 for (var id in self._issues) {
575 if (!newIssues[id]) {
576 remove$$1[id] = self._issues[id];
577 }
578 }
579
580 for (var id in newIssues) {
581 if (!self._issues[id]) {
582 add[id] = newIssues[id];
583 } else {
584 if (newIssues[id] !== self._issues[id]) {
585 update[id] = newIssues[id];
586 }
587 }
588 }
589
590 remove$$1 = assign(remove$$1, update);
591 add = assign(add, update);
592
593 self.removeProcessIssues();
594
595 self.removeIssues(remove$$1);
596 self.createIssues(add);
597
598 self._issues = newIssues;
599
600 self.updateButton();
601 });
602};
603
604Linting.prototype.createIssues = function(issues) {
605 for (var id in issues) {
606 this.createElementIssues(id, issues[id]);
607 }
608};
609
610/**
611 * Create overlays for an elements issues.
612 *
613 * @param {String} elementId - Elements ID.
614 * @param {Array} elementIssues - All element issues including warnings and errors.
615 */
616Linting.prototype.createElementIssues = function(elementId, elementIssues) {
617
618 console.log(elementId, elementIssues);
619
620 var element = this._elementRegistry.get(elementId);
621
622 if (!element) {
623 console.log('element <' + elementId + '> not found');
624
625 return;
626 }
627
628 var isProcess = element.type === 'bpmn:Process';
629
630 var position = { top: OFFSET_TOP, right: OFFSET_RIGHT };
631
632 elementIssues = groupBy(elementIssues, function(elementIssue) {
633 return elementIssue.category
634 });
635
636 var errors = elementIssues.error,
637 warnings = elementIssues.warn;
638
639 var html = domify('<div class="bpmn-js-bpmnlint-issues"><div class="icons"></div></div>');
640
641 var icons = query('.icons', html);
642
643 var group;
644
645 if (errors) {
646 icons.appendChild(domify('<span class="icon error">' + ErrorSvg + '</span>'));
647
648 group = this.createGroup(errors, 'error', 'Errors', ErrorSvg);
649
650 html.appendChild(group);
651 }
652
653 if (warnings) {
654 icons.appendChild(domify('<span class="icon warning">' + WarningSvg + '</span>'));
655
656 group = this.createGroup(warnings, 'warning', 'Warnings', WarningSvg);
657
658 html.appendChild(group);
659 }
660
661 if (isProcess) {
662 this.createProcessIssues(html);
663 } else {
664 this._overlayIds[elementId] = this._overlays.add(element, 'bpmnlint', {
665 position: position,
666 html: html,
667 scale: {
668 min: 1,
669 max: 1
670 }
671 });
672 }
673
674};
675
676Linting.prototype.createGroup = function(issues, type, label, icon) {
677
678 var group = domify(
679 '<div class="group">' +
680 '<div class="header ' + type + '">' + icon + label + '</div>' +
681 '</div>'
682 );
683
684 issues.forEach(function(issue) {
685 var collapsable = domify(
686 '<div class="collapsable ' + type + '">' +
687 issue.message +
688 '</div>'
689 );
690
691 group.appendChild(collapsable);
692 });
693
694 return group;
695};
696
697Linting.prototype.removeIssues = function(issues) {
698 var overlayId;
699
700 for (var id in issues) {
701 if (id === 'Process') {
702 this.removeProcessIssues();
703 } else {
704 overlayId = this._overlayIds[id];
705
706 // ignore process
707 if (overlayId) {
708 this._overlays.remove(overlayId);
709 }
710 }
711 }
712};
713
714/**
715 * Removes all overlays and clears cached issues.
716 */
717Linting.prototype.clearIssues = function() {
718 var overlayId;
719
720 for (var id in this._overlayIds) {
721 overlayId = this._overlayIds[id];
722
723 this._overlays.remove(overlayId);
724 }
725
726 this._issues = null;
727
728 this.removeProcessIssues();
729};
730
731/**
732 * Sets button state to reflect if linting is active.
733 *
734 * @param {String} state
735 */
736Linting.prototype.setButtonState = function(state, html) {
737 if (state === 'success') {
738 this._button.classList.add('success');
739 this._button.classList.remove('error');
740 this._button.classList.remove('inactive');
741 this._button.classList.remove('warning');
742 } else if (state === 'error') {
743 this._button.classList.add('error');
744 this._button.classList.remove('inactive');
745 this._button.classList.remove('success');
746 this._button.classList.remove('warning');
747 } else if (state === 'warning') {
748 this._button.classList.add('warning');
749 this._button.classList.remove('error');
750 this._button.classList.remove('inactive');
751 this._button.classList.remove('success');
752 } else if (state === 'inactive') {
753 this._button.classList.add('inactive');
754 this._button.classList.remove('error');
755 this._button.classList.remove('success');
756 this._button.classList.remove('warning');
757 }
758
759 this._button.innerHTML = html;
760};
761
762Linting.prototype.updateButton = function() {
763 if (this._lintingActive) {
764 var errors = 0,
765 warnings = 0;
766
767 for (var id in this._issues) {
768 this._issues[id].forEach(function(issue) {
769 if (issue.category === 'error') {
770 errors++;
771 } else if (issue.category === 'warn') {
772 warnings++;
773 }
774 });
775 }
776
777 if (errors) {
778 this.setButtonState('error', ErrorSvg + '<span>' + errors + ' Errors, ' + warnings + ' Warnings</span>');
779 } else if (warnings) {
780 this.setButtonState('warning', WarningSvg + '<span>' + errors + ' Errors, ' + warnings + ' Warnings</span>');
781 } else {
782 this.setButtonState('success', SuccessSvg + '<span>0 Errors, 0 Warnings</span>');
783 }
784
785 } else {
786 this.setButtonState('inactive', SuccessSvg + '<span>0 Errors, 0 Warnings</span>');
787 }
788
789};
790
791Linting.prototype.createProcessIssues = function(html) {
792 var container = this._canvas.getContainer();
793
794 html.classList.add('bpmn-js-bpmnlint-process-issues');
795
796 container.appendChild(html);
797};
798
799Linting.prototype.removeProcessIssues = function() {
800 var container = this._canvas.getContainer();
801
802 var html = query('.bpmn-js-bpmnlint-process-issues', container);
803
804 if (html) {
805 remove(html);
806 }
807};
808
809Linting.prototype.lint = function() {
810
811 var definitions = this._bpmnjs.getDefinitions();
812
813 var linter$$1 = new linter(this._linterConfig);
814
815 return linter$$1.lint(definitions);
816};
817
818Linting.$inject = [
819 'config.linting',
820 'bpmnjs',
821 'canvas',
822 'elementRegistry',
823 'eventBus',
824 'overlays'
825];
826
827var index = {
828 __init__: [ 'linting' ],
829 linting: [ 'type', Linting ]
830};
831
832export default index;
833//# sourceMappingURL=index.esm.js.map