]> git.proxmox.com Git - pve-eslint.git/blame - eslint/lib/cli-engine/config-array/config-array.js
upgrade to v7.0.0
[pve-eslint.git] / eslint / lib / cli-engine / config-array / config-array.js
CommitLineData
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
33const { ExtractedConfig } = require("./extracted-config");
34const { 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>} */
80const 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 */
104function 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 */
123function 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 */
136function 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 */
162class 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 */
183function 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 */
223function 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 */
263function 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 */
326function 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 */
344function 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 */
353function 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 */
367function 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 */
403function 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 */
426class 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
507const 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
524module.exports = exportObject;