UNPKG

6.85 kBJavaScriptView Raw
1const testRule = require('./test-rule');
2
3const categoryMap = {
4 0: 'off',
5 1: 'warn',
6 2: 'error'
7};
8
9
10function Linter(options = {}) {
11
12 const {
13 config,
14 resolver
15 } = options;
16
17 if (typeof resolver === 'undefined') {
18 throw new Error('must provide <options.resolver>');
19 }
20
21 this.config = config;
22 this.resolver = resolver;
23
24 this.cachedRules = {};
25 this.cachedConfigs = {};
26}
27
28
29module.exports = Linter;
30
31/**
32 * Applies a rule on the moddleRoot and adds reports to the finalReport
33 *
34 * @param {ModdleElement} moddleRoot
35 *
36 * @param {Object} ruleDefinition.name
37 * @param {Object} ruleDefinition.config
38 * @param {Object} ruleDefinition.category
39 * @param {Rule} ruleDefinition.rule
40 *
41 * @return {Array<ValidationErrors>} rule reports
42 */
43Linter.prototype.applyRule = function applyRule(moddleRoot, ruleDefinition) {
44
45 const {
46 config,
47 rule,
48 category,
49 name
50 } = ruleDefinition;
51
52 try {
53
54 const reports = testRule({
55 moddleRoot,
56 rule,
57 config
58 });
59
60 return reports.map(function(report) {
61 return {
62 ...report,
63 category
64 };
65 });
66 } catch (e) {
67 console.error('rule <' + name + '> failed with error: ', e);
68
69 return [
70 {
71 message: 'Rule error: ' + e.message,
72 category: 'error'
73 }
74 ];
75 }
76
77};
78
79
80Linter.prototype.resolveRule = function(name) {
81
82 const {
83 pkg,
84 ruleName
85 } = this.parseRuleName(name);
86
87 const id = `${pkg}-${ruleName}`;
88
89 const rule = this.cachedRules[id];
90
91 if (rule) {
92 return Promise.resolve(rule);
93 }
94
95 return Promise.resolve(this.resolver.resolveRule(pkg, ruleName)).then((ruleFactory) => {
96
97 if (!ruleFactory) {
98 throw new Error(`unknown rule <${name}>`);
99 }
100
101 const rule = this.cachedRules[id] = ruleFactory();
102
103 return rule;
104 });
105};
106
107Linter.prototype.resolveConfig = function(name) {
108
109 const {
110 pkg,
111 configName
112 } = this.parseConfigName(name);
113
114 const id = `${pkg}-${configName}`;
115
116 const config = this.cachedConfigs[id];
117
118 if (config) {
119 return Promise.resolve(config);
120 }
121
122 return Promise.resolve(this.resolver.resolveConfig(pkg, configName)).then((config) => {
123
124 if (!config) {
125 throw new Error(`unknown config <${name}>`);
126 }
127
128 const actualConfig = this.cachedConfigs[id] = normalizeConfig(config, pkg);
129
130 return actualConfig;
131 });
132};
133
134/**
135 * Take a linter config and return list of resolved rules.
136 *
137 * @param {Object} config
138 *
139 * @return {Array<RuleDefinition>}
140 */
141Linter.prototype.resolveRules = function(config) {
142
143 return this.resolveConfiguredRules(config).then((rulesConfig) => {
144
145 // parse rule values
146 const parsedRules = Object.entries(rulesConfig).map(([ name, value ]) => {
147 const {
148 category,
149 config
150 } = this.parseRuleValue(value);
151
152 return {
153 name,
154 category,
155 config
156 };
157 });
158
159 // filter only for enabled rules
160 const enabledRules = parsedRules.filter(definition => definition.category !== 'off');
161
162 // load enabled rules
163 const loaders = enabledRules.map((definition) => {
164
165 const {
166 name
167 } = definition;
168
169 return this.resolveRule(name).then(function(rule) {
170 return {
171 ...definition,
172 rule
173 };
174 });
175 });
176
177 return Promise.all(loaders);
178 });
179};
180
181
182Linter.prototype.resolveConfiguredRules = function(config) {
183
184 let parents = config.extends;
185
186 if (typeof parents === 'string') {
187 parents = [ parents ];
188 }
189
190 if (typeof parents === 'undefined') {
191 parents = [];
192 }
193
194 return Promise.all(
195 parents.map((configName) => {
196 return this.resolveConfig(configName).then((config) => {
197 return this.resolveConfiguredRules(config);
198 });
199 })
200 ).then((inheritedRules) => {
201
202 const overrideRules = normalizeConfig(config, 'bpmnlint').rules;
203
204 const rules = [ ...inheritedRules, overrideRules ].reduce((rules, currentRules) => {
205 return {
206 ...rules,
207 ...currentRules
208 };
209 }, {});
210
211 return rules;
212 });
213};
214
215
216/**
217 * Lint the given model root, using the specified linter config.
218 *
219 * @param {ModdleElement} moddleRoot
220 * @param {Object} [config] the bpmnlint configuration to use
221 *
222 * @return {Object} lint results, keyed by category names
223 */
224Linter.prototype.lint = function(moddleRoot, config) {
225
226 config = config || this.config;
227
228 // load rules
229 return this.resolveRules(config).then((ruleDefinitions) => {
230
231 const allReports = {};
232
233 ruleDefinitions.forEach((ruleDefinition) => {
234
235 const {
236 name
237 } = ruleDefinition;
238
239 const reports = this.applyRule(moddleRoot, ruleDefinition);
240
241 if (reports.length) {
242 allReports[name] = reports;
243 }
244 });
245
246 return allReports;
247 });
248};
249
250
251Linter.prototype.parseRuleValue = function(value) {
252
253 let category;
254 let config;
255
256 if (Array.isArray(value)) {
257 category = value[0];
258 config = value[1];
259 } else {
260 category = value;
261 config = {};
262 }
263
264 // normalize rule flag to <error> and <warn> which
265 // may be upper case or a number at this point
266 if (typeof category === 'string') {
267 category = category.toLowerCase();
268 }
269
270 category = categoryMap[category] || category;
271
272 return {
273 config,
274 category
275 };
276};
277
278Linter.prototype.parseRuleName = function(name) {
279
280 const slashIdx = name.indexOf('/');
281
282 // resolve rule as built-in, if unprefixed
283 if (slashIdx === -1) {
284 return {
285 pkg: 'bpmnlint',
286 ruleName: name
287 };
288 }
289
290 const pkg = name.substring(0, slashIdx);
291 const ruleName = name.substring(slashIdx + 1);
292
293 if (pkg === 'bpmnlint') {
294 return {
295 pkg: 'bpmnlint',
296 ruleName
297 };
298 } else {
299 return {
300 pkg: 'bpmnlint-plugin-' + pkg,
301 ruleName
302 };
303 }
304};
305
306
307Linter.prototype.parseConfigName = function(name) {
308
309 const localMatch = /^bpmnlint:(.*)$/.exec(name);
310
311 if (localMatch) {
312 return {
313 pkg: 'bpmnlint',
314 configName: localMatch[1]
315 };
316 }
317
318 const pluginMatch = /^plugin:([^/]+)\/(.+)$/.exec(name);
319
320 if (!pluginMatch) {
321 throw new Error(`invalid config name <${ name }>`);
322 }
323
324 return {
325 pkg: 'bpmnlint-plugin-' + pluginMatch[1],
326 configName: pluginMatch[2]
327 };
328};
329
330
331// helpers ///////////////////////////
332
333/**
334 * Validate and return validated config.
335 *
336 * @param {Object} config
337 * @param {String} pkg
338 *
339 * @return {Object} validated config
340 */
341function normalizeConfig(config, pkg) {
342
343 const rules = config.rules || {};
344
345 const validatedRules = Object.keys(rules).reduce((normalizedRules, name) => {
346
347 const value = rules[name];
348
349 // prefix local rule definition
350 if (name.indexOf('bpmnlint/') === 0) {
351 name = name.substring('bpmnlint/'.length);
352 }
353
354 normalizedRules[name] = value;
355
356 return normalizedRules;
357 }, {});
358
359 return {
360 ...config,
361 rules: validatedRules
362 };
363}
\No newline at end of file