UNPKG

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