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