1 | /**
|
---|
2 | * @fileoverview Flat Config Array
|
---|
3 | * @author Nicholas C. Zakas
|
---|
4 | */
|
---|
5 |
|
---|
6 | "use strict";
|
---|
7 |
|
---|
8 | //-----------------------------------------------------------------------------
|
---|
9 | // Requirements
|
---|
10 | //-----------------------------------------------------------------------------
|
---|
11 |
|
---|
12 | const { ConfigArray, ConfigArraySymbol } = require("@humanwhocodes/config-array");
|
---|
13 | const { flatConfigSchema } = require("./flat-config-schema");
|
---|
14 | const { RuleValidator } = require("./rule-validator");
|
---|
15 | const { defaultConfig } = require("./default-config");
|
---|
16 | const jsPlugin = require("@eslint/js");
|
---|
17 |
|
---|
18 | //-----------------------------------------------------------------------------
|
---|
19 | // Helpers
|
---|
20 | //-----------------------------------------------------------------------------
|
---|
21 |
|
---|
22 | /**
|
---|
23 | * Fields that are considered metadata and not part of the config object.
|
---|
24 | */
|
---|
25 | const META_FIELDS = new Set(["name"]);
|
---|
26 |
|
---|
27 | const ruleValidator = new RuleValidator();
|
---|
28 |
|
---|
29 | /**
|
---|
30 | * Splits a plugin identifier in the form a/b/c into two parts: a/b and c.
|
---|
31 | * @param {string} identifier The identifier to parse.
|
---|
32 | * @returns {{objectName: string, pluginName: string}} The parts of the plugin
|
---|
33 | * name.
|
---|
34 | */
|
---|
35 | function splitPluginIdentifier(identifier) {
|
---|
36 | const parts = identifier.split("/");
|
---|
37 |
|
---|
38 | return {
|
---|
39 | objectName: parts.pop(),
|
---|
40 | pluginName: parts.join("/")
|
---|
41 | };
|
---|
42 | }
|
---|
43 |
|
---|
44 | /**
|
---|
45 | * Returns the name of an object in the config by reading its `meta` key.
|
---|
46 | * @param {Object} object The object to check.
|
---|
47 | * @returns {string?} The name of the object if found or `null` if there
|
---|
48 | * is no name.
|
---|
49 | */
|
---|
50 | function getObjectId(object) {
|
---|
51 |
|
---|
52 | // first check old-style name
|
---|
53 | let name = object.name;
|
---|
54 |
|
---|
55 | if (!name) {
|
---|
56 |
|
---|
57 | if (!object.meta) {
|
---|
58 | return null;
|
---|
59 | }
|
---|
60 |
|
---|
61 | name = object.meta.name;
|
---|
62 |
|
---|
63 | if (!name) {
|
---|
64 | return null;
|
---|
65 | }
|
---|
66 | }
|
---|
67 |
|
---|
68 | // now check for old-style version
|
---|
69 | let version = object.version;
|
---|
70 |
|
---|
71 | if (!version) {
|
---|
72 | version = object.meta && object.meta.version;
|
---|
73 | }
|
---|
74 |
|
---|
75 | // if there's a version then append that
|
---|
76 | if (version) {
|
---|
77 | return `${name}@${version}`;
|
---|
78 | }
|
---|
79 |
|
---|
80 | return name;
|
---|
81 | }
|
---|
82 |
|
---|
83 | /**
|
---|
84 | * Wraps a config error with details about where the error occurred.
|
---|
85 | * @param {Error} error The original error.
|
---|
86 | * @param {number} originalLength The original length of the config array.
|
---|
87 | * @param {number} baseLength The length of the base config.
|
---|
88 | * @returns {TypeError} The new error with details.
|
---|
89 | */
|
---|
90 | function wrapConfigErrorWithDetails(error, originalLength, baseLength) {
|
---|
91 |
|
---|
92 | let location = "user-defined";
|
---|
93 | let configIndex = error.index;
|
---|
94 |
|
---|
95 | /*
|
---|
96 | * A config array is set up in this order:
|
---|
97 | * 1. Base config
|
---|
98 | * 2. Original configs
|
---|
99 | * 3. User-defined configs
|
---|
100 | * 4. CLI-defined configs
|
---|
101 | *
|
---|
102 | * So we need to adjust the index to account for the base config.
|
---|
103 | *
|
---|
104 | * - If the index is less than the base length, it's in the base config
|
---|
105 | * (as specified by `baseConfig` argument to `FlatConfigArray` constructor).
|
---|
106 | * - If the index is greater than the base length but less than the original
|
---|
107 | * length + base length, it's in the original config. The original config
|
---|
108 | * is passed to the `FlatConfigArray` constructor as the first argument.
|
---|
109 | * - Otherwise, it's in the user-defined config, which is loaded from the
|
---|
110 | * config file and merged with any command-line options.
|
---|
111 | */
|
---|
112 | if (error.index < baseLength) {
|
---|
113 | location = "base";
|
---|
114 | } else if (error.index < originalLength + baseLength) {
|
---|
115 | location = "original";
|
---|
116 | configIndex = error.index - baseLength;
|
---|
117 | } else {
|
---|
118 | configIndex = error.index - originalLength - baseLength;
|
---|
119 | }
|
---|
120 |
|
---|
121 | return new TypeError(
|
---|
122 | `${error.message.slice(0, -1)} at ${location} index ${configIndex}.`,
|
---|
123 | { cause: error }
|
---|
124 | );
|
---|
125 | }
|
---|
126 |
|
---|
127 | const originalBaseConfig = Symbol("originalBaseConfig");
|
---|
128 | const originalLength = Symbol("originalLength");
|
---|
129 | const baseLength = Symbol("baseLength");
|
---|
130 |
|
---|
131 | //-----------------------------------------------------------------------------
|
---|
132 | // Exports
|
---|
133 | //-----------------------------------------------------------------------------
|
---|
134 |
|
---|
135 | /**
|
---|
136 | * Represents an array containing configuration information for ESLint.
|
---|
137 | */
|
---|
138 | class FlatConfigArray extends ConfigArray {
|
---|
139 |
|
---|
140 | /**
|
---|
141 | * Creates a new instance.
|
---|
142 | * @param {*[]} configs An array of configuration information.
|
---|
143 | * @param {{basePath: string, shouldIgnore: boolean, baseConfig: FlatConfig}} options The options
|
---|
144 | * to use for the config array instance.
|
---|
145 | */
|
---|
146 | constructor(configs, {
|
---|
147 | basePath,
|
---|
148 | shouldIgnore = true,
|
---|
149 | baseConfig = defaultConfig
|
---|
150 | } = {}) {
|
---|
151 | super(configs, {
|
---|
152 | basePath,
|
---|
153 | schema: flatConfigSchema
|
---|
154 | });
|
---|
155 |
|
---|
156 | /**
|
---|
157 | * The original length of the array before any modifications.
|
---|
158 | * @type {number}
|
---|
159 | */
|
---|
160 | this[originalLength] = this.length;
|
---|
161 |
|
---|
162 | if (baseConfig[Symbol.iterator]) {
|
---|
163 | this.unshift(...baseConfig);
|
---|
164 | } else {
|
---|
165 | this.unshift(baseConfig);
|
---|
166 | }
|
---|
167 |
|
---|
168 | /**
|
---|
169 | * The length of the array after applying the base config.
|
---|
170 | * @type {number}
|
---|
171 | */
|
---|
172 | this[baseLength] = this.length - this[originalLength];
|
---|
173 |
|
---|
174 | /**
|
---|
175 | * The base config used to build the config array.
|
---|
176 | * @type {Array<FlatConfig>}
|
---|
177 | */
|
---|
178 | this[originalBaseConfig] = baseConfig;
|
---|
179 | Object.defineProperty(this, originalBaseConfig, { writable: false });
|
---|
180 |
|
---|
181 | /**
|
---|
182 | * Determines if `ignores` fields should be honored.
|
---|
183 | * If true, then all `ignores` fields are honored.
|
---|
184 | * if false, then only `ignores` fields in the baseConfig are honored.
|
---|
185 | * @type {boolean}
|
---|
186 | */
|
---|
187 | this.shouldIgnore = shouldIgnore;
|
---|
188 | Object.defineProperty(this, "shouldIgnore", { writable: false });
|
---|
189 | }
|
---|
190 |
|
---|
191 | /**
|
---|
192 | * Normalizes the array by calling the superclass method and catching/rethrowing
|
---|
193 | * any ConfigError exceptions with additional details.
|
---|
194 | * @param {any} [context] The context to use to normalize the array.
|
---|
195 | * @returns {Promise<FlatConfigArray>} A promise that resolves when the array is normalized.
|
---|
196 | */
|
---|
197 | normalize(context) {
|
---|
198 | return super.normalize(context)
|
---|
199 | .catch(error => {
|
---|
200 | if (error.name === "ConfigError") {
|
---|
201 | throw wrapConfigErrorWithDetails(error, this[originalLength], this[baseLength]);
|
---|
202 | }
|
---|
203 |
|
---|
204 | throw error;
|
---|
205 |
|
---|
206 | });
|
---|
207 | }
|
---|
208 |
|
---|
209 | /**
|
---|
210 | * Normalizes the array by calling the superclass method and catching/rethrowing
|
---|
211 | * any ConfigError exceptions with additional details.
|
---|
212 | * @param {any} [context] The context to use to normalize the array.
|
---|
213 | * @returns {FlatConfigArray} The current instance.
|
---|
214 | * @throws {TypeError} If the config is invalid.
|
---|
215 | */
|
---|
216 | normalizeSync(context) {
|
---|
217 |
|
---|
218 | try {
|
---|
219 |
|
---|
220 | return super.normalizeSync(context);
|
---|
221 |
|
---|
222 | } catch (error) {
|
---|
223 |
|
---|
224 | if (error.name === "ConfigError") {
|
---|
225 | throw wrapConfigErrorWithDetails(error, this[originalLength], this[baseLength]);
|
---|
226 | }
|
---|
227 |
|
---|
228 | throw error;
|
---|
229 |
|
---|
230 | }
|
---|
231 |
|
---|
232 | }
|
---|
233 |
|
---|
234 | /* eslint-disable class-methods-use-this -- Desired as instance method */
|
---|
235 | /**
|
---|
236 | * Replaces a config with another config to allow us to put strings
|
---|
237 | * in the config array that will be replaced by objects before
|
---|
238 | * normalization.
|
---|
239 | * @param {Object} config The config to preprocess.
|
---|
240 | * @returns {Object} The preprocessed config.
|
---|
241 | */
|
---|
242 | [ConfigArraySymbol.preprocessConfig](config) {
|
---|
243 | if (config === "eslint:recommended") {
|
---|
244 |
|
---|
245 | // if we are in a Node.js environment warn the user
|
---|
246 | if (typeof process !== "undefined" && process.emitWarning) {
|
---|
247 | process.emitWarning("The 'eslint:recommended' string configuration is deprecated and will be replaced by the @eslint/js package's 'recommended' config.");
|
---|
248 | }
|
---|
249 |
|
---|
250 | return jsPlugin.configs.recommended;
|
---|
251 | }
|
---|
252 |
|
---|
253 | if (config === "eslint:all") {
|
---|
254 |
|
---|
255 | // if we are in a Node.js environment warn the user
|
---|
256 | if (typeof process !== "undefined" && process.emitWarning) {
|
---|
257 | process.emitWarning("The 'eslint:all' string configuration is deprecated and will be replaced by the @eslint/js package's 'all' config.");
|
---|
258 | }
|
---|
259 |
|
---|
260 | return jsPlugin.configs.all;
|
---|
261 | }
|
---|
262 |
|
---|
263 | /*
|
---|
264 | * If a config object has `ignores` and no other non-meta fields, then it's an object
|
---|
265 | * for global ignores. If `shouldIgnore` is false, that object shouldn't apply,
|
---|
266 | * so we'll remove its `ignores`.
|
---|
267 | */
|
---|
268 | if (
|
---|
269 | !this.shouldIgnore &&
|
---|
270 | !this[originalBaseConfig].includes(config) &&
|
---|
271 | config.ignores &&
|
---|
272 | Object.keys(config).filter(key => !META_FIELDS.has(key)).length === 1
|
---|
273 | ) {
|
---|
274 | /* eslint-disable-next-line no-unused-vars -- need to strip off other keys */
|
---|
275 | const { ignores, ...otherKeys } = config;
|
---|
276 |
|
---|
277 | return otherKeys;
|
---|
278 | }
|
---|
279 |
|
---|
280 | return config;
|
---|
281 | }
|
---|
282 |
|
---|
283 | /**
|
---|
284 | * Finalizes the config by replacing plugin references with their objects
|
---|
285 | * and validating rule option schemas.
|
---|
286 | * @param {Object} config The config to finalize.
|
---|
287 | * @returns {Object} The finalized config.
|
---|
288 | * @throws {TypeError} If the config is invalid.
|
---|
289 | */
|
---|
290 | [ConfigArraySymbol.finalizeConfig](config) {
|
---|
291 |
|
---|
292 | const { plugins, languageOptions, processor } = config;
|
---|
293 | let parserName, processorName;
|
---|
294 | let invalidParser = false,
|
---|
295 | invalidProcessor = false;
|
---|
296 |
|
---|
297 | // Check parser value
|
---|
298 | if (languageOptions && languageOptions.parser) {
|
---|
299 | const { parser } = languageOptions;
|
---|
300 |
|
---|
301 | if (typeof parser === "object") {
|
---|
302 | parserName = getObjectId(parser);
|
---|
303 |
|
---|
304 | if (!parserName) {
|
---|
305 | invalidParser = true;
|
---|
306 | }
|
---|
307 |
|
---|
308 | } else {
|
---|
309 | invalidParser = true;
|
---|
310 | }
|
---|
311 | }
|
---|
312 |
|
---|
313 | // Check processor value
|
---|
314 | if (processor) {
|
---|
315 | if (typeof processor === "string") {
|
---|
316 | const { pluginName, objectName: localProcessorName } = splitPluginIdentifier(processor);
|
---|
317 |
|
---|
318 | processorName = processor;
|
---|
319 |
|
---|
320 | if (!plugins || !plugins[pluginName] || !plugins[pluginName].processors || !plugins[pluginName].processors[localProcessorName]) {
|
---|
321 | throw new TypeError(`Key "processor": Could not find "${localProcessorName}" in plugin "${pluginName}".`);
|
---|
322 | }
|
---|
323 |
|
---|
324 | config.processor = plugins[pluginName].processors[localProcessorName];
|
---|
325 | } else if (typeof processor === "object") {
|
---|
326 | processorName = getObjectId(processor);
|
---|
327 |
|
---|
328 | if (!processorName) {
|
---|
329 | invalidProcessor = true;
|
---|
330 | }
|
---|
331 |
|
---|
332 | } else {
|
---|
333 | invalidProcessor = true;
|
---|
334 | }
|
---|
335 | }
|
---|
336 |
|
---|
337 | ruleValidator.validate(config);
|
---|
338 |
|
---|
339 | // apply special logic for serialization into JSON
|
---|
340 | /* eslint-disable object-shorthand -- shorthand would change "this" value */
|
---|
341 | Object.defineProperty(config, "toJSON", {
|
---|
342 | value: function() {
|
---|
343 |
|
---|
344 | if (invalidParser) {
|
---|
345 | throw new Error("Could not serialize parser object (missing 'meta' object).");
|
---|
346 | }
|
---|
347 |
|
---|
348 | if (invalidProcessor) {
|
---|
349 | throw new Error("Could not serialize processor object (missing 'meta' object).");
|
---|
350 | }
|
---|
351 |
|
---|
352 | return {
|
---|
353 | ...this,
|
---|
354 | plugins: Object.entries(plugins).map(([namespace, plugin]) => {
|
---|
355 |
|
---|
356 | const pluginId = getObjectId(plugin);
|
---|
357 |
|
---|
358 | if (!pluginId) {
|
---|
359 | return namespace;
|
---|
360 | }
|
---|
361 |
|
---|
362 | return `${namespace}:${pluginId}`;
|
---|
363 | }),
|
---|
364 | languageOptions: {
|
---|
365 | ...languageOptions,
|
---|
366 | parser: parserName
|
---|
367 | },
|
---|
368 | processor: processorName
|
---|
369 | };
|
---|
370 | }
|
---|
371 | });
|
---|
372 | /* eslint-enable object-shorthand -- ok to enable now */
|
---|
373 |
|
---|
374 | return config;
|
---|
375 | }
|
---|
376 | /* eslint-enable class-methods-use-this -- Desired as instance method */
|
---|
377 |
|
---|
378 | }
|
---|
379 |
|
---|
380 | exports.FlatConfigArray = FlatConfigArray;
|
---|