eslint 8.20.0 → 8.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,622 @@
1
+ /**
2
+ * @fileoverview Helper functions for ESLint class
3
+ * @author Nicholas C. Zakas
4
+ */
5
+
6
+ "use strict";
7
+
8
+ //-----------------------------------------------------------------------------
9
+ // Requirements
10
+ //-----------------------------------------------------------------------------
11
+
12
+ const path = require("path");
13
+ const fs = require("fs");
14
+ const fsp = fs.promises;
15
+ const isGlob = require("is-glob");
16
+ const globby = require("globby");
17
+ const hash = require("../cli-engine/hash");
18
+
19
+ //-----------------------------------------------------------------------------
20
+ // Errors
21
+ //-----------------------------------------------------------------------------
22
+
23
+ /**
24
+ * The error type when no files match a glob.
25
+ */
26
+ class NoFilesFoundError extends Error {
27
+
28
+ /**
29
+ * @param {string} pattern The glob pattern which was not found.
30
+ * @param {boolean} globEnabled If `false` then the pattern was a glob pattern, but glob was disabled.
31
+ */
32
+ constructor(pattern, globEnabled) {
33
+ super(`No files matching '${pattern}' were found${!globEnabled ? " (glob was disabled)" : ""}.`);
34
+ this.messageTemplate = "file-not-found";
35
+ this.messageData = { pattern, globDisabled: !globEnabled };
36
+ }
37
+ }
38
+
39
+ /**
40
+ * The error type when there are files matched by a glob, but all of them have been ignored.
41
+ */
42
+ class AllFilesIgnoredError extends Error {
43
+
44
+ /**
45
+ * @param {string} pattern The glob pattern which was not found.
46
+ */
47
+ constructor(pattern) {
48
+ super(`All files matched by '${pattern}' are ignored.`);
49
+ this.messageTemplate = "all-files-ignored";
50
+ this.messageData = { pattern };
51
+ }
52
+ }
53
+
54
+
55
+ //-----------------------------------------------------------------------------
56
+ // General Helpers
57
+ //-----------------------------------------------------------------------------
58
+
59
+ /**
60
+ * Check if a given value is a non-empty string or not.
61
+ * @param {any} x The value to check.
62
+ * @returns {boolean} `true` if `x` is a non-empty string.
63
+ */
64
+ function isNonEmptyString(x) {
65
+ return typeof x === "string" && x.trim() !== "";
66
+ }
67
+
68
+ /**
69
+ * Check if a given value is an array of non-empty stringss or not.
70
+ * @param {any} x The value to check.
71
+ * @returns {boolean} `true` if `x` is an array of non-empty stringss.
72
+ */
73
+ function isArrayOfNonEmptyString(x) {
74
+ return Array.isArray(x) && x.every(isNonEmptyString);
75
+ }
76
+
77
+ //-----------------------------------------------------------------------------
78
+ // File-related Helpers
79
+ //-----------------------------------------------------------------------------
80
+
81
+ /**
82
+ * Normalizes slashes in a file pattern to posix-style.
83
+ * @param {string} pattern The pattern to replace slashes in.
84
+ * @returns {string} The pattern with slashes normalized.
85
+ */
86
+ function normalizeToPosix(pattern) {
87
+ return pattern.replace(/\\/gu, "/");
88
+ }
89
+
90
+ /**
91
+ * Check if a string is a glob pattern or not.
92
+ * @param {string} pattern A glob pattern.
93
+ * @returns {boolean} `true` if the string is a glob pattern.
94
+ */
95
+ function isGlobPattern(pattern) {
96
+ return isGlob(path.sep === "\\" ? normalizeToPosix(pattern) : pattern);
97
+ }
98
+
99
+ /**
100
+ * Finds all files matching the options specified.
101
+ * @param {Object} args The arguments objects.
102
+ * @param {Array<string>} args.patterns An array of glob patterns.
103
+ * @param {boolean} args.globInputPaths true to interpret glob patterns,
104
+ * false to not interpret glob patterns.
105
+ * @param {string} args.cwd The current working directory to find from.
106
+ * @param {FlatConfigArray} args.configs The configs for the current run.
107
+ * @param {boolean} args.errorOnUnmatchedPattern Determines if an unmatched pattern
108
+ * should throw an error.
109
+ * @returns {Promise<Array<string>>} The fully resolved file paths.
110
+ * @throws {AllFilesIgnoredError} If there are no results due to an ignore pattern.
111
+ * @throws {NoFilesFoundError} If no files matched the given patterns.
112
+ */
113
+ async function findFiles({
114
+ patterns,
115
+ globInputPaths,
116
+ cwd,
117
+ configs,
118
+ errorOnUnmatchedPattern
119
+ }) {
120
+
121
+ const results = [];
122
+ const globbyPatterns = [];
123
+ const missingPatterns = [];
124
+
125
+ // check to see if we have explicit files and directories
126
+ const filePaths = patterns.map(filePath => path.resolve(cwd, filePath));
127
+ const stats = await Promise.all(
128
+ filePaths.map(
129
+ filePath => fsp.stat(filePath).catch(() => {})
130
+ )
131
+ );
132
+
133
+ stats.forEach((stat, index) => {
134
+
135
+ const filePath = filePaths[index];
136
+ const pattern = patterns[index];
137
+
138
+ if (stat) {
139
+
140
+ // files are added directly to the list
141
+ if (stat.isFile()) {
142
+ results.push({
143
+ filePath,
144
+ ignored: configs.isIgnored(filePath)
145
+ });
146
+ }
147
+
148
+ // directories need extensions attached
149
+ if (stat.isDirectory()) {
150
+
151
+ // filePatterns are all relative to cwd
152
+ const filePatterns = configs.files
153
+ .filter(filePattern => {
154
+
155
+ // can only do this for strings, not functions
156
+ if (typeof filePattern !== "string") {
157
+ return false;
158
+ }
159
+
160
+ // patterns ending with * are not used for file search
161
+ if (filePattern.endsWith("*")) {
162
+ return false;
163
+ }
164
+
165
+ // not sure how to handle negated patterns yet
166
+ if (filePattern.startsWith("!")) {
167
+ return false;
168
+ }
169
+
170
+ // check if the pattern would be inside the cwd or not
171
+ const fullFilePattern = path.join(cwd, filePattern);
172
+ const relativeFilePattern = path.relative(configs.basePath, fullFilePattern);
173
+
174
+ return !relativeFilePattern.startsWith("..");
175
+ })
176
+ .map(filePattern => {
177
+ if (filePattern.startsWith("**")) {
178
+ return path.join(pattern, filePattern);
179
+ }
180
+
181
+ // adjust the path to be relative to the cwd
182
+ return path.relative(
183
+ cwd,
184
+ path.join(configs.basePath, filePattern)
185
+ );
186
+ })
187
+ .map(normalizeToPosix);
188
+
189
+ if (filePatterns.length) {
190
+ globbyPatterns.push(...filePatterns);
191
+ }
192
+
193
+ }
194
+
195
+ return;
196
+ }
197
+
198
+ // save patterns for later use based on whether globs are enabled
199
+ if (globInputPaths && isGlobPattern(filePath)) {
200
+ globbyPatterns.push(pattern);
201
+ } else {
202
+ missingPatterns.push(pattern);
203
+ }
204
+ });
205
+
206
+ // note: globbyPatterns can be an empty array
207
+ const globbyResults = (await globby(globbyPatterns, {
208
+ cwd,
209
+ absolute: true,
210
+ ignore: configs.ignores.filter(matcher => typeof matcher === "string")
211
+ }));
212
+
213
+ // if there are no results, tell the user why
214
+ if (!results.length && !globbyResults.length) {
215
+
216
+ // try globby without ignoring anything
217
+ /* eslint-disable no-unreachable-loop -- We want to exit early. */
218
+ for (const globbyPattern of globbyPatterns) {
219
+
220
+ /* eslint-disable-next-line no-unused-vars -- Want to exit early. */
221
+ for await (const filePath of globby.stream(globbyPattern, { cwd, absolute: true })) {
222
+
223
+ // files were found but ignored
224
+ throw new AllFilesIgnoredError(globbyPattern);
225
+ }
226
+
227
+ // no files were found
228
+ if (errorOnUnmatchedPattern) {
229
+ throw new NoFilesFoundError(globbyPattern, globInputPaths);
230
+ }
231
+ }
232
+ /* eslint-enable no-unreachable-loop -- Go back to normal. */
233
+
234
+ }
235
+
236
+ // there were patterns that didn't match anything, tell the user
237
+ if (errorOnUnmatchedPattern && missingPatterns.length) {
238
+ throw new NoFilesFoundError(missingPatterns[0], globInputPaths);
239
+ }
240
+
241
+
242
+ return [
243
+ ...results,
244
+ ...globbyResults.map(filePath => ({
245
+ filePath: path.resolve(filePath),
246
+ ignored: false
247
+ }))
248
+ ];
249
+ }
250
+
251
+
252
+ /**
253
+ * Checks whether a file exists at the given location
254
+ * @param {string} resolvedPath A path from the CWD
255
+ * @throws {Error} As thrown by `fs.statSync` or `fs.isFile`.
256
+ * @returns {boolean} `true` if a file exists
257
+ */
258
+ function fileExists(resolvedPath) {
259
+ try {
260
+ return fs.statSync(resolvedPath).isFile();
261
+ } catch (error) {
262
+ if (error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
263
+ return false;
264
+ }
265
+ throw error;
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Checks whether a directory exists at the given location
271
+ * @param {string} resolvedPath A path from the CWD
272
+ * @throws {Error} As thrown by `fs.statSync` or `fs.isDirectory`.
273
+ * @returns {boolean} `true` if a directory exists
274
+ */
275
+ function directoryExists(resolvedPath) {
276
+ try {
277
+ return fs.statSync(resolvedPath).isDirectory();
278
+ } catch (error) {
279
+ if (error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
280
+ return false;
281
+ }
282
+ throw error;
283
+ }
284
+ }
285
+
286
+ //-----------------------------------------------------------------------------
287
+ // Results-related Helpers
288
+ //-----------------------------------------------------------------------------
289
+
290
+ /**
291
+ * Checks if the given message is an error message.
292
+ * @param {LintMessage} message The message to check.
293
+ * @returns {boolean} Whether or not the message is an error message.
294
+ * @private
295
+ */
296
+ function isErrorMessage(message) {
297
+ return message.severity === 2;
298
+ }
299
+
300
+ /**
301
+ * Returns result with warning by ignore settings
302
+ * @param {string} filePath File path of checked code
303
+ * @param {string} baseDir Absolute path of base directory
304
+ * @returns {LintResult} Result with single warning
305
+ * @private
306
+ */
307
+ function createIgnoreResult(filePath, baseDir) {
308
+ let message;
309
+ const isHidden = filePath.split(path.sep)
310
+ .find(segment => /^\./u.test(segment));
311
+ const isInNodeModules = baseDir && path.relative(baseDir, filePath).startsWith("node_modules");
312
+
313
+ if (isHidden) {
314
+ message = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!<relative/path/to/filename>'\") to override.";
315
+ } else if (isInNodeModules) {
316
+ message = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override.";
317
+ } else {
318
+ message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override.";
319
+ }
320
+
321
+ return {
322
+ filePath: path.resolve(filePath),
323
+ messages: [
324
+ {
325
+ fatal: false,
326
+ severity: 1,
327
+ message
328
+ }
329
+ ],
330
+ suppressedMessages: [],
331
+ errorCount: 0,
332
+ warningCount: 1,
333
+ fatalErrorCount: 0,
334
+ fixableErrorCount: 0,
335
+ fixableWarningCount: 0
336
+ };
337
+ }
338
+
339
+ //-----------------------------------------------------------------------------
340
+ // Options-related Helpers
341
+ //-----------------------------------------------------------------------------
342
+
343
+
344
+ /**
345
+ * Check if a given value is a valid fix type or not.
346
+ * @param {any} x The value to check.
347
+ * @returns {boolean} `true` if `x` is valid fix type.
348
+ */
349
+ function isFixType(x) {
350
+ return x === "directive" || x === "problem" || x === "suggestion" || x === "layout";
351
+ }
352
+
353
+ /**
354
+ * Check if a given value is an array of fix types or not.
355
+ * @param {any} x The value to check.
356
+ * @returns {boolean} `true` if `x` is an array of fix types.
357
+ */
358
+ function isFixTypeArray(x) {
359
+ return Array.isArray(x) && x.every(isFixType);
360
+ }
361
+
362
+ /**
363
+ * The error for invalid options.
364
+ */
365
+ class ESLintInvalidOptionsError extends Error {
366
+ constructor(messages) {
367
+ super(`Invalid Options:\n- ${messages.join("\n- ")}`);
368
+ this.code = "ESLINT_INVALID_OPTIONS";
369
+ Error.captureStackTrace(this, ESLintInvalidOptionsError);
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Validates and normalizes options for the wrapped CLIEngine instance.
375
+ * @param {FlatESLintOptions} options The options to process.
376
+ * @throws {ESLintInvalidOptionsError} If of any of a variety of type errors.
377
+ * @returns {FlatESLintOptions} The normalized options.
378
+ */
379
+ function processOptions({
380
+ allowInlineConfig = true, // ← we cannot use `overrideConfig.noInlineConfig` instead because `allowInlineConfig` has side-effect that suppress warnings that show inline configs are ignored.
381
+ baseConfig = null,
382
+ cache = false,
383
+ cacheLocation = ".eslintcache",
384
+ cacheStrategy = "metadata",
385
+ cwd = process.cwd(),
386
+ errorOnUnmatchedPattern = true,
387
+ fix = false,
388
+ fixTypes = null, // ← should be null by default because if it's an array then it suppresses rules that don't have the `meta.type` property.
389
+ globInputPaths = true,
390
+ ignore = true,
391
+ ignorePath = null, // ← should be null by default because if it's a string then it may throw ENOENT.
392
+ ignorePatterns = null,
393
+ overrideConfig = null,
394
+ overrideConfigFile = null,
395
+ plugins = {},
396
+ reportUnusedDisableDirectives = null, // ← should be null by default because if it's a string then it overrides the 'reportUnusedDisableDirectives' setting in config files. And we cannot use `overrideConfig.reportUnusedDisableDirectives` instead because we cannot configure the `error` severity with that.
397
+ ...unknownOptions
398
+ }) {
399
+ const errors = [];
400
+ const unknownOptionKeys = Object.keys(unknownOptions);
401
+
402
+ if (unknownOptionKeys.length >= 1) {
403
+ errors.push(`Unknown options: ${unknownOptionKeys.join(", ")}`);
404
+ if (unknownOptionKeys.includes("cacheFile")) {
405
+ errors.push("'cacheFile' has been removed. Please use the 'cacheLocation' option instead.");
406
+ }
407
+ if (unknownOptionKeys.includes("configFile")) {
408
+ errors.push("'configFile' has been removed. Please use the 'overrideConfigFile' option instead.");
409
+ }
410
+ if (unknownOptionKeys.includes("envs")) {
411
+ errors.push("'envs' has been removed.");
412
+ }
413
+ if (unknownOptionKeys.includes("extensions")) {
414
+ errors.push("'extensions' has been removed.");
415
+ }
416
+ if (unknownOptionKeys.includes("resolvePluginsRelativeTo")) {
417
+ errors.push("'resolvePluginsRelativeTo' has been removed.");
418
+ }
419
+ if (unknownOptionKeys.includes("globals")) {
420
+ errors.push("'globals' has been removed. Please use the 'overrideConfig.languageOptions.globals' option instead.");
421
+ }
422
+ if (unknownOptionKeys.includes("ignorePattern")) {
423
+ errors.push("'ignorePattern' has been removed. Please use the 'overrideConfig.ignorePatterns' option instead.");
424
+ }
425
+ if (unknownOptionKeys.includes("parser")) {
426
+ errors.push("'parser' has been removed. Please use the 'overrideConfig.languageOptions.parser' option instead.");
427
+ }
428
+ if (unknownOptionKeys.includes("parserOptions")) {
429
+ errors.push("'parserOptions' has been removed. Please use the 'overrideConfig.languageOptions.parserOptions' option instead.");
430
+ }
431
+ if (unknownOptionKeys.includes("rules")) {
432
+ errors.push("'rules' has been removed. Please use the 'overrideConfig.rules' option instead.");
433
+ }
434
+ if (unknownOptionKeys.includes("rulePaths")) {
435
+ errors.push("'rulePaths' has been removed. Please define your rules using plugins.");
436
+ }
437
+ }
438
+ if (typeof allowInlineConfig !== "boolean") {
439
+ errors.push("'allowInlineConfig' must be a boolean.");
440
+ }
441
+ if (typeof baseConfig !== "object") {
442
+ errors.push("'baseConfig' must be an object or null.");
443
+ }
444
+ if (typeof cache !== "boolean") {
445
+ errors.push("'cache' must be a boolean.");
446
+ }
447
+ if (!isNonEmptyString(cacheLocation)) {
448
+ errors.push("'cacheLocation' must be a non-empty string.");
449
+ }
450
+ if (
451
+ cacheStrategy !== "metadata" &&
452
+ cacheStrategy !== "content"
453
+ ) {
454
+ errors.push("'cacheStrategy' must be any of \"metadata\", \"content\".");
455
+ }
456
+ if (!isNonEmptyString(cwd) || !path.isAbsolute(cwd)) {
457
+ errors.push("'cwd' must be an absolute path.");
458
+ }
459
+ if (typeof errorOnUnmatchedPattern !== "boolean") {
460
+ errors.push("'errorOnUnmatchedPattern' must be a boolean.");
461
+ }
462
+ if (typeof fix !== "boolean" && typeof fix !== "function") {
463
+ errors.push("'fix' must be a boolean or a function.");
464
+ }
465
+ if (fixTypes !== null && !isFixTypeArray(fixTypes)) {
466
+ errors.push("'fixTypes' must be an array of any of \"directive\", \"problem\", \"suggestion\", and \"layout\".");
467
+ }
468
+ if (typeof globInputPaths !== "boolean") {
469
+ errors.push("'globInputPaths' must be a boolean.");
470
+ }
471
+ if (typeof ignore !== "boolean") {
472
+ errors.push("'ignore' must be a boolean.");
473
+ }
474
+ if (!isNonEmptyString(ignorePath) && ignorePath !== null) {
475
+ errors.push("'ignorePath' must be a non-empty string or null.");
476
+ }
477
+ if (typeof overrideConfig !== "object") {
478
+ errors.push("'overrideConfig' must be an object or null.");
479
+ }
480
+ if (!isNonEmptyString(overrideConfigFile) && overrideConfigFile !== null && overrideConfigFile !== true) {
481
+ errors.push("'overrideConfigFile' must be a non-empty string, null, or true.");
482
+ }
483
+ if (typeof plugins !== "object") {
484
+ errors.push("'plugins' must be an object or null.");
485
+ } else if (plugins !== null && Object.keys(plugins).includes("")) {
486
+ errors.push("'plugins' must not include an empty string.");
487
+ }
488
+ if (Array.isArray(plugins)) {
489
+ errors.push("'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead.");
490
+ }
491
+ if (
492
+ reportUnusedDisableDirectives !== "error" &&
493
+ reportUnusedDisableDirectives !== "warn" &&
494
+ reportUnusedDisableDirectives !== "off" &&
495
+ reportUnusedDisableDirectives !== null
496
+ ) {
497
+ errors.push("'reportUnusedDisableDirectives' must be any of \"error\", \"warn\", \"off\", and null.");
498
+ }
499
+ if (errors.length > 0) {
500
+ throw new ESLintInvalidOptionsError(errors);
501
+ }
502
+
503
+ return {
504
+ allowInlineConfig,
505
+ baseConfig,
506
+ cache,
507
+ cacheLocation,
508
+ cacheStrategy,
509
+
510
+ // when overrideConfigFile is true that means don't do config file lookup
511
+ configFile: overrideConfigFile === true ? false : overrideConfigFile,
512
+ overrideConfig,
513
+ cwd,
514
+ errorOnUnmatchedPattern,
515
+ fix,
516
+ fixTypes,
517
+ globInputPaths,
518
+ ignore,
519
+ ignorePath,
520
+ ignorePatterns,
521
+ reportUnusedDisableDirectives
522
+ };
523
+ }
524
+
525
+
526
+ //-----------------------------------------------------------------------------
527
+ // Cache-related helpers
528
+ //-----------------------------------------------------------------------------
529
+
530
+ /**
531
+ * return the cacheFile to be used by eslint, based on whether the provided parameter is
532
+ * a directory or looks like a directory (ends in `path.sep`), in which case the file
533
+ * name will be the `cacheFile/.cache_hashOfCWD`
534
+ *
535
+ * if cacheFile points to a file or looks like a file then in will just use that file
536
+ * @param {string} cacheFile The name of file to be used to store the cache
537
+ * @param {string} cwd Current working directory
538
+ * @returns {string} the resolved path to the cache file
539
+ */
540
+ function getCacheFile(cacheFile, cwd) {
541
+
542
+ /*
543
+ * make sure the path separators are normalized for the environment/os
544
+ * keeping the trailing path separator if present
545
+ */
546
+ const normalizedCacheFile = path.normalize(cacheFile);
547
+
548
+ const resolvedCacheFile = path.resolve(cwd, normalizedCacheFile);
549
+ const looksLikeADirectory = normalizedCacheFile.slice(-1) === path.sep;
550
+
551
+ /**
552
+ * return the name for the cache file in case the provided parameter is a directory
553
+ * @returns {string} the resolved path to the cacheFile
554
+ */
555
+ function getCacheFileForDirectory() {
556
+ return path.join(resolvedCacheFile, `.cache_${hash(cwd)}`);
557
+ }
558
+
559
+ let fileStats;
560
+
561
+ try {
562
+ fileStats = fs.lstatSync(resolvedCacheFile);
563
+ } catch {
564
+ fileStats = null;
565
+ }
566
+
567
+
568
+ /*
569
+ * in case the file exists we need to verify if the provided path
570
+ * is a directory or a file. If it is a directory we want to create a file
571
+ * inside that directory
572
+ */
573
+ if (fileStats) {
574
+
575
+ /*
576
+ * is a directory or is a file, but the original file the user provided
577
+ * looks like a directory but `path.resolve` removed the `last path.sep`
578
+ * so we need to still treat this like a directory
579
+ */
580
+ if (fileStats.isDirectory() || looksLikeADirectory) {
581
+ return getCacheFileForDirectory();
582
+ }
583
+
584
+ // is file so just use that file
585
+ return resolvedCacheFile;
586
+ }
587
+
588
+ /*
589
+ * here we known the file or directory doesn't exist,
590
+ * so we will try to infer if its a directory if it looks like a directory
591
+ * for the current operating system.
592
+ */
593
+
594
+ // if the last character passed is a path separator we assume is a directory
595
+ if (looksLikeADirectory) {
596
+ return getCacheFileForDirectory();
597
+ }
598
+
599
+ return resolvedCacheFile;
600
+ }
601
+
602
+
603
+ //-----------------------------------------------------------------------------
604
+ // Exports
605
+ //-----------------------------------------------------------------------------
606
+
607
+ module.exports = {
608
+ isGlobPattern,
609
+ directoryExists,
610
+ fileExists,
611
+ findFiles,
612
+
613
+ isNonEmptyString,
614
+ isArrayOfNonEmptyString,
615
+
616
+ createIgnoreResult,
617
+ isErrorMessage,
618
+
619
+ processOptions,
620
+
621
+ getCacheFile
622
+ };