]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview `ConfigArray` class. | |
3 | * | |
4 | * `ConfigArray` class expresses the full of a configuration. It has the entry | |
5 | * config file, base config files that were extended, loaded parsers, and loaded | |
6 | * plugins. | |
7 | * | |
8 | * `ConfigArray` class provides three properties and two methods. | |
9 | * | |
10 | * - `pluginEnvironments` | |
11 | * - `pluginProcessors` | |
12 | * - `pluginRules` | |
13 | * The `Map` objects that contain the members of all plugins that this | |
14 | * config array contains. Those map objects don't have mutation methods. | |
15 | * Those keys are the member ID such as `pluginId/memberName`. | |
16 | * - `isRoot()` | |
17 | * If `true` then this configuration has `root:true` property. | |
18 | * - `extractConfig(filePath)` | |
19 | * Extract the final configuration for a given file. This means merging | |
20 | * every config array element which that `criteria` property matched. The | |
21 | * `filePath` argument must be an absolute path. | |
22 | * | |
23 | * `ConfigArrayFactory` provides the loading logic of config files. | |
24 | * | |
25 | * @author Toru Nagashima <https://github.com/mysticatea> | |
26 | */ | |
27 | "use strict"; | |
28 | ||
29 | //------------------------------------------------------------------------------ | |
30 | // Requirements | |
31 | //------------------------------------------------------------------------------ | |
32 | ||
33 | const { ExtractedConfig } = require("./extracted-config"); | |
34 | const { IgnorePattern } = require("./ignore-pattern"); | |
35 | ||
36 | //------------------------------------------------------------------------------ | |
37 | // Helpers | |
38 | //------------------------------------------------------------------------------ | |
39 | ||
40 | // Define types for VSCode IntelliSense. | |
41 | /** @typedef {import("../../shared/types").Environment} Environment */ | |
42 | /** @typedef {import("../../shared/types").GlobalConf} GlobalConf */ | |
43 | /** @typedef {import("../../shared/types").RuleConf} RuleConf */ | |
44 | /** @typedef {import("../../shared/types").Rule} Rule */ | |
45 | /** @typedef {import("../../shared/types").Plugin} Plugin */ | |
46 | /** @typedef {import("../../shared/types").Processor} Processor */ | |
47 | /** @typedef {import("./config-dependency").DependentParser} DependentParser */ | |
48 | /** @typedef {import("./config-dependency").DependentPlugin} DependentPlugin */ | |
49 | /** @typedef {import("./override-tester")["OverrideTester"]} OverrideTester */ | |
50 | ||
51 | /** | |
52 | * @typedef {Object} ConfigArrayElement | |
53 | * @property {string} name The name of this config element. | |
54 | * @property {string} filePath The path to the source file of this config element. | |
55 | * @property {InstanceType<OverrideTester>|null} criteria The tester for the `files` and `excludedFiles` of this config element. | |
56 | * @property {Record<string, boolean>|undefined} env The environment settings. | |
57 | * @property {Record<string, GlobalConf>|undefined} globals The global variable settings. | |
58 | * @property {IgnorePattern|undefined} ignorePattern The ignore patterns. | |
59 | * @property {boolean|undefined} noInlineConfig The flag that disables directive comments. | |
60 | * @property {DependentParser|undefined} parser The parser loader. | |
61 | * @property {Object|undefined} parserOptions The parser options. | |
62 | * @property {Record<string, DependentPlugin>|undefined} plugins The plugin loaders. | |
63 | * @property {string|undefined} processor The processor name to refer plugin's processor. | |
64 | * @property {boolean|undefined} reportUnusedDisableDirectives The flag to report unused `eslint-disable` comments. | |
65 | * @property {boolean|undefined} root The flag to express root. | |
66 | * @property {Record<string, RuleConf>|undefined} rules The rule settings | |
67 | * @property {Object|undefined} settings The shared settings. | |
68 | * @property {"config" | "ignore" | "implicit-processor"} type The element type. | |
69 | */ | |
70 | ||
71 | /** | |
72 | * @typedef {Object} ConfigArrayInternalSlots | |
73 | * @property {Map<string, ExtractedConfig>} cache The cache to extract configs. | |
74 | * @property {ReadonlyMap<string, Environment>|null} envMap The map from environment ID to environment definition. | |
75 | * @property {ReadonlyMap<string, Processor>|null} processorMap The map from processor ID to environment definition. | |
76 | * @property {ReadonlyMap<string, Rule>|null} ruleMap The map from rule ID to rule definition. | |
77 | */ | |
78 | ||
79 | /** @type {WeakMap<ConfigArray, ConfigArrayInternalSlots>} */ | |
80 | const internalSlotsMap = new class extends WeakMap { | |
81 | get(key) { | |
82 | let value = super.get(key); | |
83 | ||
84 | if (!value) { | |
85 | value = { | |
86 | cache: new Map(), | |
87 | envMap: null, | |
88 | processorMap: null, | |
89 | ruleMap: null | |
90 | }; | |
91 | super.set(key, value); | |
92 | } | |
93 | ||
94 | return value; | |
95 | } | |
96 | }(); | |
97 | ||
98 | /** | |
99 | * Get the indices which are matched to a given file. | |
100 | * @param {ConfigArrayElement[]} elements The elements. | |
101 | * @param {string} filePath The path to a target file. | |
102 | * @returns {number[]} The indices. | |
103 | */ | |
104 | function getMatchedIndices(elements, filePath) { | |
105 | const indices = []; | |
106 | ||
107 | for (let i = elements.length - 1; i >= 0; --i) { | |
108 | const element = elements[i]; | |
109 | ||
56c4a2cb | 110 | if (!element.criteria || (filePath && element.criteria.test(filePath))) { |
eb39fafa DC |
111 | indices.push(i); |
112 | } | |
113 | } | |
114 | ||
115 | return indices; | |
116 | } | |
117 | ||
118 | /** | |
119 | * Check if a value is a non-null object. | |
120 | * @param {any} x The value to check. | |
121 | * @returns {boolean} `true` if the value is a non-null object. | |
122 | */ | |
123 | function isNonNullObject(x) { | |
124 | return typeof x === "object" && x !== null; | |
125 | } | |
126 | ||
127 | /** | |
128 | * Merge two objects. | |
129 | * | |
130 | * Assign every property values of `y` to `x` if `x` doesn't have the property. | |
131 | * If `x`'s property value is an object, it does recursive. | |
132 | * @param {Object} target The destination to merge | |
133 | * @param {Object|undefined} source The source to merge. | |
134 | * @returns {void} | |
135 | */ | |
136 | function mergeWithoutOverwrite(target, source) { | |
137 | if (!isNonNullObject(source)) { | |
138 | return; | |
139 | } | |
140 | ||
141 | for (const key of Object.keys(source)) { | |
142 | if (key === "__proto__") { | |
143 | continue; | |
144 | } | |
145 | ||
146 | if (isNonNullObject(target[key])) { | |
147 | mergeWithoutOverwrite(target[key], source[key]); | |
148 | } else if (target[key] === void 0) { | |
149 | if (isNonNullObject(source[key])) { | |
150 | target[key] = Array.isArray(source[key]) ? [] : {}; | |
151 | mergeWithoutOverwrite(target[key], source[key]); | |
152 | } else if (source[key] !== void 0) { | |
153 | target[key] = source[key]; | |
154 | } | |
155 | } | |
156 | } | |
157 | } | |
158 | ||
159 | /** | |
160 | * The error for plugin conflicts. | |
161 | */ | |
162 | class PluginConflictError extends Error { | |
163 | ||
164 | /** | |
165 | * Initialize this error object. | |
166 | * @param {string} pluginId The plugin ID. | |
167 | * @param {{filePath:string, importerName:string}[]} plugins The resolved plugins. | |
168 | */ | |
169 | constructor(pluginId, plugins) { | |
170 | super(`Plugin "${pluginId}" was conflicted between ${plugins.map(p => `"${p.importerName}"`).join(" and ")}.`); | |
171 | this.messageTemplate = "plugin-conflict"; | |
172 | this.messageData = { pluginId, plugins }; | |
173 | } | |
174 | } | |
175 | ||
176 | /** | |
177 | * Merge plugins. | |
178 | * `target`'s definition is prior to `source`'s. | |
179 | * @param {Record<string, DependentPlugin>} target The destination to merge | |
180 | * @param {Record<string, DependentPlugin>|undefined} source The source to merge. | |
181 | * @returns {void} | |
182 | */ | |
183 | function mergePlugins(target, source) { | |
184 | if (!isNonNullObject(source)) { | |
185 | return; | |
186 | } | |
187 | ||
188 | for (const key of Object.keys(source)) { | |
189 | if (key === "__proto__") { | |
190 | continue; | |
191 | } | |
192 | const targetValue = target[key]; | |
193 | const sourceValue = source[key]; | |
194 | ||
195 | // Adopt the plugin which was found at first. | |
196 | if (targetValue === void 0) { | |
197 | if (sourceValue.error) { | |
198 | throw sourceValue.error; | |
199 | } | |
200 | target[key] = sourceValue; | |
201 | } else if (sourceValue.filePath !== targetValue.filePath) { | |
202 | throw new PluginConflictError(key, [ | |
203 | { | |
204 | filePath: targetValue.filePath, | |
205 | importerName: targetValue.importerName | |
206 | }, | |
207 | { | |
208 | filePath: sourceValue.filePath, | |
209 | importerName: sourceValue.importerName | |
210 | } | |
211 | ]); | |
212 | } | |
213 | } | |
214 | } | |
215 | ||
216 | /** | |
217 | * Merge rule configs. | |
218 | * `target`'s definition is prior to `source`'s. | |
219 | * @param {Record<string, Array>} target The destination to merge | |
220 | * @param {Record<string, RuleConf>|undefined} source The source to merge. | |
221 | * @returns {void} | |
222 | */ | |
223 | function mergeRuleConfigs(target, source) { | |
224 | if (!isNonNullObject(source)) { | |
225 | return; | |
226 | } | |
227 | ||
228 | for (const key of Object.keys(source)) { | |
229 | if (key === "__proto__") { | |
230 | continue; | |
231 | } | |
232 | const targetDef = target[key]; | |
233 | const sourceDef = source[key]; | |
234 | ||
235 | // Adopt the rule config which was found at first. | |
236 | if (targetDef === void 0) { | |
237 | if (Array.isArray(sourceDef)) { | |
238 | target[key] = [...sourceDef]; | |
239 | } else { | |
240 | target[key] = [sourceDef]; | |
241 | } | |
242 | ||
243 | /* | |
244 | * If the first found rule config is severity only and the current rule | |
245 | * config has options, merge the severity and the options. | |
246 | */ | |
247 | } else if ( | |
248 | targetDef.length === 1 && | |
249 | Array.isArray(sourceDef) && | |
250 | sourceDef.length >= 2 | |
251 | ) { | |
252 | targetDef.push(...sourceDef.slice(1)); | |
253 | } | |
254 | } | |
255 | } | |
256 | ||
257 | /** | |
258 | * Create the extracted config. | |
259 | * @param {ConfigArray} instance The config elements. | |
260 | * @param {number[]} indices The indices to use. | |
261 | * @returns {ExtractedConfig} The extracted config. | |
262 | */ | |
263 | function createConfig(instance, indices) { | |
264 | const config = new ExtractedConfig(); | |
265 | const ignorePatterns = []; | |
266 | ||
267 | // Merge elements. | |
268 | for (const index of indices) { | |
269 | const element = instance[index]; | |
270 | ||
271 | // Adopt the parser which was found at first. | |
272 | if (!config.parser && element.parser) { | |
273 | if (element.parser.error) { | |
274 | throw element.parser.error; | |
275 | } | |
276 | config.parser = element.parser; | |
277 | } | |
278 | ||
279 | // Adopt the processor which was found at first. | |
280 | if (!config.processor && element.processor) { | |
281 | config.processor = element.processor; | |
282 | } | |
283 | ||
284 | // Adopt the noInlineConfig which was found at first. | |
285 | if (config.noInlineConfig === void 0 && element.noInlineConfig !== void 0) { | |
286 | config.noInlineConfig = element.noInlineConfig; | |
287 | config.configNameOfNoInlineConfig = element.name; | |
288 | } | |
289 | ||
290 | // Adopt the reportUnusedDisableDirectives which was found at first. | |
291 | if (config.reportUnusedDisableDirectives === void 0 && element.reportUnusedDisableDirectives !== void 0) { | |
292 | config.reportUnusedDisableDirectives = element.reportUnusedDisableDirectives; | |
293 | } | |
294 | ||
295 | // Collect ignorePatterns | |
296 | if (element.ignorePattern) { | |
297 | ignorePatterns.push(element.ignorePattern); | |
298 | } | |
299 | ||
300 | // Merge others. | |
301 | mergeWithoutOverwrite(config.env, element.env); | |
302 | mergeWithoutOverwrite(config.globals, element.globals); | |
303 | mergeWithoutOverwrite(config.parserOptions, element.parserOptions); | |
304 | mergeWithoutOverwrite(config.settings, element.settings); | |
305 | mergePlugins(config.plugins, element.plugins); | |
306 | mergeRuleConfigs(config.rules, element.rules); | |
307 | } | |
308 | ||
309 | // Create the predicate function for ignore patterns. | |
310 | if (ignorePatterns.length > 0) { | |
311 | config.ignores = IgnorePattern.createIgnore(ignorePatterns.reverse()); | |
312 | } | |
313 | ||
314 | return config; | |
315 | } | |
316 | ||
317 | /** | |
318 | * Collect definitions. | |
319 | * @template T, U | |
320 | * @param {string} pluginId The plugin ID for prefix. | |
321 | * @param {Record<string,T>} defs The definitions to collect. | |
322 | * @param {Map<string, U>} map The map to output. | |
323 | * @param {function(T): U} [normalize] The normalize function for each value. | |
324 | * @returns {void} | |
325 | */ | |
326 | function collect(pluginId, defs, map, normalize) { | |
327 | if (defs) { | |
328 | const prefix = pluginId && `${pluginId}/`; | |
329 | ||
330 | for (const [key, value] of Object.entries(defs)) { | |
331 | map.set( | |
332 | `${prefix}${key}`, | |
333 | normalize ? normalize(value) : value | |
334 | ); | |
335 | } | |
336 | } | |
337 | } | |
338 | ||
339 | /** | |
340 | * Normalize a rule definition. | |
341 | * @param {Function|Rule} rule The rule definition to normalize. | |
342 | * @returns {Rule} The normalized rule definition. | |
343 | */ | |
344 | function normalizePluginRule(rule) { | |
345 | return typeof rule === "function" ? { create: rule } : rule; | |
346 | } | |
347 | ||
348 | /** | |
349 | * Delete the mutation methods from a given map. | |
350 | * @param {Map<any, any>} map The map object to delete. | |
351 | * @returns {void} | |
352 | */ | |
353 | function deleteMutationMethods(map) { | |
354 | Object.defineProperties(map, { | |
355 | clear: { configurable: true, value: void 0 }, | |
356 | delete: { configurable: true, value: void 0 }, | |
357 | set: { configurable: true, value: void 0 } | |
358 | }); | |
359 | } | |
360 | ||
361 | /** | |
362 | * Create `envMap`, `processorMap`, `ruleMap` with the plugins in the config array. | |
363 | * @param {ConfigArrayElement[]} elements The config elements. | |
364 | * @param {ConfigArrayInternalSlots} slots The internal slots. | |
365 | * @returns {void} | |
366 | */ | |
367 | function initPluginMemberMaps(elements, slots) { | |
368 | const processed = new Set(); | |
369 | ||
370 | slots.envMap = new Map(); | |
371 | slots.processorMap = new Map(); | |
372 | slots.ruleMap = new Map(); | |
373 | ||
374 | for (const element of elements) { | |
375 | if (!element.plugins) { | |
376 | continue; | |
377 | } | |
378 | ||
379 | for (const [pluginId, value] of Object.entries(element.plugins)) { | |
380 | const plugin = value.definition; | |
381 | ||
382 | if (!plugin || processed.has(pluginId)) { | |
383 | continue; | |
384 | } | |
385 | processed.add(pluginId); | |
386 | ||
387 | collect(pluginId, plugin.environments, slots.envMap); | |
388 | collect(pluginId, plugin.processors, slots.processorMap); | |
389 | collect(pluginId, plugin.rules, slots.ruleMap, normalizePluginRule); | |
390 | } | |
391 | } | |
392 | ||
393 | deleteMutationMethods(slots.envMap); | |
394 | deleteMutationMethods(slots.processorMap); | |
395 | deleteMutationMethods(slots.ruleMap); | |
396 | } | |
397 | ||
398 | /** | |
399 | * Create `envMap`, `processorMap`, `ruleMap` with the plugins in the config array. | |
400 | * @param {ConfigArray} instance The config elements. | |
401 | * @returns {ConfigArrayInternalSlots} The extracted config. | |
402 | */ | |
403 | function ensurePluginMemberMaps(instance) { | |
404 | const slots = internalSlotsMap.get(instance); | |
405 | ||
406 | if (!slots.ruleMap) { | |
407 | initPluginMemberMaps(instance, slots); | |
408 | } | |
409 | ||
410 | return slots; | |
411 | } | |
412 | ||
413 | //------------------------------------------------------------------------------ | |
414 | // Public Interface | |
415 | //------------------------------------------------------------------------------ | |
416 | ||
417 | /** | |
418 | * The Config Array. | |
419 | * | |
420 | * `ConfigArray` instance contains all settings, parsers, and plugins. | |
421 | * You need to call `ConfigArray#extractConfig(filePath)` method in order to | |
422 | * extract, merge and get only the config data which is related to an arbitrary | |
423 | * file. | |
424 | * @extends {Array<ConfigArrayElement>} | |
425 | */ | |
426 | class ConfigArray extends Array { | |
427 | ||
428 | /** | |
429 | * Get the plugin environments. | |
430 | * The returned map cannot be mutated. | |
431 | * @type {ReadonlyMap<string, Environment>} The plugin environments. | |
432 | */ | |
433 | get pluginEnvironments() { | |
434 | return ensurePluginMemberMaps(this).envMap; | |
435 | } | |
436 | ||
437 | /** | |
438 | * Get the plugin processors. | |
439 | * The returned map cannot be mutated. | |
440 | * @type {ReadonlyMap<string, Processor>} The plugin processors. | |
441 | */ | |
442 | get pluginProcessors() { | |
443 | return ensurePluginMemberMaps(this).processorMap; | |
444 | } | |
445 | ||
446 | /** | |
447 | * Get the plugin rules. | |
448 | * The returned map cannot be mutated. | |
449 | * @returns {ReadonlyMap<string, Rule>} The plugin rules. | |
450 | */ | |
451 | get pluginRules() { | |
452 | return ensurePluginMemberMaps(this).ruleMap; | |
453 | } | |
454 | ||
455 | /** | |
456 | * Check if this config has `root` flag. | |
457 | * @returns {boolean} `true` if this config array is root. | |
458 | */ | |
459 | isRoot() { | |
460 | for (let i = this.length - 1; i >= 0; --i) { | |
461 | const root = this[i].root; | |
462 | ||
463 | if (typeof root === "boolean") { | |
464 | return root; | |
465 | } | |
466 | } | |
467 | return false; | |
468 | } | |
469 | ||
470 | /** | |
471 | * Extract the config data which is related to a given file. | |
472 | * @param {string} filePath The absolute path to the target file. | |
473 | * @returns {ExtractedConfig} The extracted config data. | |
474 | */ | |
475 | extractConfig(filePath) { | |
476 | const { cache } = internalSlotsMap.get(this); | |
477 | const indices = getMatchedIndices(this, filePath); | |
478 | const cacheKey = indices.join(","); | |
479 | ||
480 | if (!cache.has(cacheKey)) { | |
481 | cache.set(cacheKey, createConfig(this, indices)); | |
482 | } | |
483 | ||
484 | return cache.get(cacheKey); | |
485 | } | |
486 | ||
487 | /** | |
488 | * Check if a given path is an additional lint target. | |
489 | * @param {string} filePath The absolute path to the target file. | |
490 | * @returns {boolean} `true` if the file is an additional lint target. | |
491 | */ | |
492 | isAdditionalTargetPath(filePath) { | |
493 | for (const { criteria, type } of this) { | |
494 | if ( | |
495 | type === "config" && | |
496 | criteria && | |
497 | !criteria.endsWithWildcard && | |
498 | criteria.test(filePath) | |
499 | ) { | |
500 | return true; | |
501 | } | |
502 | } | |
503 | return false; | |
504 | } | |
505 | } | |
506 | ||
507 | const exportObject = { | |
508 | ConfigArray, | |
509 | ||
510 | /** | |
511 | * Get the used extracted configs. | |
512 | * CLIEngine will use this method to collect used deprecated rules. | |
513 | * @param {ConfigArray} instance The config array object to get. | |
514 | * @returns {ExtractedConfig[]} The used extracted configs. | |
515 | * @private | |
516 | */ | |
517 | getUsedExtractedConfigs(instance) { | |
518 | const { cache } = internalSlotsMap.get(instance); | |
519 | ||
520 | return Array.from(cache.values()); | |
521 | } | |
522 | }; | |
523 | ||
524 | module.exports = exportObject; |