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