eslint 8.26.0 → 8.28.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.
@@ -43,85 +43,110 @@ function pageTemplate(it) {
43
43
  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PScwIDAgMjk0LjgyNSAyNTguOTgyJyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnPg0KPHBhdGggZmlsbD0nIzgwODBGMicgZD0nTTk3LjAyMSw5OS4wMTZsNDguNDMyLTI3Ljk2MmMxLjIxMi0wLjcsMi43MDYtMC43LDMuOTE4LDBsNDguNDMzLDI3Ljk2MiBjMS4yMTEsMC43LDEuOTU5LDEuOTkzLDEuOTU5LDMuMzkzdjU1LjkyNGMwLDEuMzk5LTAuNzQ4LDIuNjkzLTEuOTU5LDMuMzk0bC00OC40MzMsMjcuOTYyYy0xLjIxMiwwLjctMi43MDYsMC43LTMuOTE4LDAgbC00OC40MzItMjcuOTYyYy0xLjIxMi0wLjctMS45NTktMS45OTQtMS45NTktMy4zOTR2LTU1LjkyNEM5NS4wNjMsMTAxLjAwOSw5NS44MSw5OS43MTYsOTcuMDIxLDk5LjAxNicvPg0KPHBhdGggZmlsbD0nIzRCMzJDMycgZD0nTTI3My4zMzYsMTI0LjQ4OEwyMTUuNDY5LDIzLjgxNmMtMi4xMDItMy42NC01Ljk4NS02LjMyNS0xMC4xODgtNi4zMjVIODkuNTQ1IGMtNC4yMDQsMC04LjA4OCwyLjY4NS0xMC4xOSw2LjMyNWwtNTcuODY3LDEwMC40NWMtMi4xMDIsMy42NDEtMi4xMDIsOC4yMzYsMCwxMS44NzdsNTcuODY3LDk5Ljg0NyBjMi4xMDIsMy42NCw1Ljk4Niw1LjUwMSwxMC4xOSw1LjUwMWgxMTUuNzM1YzQuMjAzLDAsOC4wODctMS44MDUsMTAuMTg4LTUuNDQ2bDU3Ljg2Ny0xMDAuMDEgQzI3NS40MzksMTMyLjM5NiwyNzUuNDM5LDEyOC4xMjgsMjczLjMzNiwxMjQuNDg4IE0yMjUuNDE5LDE3Mi44OThjMCwxLjQ4LTAuODkxLDIuODQ5LTIuMTc0LDMuNTlsLTczLjcxLDQyLjUyNyBjLTEuMjgyLDAuNzQtMi44ODgsMC43NC00LjE3LDBsLTczLjc2Ny00Mi41MjdjLTEuMjgyLTAuNzQxLTIuMTc5LTIuMTA5LTIuMTc5LTMuNTlWODcuODQzYzAtMS40ODEsMC44ODQtMi44NDksMi4xNjctMy41OSBsNzMuNzA3LTQyLjUyN2MxLjI4Mi0wLjc0MSwyLjg4Ni0wLjc0MSw0LjE2OCwwbDczLjc3Miw0Mi41MjdjMS4yODMsMC43NDEsMi4xODYsMi4xMDksMi4xODYsMy41OVYxNzIuODk4eicvPg0KPC9zdmc+">
44
44
  <style>
45
45
  body {
46
- font-family:Arial, "Helvetica Neue", Helvetica, sans-serif;
47
- font-size:16px;
48
- font-weight:normal;
49
- margin:0;
50
- padding:0;
51
- color:#333
46
+ font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
47
+ font-size: 16px;
48
+ font-weight: normal;
49
+ margin: 0;
50
+ padding: 0;
51
+ color: #333;
52
52
  }
53
+
53
54
  #overview {
54
- padding:20px 30px
55
+ padding: 20px 30px;
55
56
  }
56
- td, th {
57
- padding:5px 10px
57
+
58
+ td,
59
+ th {
60
+ padding: 5px 10px;
58
61
  }
62
+
59
63
  h1 {
60
- margin:0
64
+ margin: 0;
61
65
  }
66
+
62
67
  table {
63
- margin:30px;
64
- width:calc(100% - 60px);
65
- max-width:1000px;
66
- border-radius:5px;
67
- border:1px solid #ddd;
68
- border-spacing:0px;
68
+ margin: 30px;
69
+ width: calc(100% - 60px);
70
+ max-width: 1000px;
71
+ border-radius: 5px;
72
+ border: 1px solid #ddd;
73
+ border-spacing: 0;
69
74
  }
75
+
70
76
  th {
71
- font-weight:400;
72
- font-size:medium;
73
- text-align:left;
74
- cursor:pointer
77
+ font-weight: 400;
78
+ font-size: medium;
79
+ text-align: left;
80
+ cursor: pointer;
75
81
  }
76
- td.clr-1, td.clr-2, th span {
77
- font-weight:700
82
+
83
+ td.clr-1,
84
+ td.clr-2,
85
+ th span {
86
+ font-weight: 700;
78
87
  }
88
+
79
89
  th span {
80
- float:right;
81
- margin-left:20px
90
+ float: right;
91
+ margin-left: 20px;
82
92
  }
83
- th span:after {
84
- content:"";
85
- clear:both;
86
- display:block
93
+
94
+ th span::after {
95
+ content: "";
96
+ clear: both;
97
+ display: block;
87
98
  }
99
+
88
100
  tr:last-child td {
89
- border-bottom:none
101
+ border-bottom: none;
90
102
  }
91
- tr td:first-child, tr td:last-child {
92
- color:#9da0a4
103
+
104
+ tr td:first-child,
105
+ tr td:last-child {
106
+ color: #9da0a4;
93
107
  }
94
- #overview.bg-0, tr.bg-0 th {
95
- color:#468847;
96
- background:#dff0d8;
97
- border-bottom:1px solid #d6e9c6
108
+
109
+ #overview.bg-0,
110
+ tr.bg-0 th {
111
+ color: #468847;
112
+ background: #dff0d8;
113
+ border-bottom: 1px solid #d6e9c6;
98
114
  }
99
- #overview.bg-1, tr.bg-1 th {
100
- color:#f0ad4e;
101
- background:#fcf8e3;
102
- border-bottom:1px solid #fbeed5
115
+
116
+ #overview.bg-1,
117
+ tr.bg-1 th {
118
+ color: #f0ad4e;
119
+ background: #fcf8e3;
120
+ border-bottom: 1px solid #fbeed5;
103
121
  }
104
- #overview.bg-2, tr.bg-2 th {
105
- color:#b94a48;
106
- background:#f2dede;
107
- border-bottom:1px solid #eed3d7
122
+
123
+ #overview.bg-2,
124
+ tr.bg-2 th {
125
+ color: #b94a48;
126
+ background: #f2dede;
127
+ border-bottom: 1px solid #eed3d7;
108
128
  }
129
+
109
130
  td {
110
- border-bottom:1px solid #ddd
131
+ border-bottom: 1px solid #ddd;
111
132
  }
133
+
112
134
  td.clr-1 {
113
- color:#f0ad4e
135
+ color: #f0ad4e;
114
136
  }
137
+
115
138
  td.clr-2 {
116
- color:#b94a48
139
+ color: #b94a48;
117
140
  }
141
+
118
142
  td a {
119
- color:#3a33d1;
120
- text-decoration:none
143
+ color: #3a33d1;
144
+ text-decoration: none;
121
145
  }
146
+
122
147
  td a:hover {
123
- color:#272296;
124
- text-decoration:underline
148
+ color: #272296;
149
+ text-decoration: underline;
125
150
  }
126
151
  </style>
127
152
  </head>
@@ -214,7 +239,7 @@ function messageTemplate(it) {
214
239
  } = it;
215
240
 
216
241
  return `
217
- <tr style="display:none" class="f-${parentIndex}">
242
+ <tr style="display: none;" class="f-${parentIndex}">
218
243
  <td>${lineNumber}:${columnNumber}</td>
219
244
  <td class="clr-${severityNumber}">${severityName}</td>
220
245
  <td>${encodeHTML(message)}</td>
@@ -26,6 +26,18 @@ const isPathInside = require("is-path-inside");
26
26
 
27
27
  const doFsWalk = util.promisify(fswalk.walk);
28
28
  const Minimatch = minimatch.Minimatch;
29
+ const MINIMATCH_OPTIONS = { dot: true };
30
+
31
+ //-----------------------------------------------------------------------------
32
+ // Types
33
+ //-----------------------------------------------------------------------------
34
+
35
+ /**
36
+ * @typedef {Object} GlobSearch
37
+ * @property {Array<string>} patterns The normalized patterns to use for a search.
38
+ * @property {Array<string>} rawPatterns The patterns as entered by the user
39
+ * before doing any normalization.
40
+ */
29
41
 
30
42
  //-----------------------------------------------------------------------------
31
43
  // Errors
@@ -47,6 +59,30 @@ class NoFilesFoundError extends Error {
47
59
  }
48
60
  }
49
61
 
62
+ /**
63
+ * The error type when a search fails to match multiple patterns.
64
+ */
65
+ class UnmatchedSearchPatternsError extends Error {
66
+
67
+ /**
68
+ * @param {Object} options The options for the error.
69
+ * @param {string} options.basePath The directory that was searched.
70
+ * @param {Array<string>} options.unmatchedPatterns The glob patterns
71
+ * which were not found.
72
+ * @param {Array<string>} options.patterns The glob patterns that were
73
+ * searched.
74
+ * @param {Array<string>} options.rawPatterns The raw glob patterns that
75
+ * were searched.
76
+ */
77
+ constructor({ basePath, unmatchedPatterns, patterns, rawPatterns }) {
78
+ super(`No files matching '${rawPatterns}' in '${basePath}' were found.`);
79
+ this.basePath = basePath;
80
+ this.unmatchedPatterns = unmatchedPatterns;
81
+ this.patterns = patterns;
82
+ this.rawPatterns = rawPatterns;
83
+ }
84
+ }
85
+
50
86
  /**
51
87
  * The error type when there are files matched by a glob, but all of them have been ignored.
52
88
  */
@@ -107,6 +143,69 @@ function isGlobPattern(pattern) {
107
143
  return isGlob(path.sep === "\\" ? normalizeToPosix(pattern) : pattern);
108
144
  }
109
145
 
146
+
147
+ /**
148
+ * Determines if a given glob pattern will return any results.
149
+ * Used primarily to help with useful error messages.
150
+ * @param {Object} options The options for the function.
151
+ * @param {string} options.basePath The directory to search.
152
+ * @param {string} options.pattern A glob pattern to match.
153
+ * @returns {Promise<boolean>} True if there is a glob match, false if not.
154
+ */
155
+ function globMatch({ basePath, pattern }) {
156
+
157
+ let found = false;
158
+ const patternToUse = path.isAbsolute(pattern)
159
+ ? normalizeToPosix(path.relative(basePath, pattern))
160
+ : pattern;
161
+
162
+ const matcher = new Minimatch(patternToUse, MINIMATCH_OPTIONS);
163
+
164
+ const fsWalkSettings = {
165
+
166
+ deepFilter(entry) {
167
+ const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
168
+
169
+ return !found && matcher.match(relativePath, true);
170
+ },
171
+
172
+ entryFilter(entry) {
173
+ if (found || entry.dirent.isDirectory()) {
174
+ return false;
175
+ }
176
+
177
+ const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
178
+
179
+ if (matcher.match(relativePath)) {
180
+ found = true;
181
+ return true;
182
+ }
183
+
184
+ return false;
185
+ }
186
+ };
187
+
188
+ return new Promise(resolve => {
189
+
190
+ // using a stream so we can exit early because we just need one match
191
+ const globStream = fswalk.walkStream(basePath, fsWalkSettings);
192
+
193
+ globStream.on("data", () => {
194
+ globStream.destroy();
195
+ resolve(true);
196
+ });
197
+
198
+ // swallow errors as they're not important here
199
+ globStream.on("error", () => { });
200
+
201
+ globStream.on("end", () => {
202
+ resolve(false);
203
+ });
204
+ globStream.read();
205
+ });
206
+
207
+ }
208
+
110
209
  /**
111
210
  * Searches a directory looking for matching glob patterns. This uses
112
211
  * the config array's logic to determine if a directory or file should
@@ -116,26 +215,62 @@ function isGlobPattern(pattern) {
116
215
  * @param {string} options.basePath The directory to search.
117
216
  * @param {Array<string>} options.patterns An array of glob patterns
118
217
  * to match.
218
+ * @param {Array<string>} options.rawPatterns An array of glob patterns
219
+ * as the user inputted them. Used for errors.
119
220
  * @param {FlatConfigArray} options.configs The config array to use for
120
221
  * determining what to ignore.
222
+ * @param {boolean} options.errorOnUnmatchedPattern Determines if an error
223
+ * should be thrown when a pattern is unmatched.
121
224
  * @returns {Promise<Array<string>>} An array of matching file paths
122
225
  * or an empty array if there are no matches.
226
+ * @throws {UnmatchedSearchPatternsErrror} If there is a pattern that doesn't
227
+ * match any files.
123
228
  */
124
- async function globSearch({ basePath, patterns, configs }) {
229
+ async function globSearch({
230
+ basePath,
231
+ patterns,
232
+ rawPatterns,
233
+ configs,
234
+ errorOnUnmatchedPattern
235
+ }) {
125
236
 
126
237
  if (patterns.length === 0) {
127
238
  return [];
128
239
  }
129
240
 
130
- const matchers = patterns.map(pattern => {
241
+ /*
242
+ * In this section we are converting the patterns into Minimatch
243
+ * instances for performance reasons. Because we are doing the same
244
+ * matches repeatedly, it's best to compile those patterns once and
245
+ * reuse them multiple times.
246
+ *
247
+ * To do that, we convert any patterns with an absolute path into a
248
+ * relative path and normalize it to Posix-style slashes. We also keep
249
+ * track of the relative patterns to map them back to the original
250
+ * patterns, which we need in order to throw an error if there are any
251
+ * unmatched patterns.
252
+ */
253
+ const relativeToPatterns = new Map();
254
+ const matchers = patterns.map((pattern, i) => {
131
255
  const patternToUse = path.isAbsolute(pattern)
132
256
  ? normalizeToPosix(path.relative(basePath, pattern))
133
257
  : pattern;
134
258
 
135
- return new minimatch.Minimatch(patternToUse);
259
+ relativeToPatterns.set(patternToUse, patterns[i]);
260
+
261
+ return new Minimatch(patternToUse, MINIMATCH_OPTIONS);
136
262
  });
137
263
 
138
- return (await doFsWalk(basePath, {
264
+ /*
265
+ * We track unmatched patterns because we may want to throw an error when
266
+ * they occur. To start, this set is initialized with all of the patterns.
267
+ * Every time a match occurs, the pattern is removed from the set, making
268
+ * it easy to tell if we have any unmatched patterns left at the end of
269
+ * search.
270
+ */
271
+ const unmatchedPatterns = new Set([...relativeToPatterns.keys()]);
272
+
273
+ const filePaths = (await doFsWalk(basePath, {
139
274
 
140
275
  deepFilter(entry) {
141
276
  const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
@@ -151,94 +286,171 @@ async function globSearch({ basePath, patterns, configs }) {
151
286
  return false;
152
287
  }
153
288
 
154
- const matchesPattern = matchers.some(matcher => matcher.match(relativePath));
289
+ /*
290
+ * Optimization: We need to track when patterns are left unmatched
291
+ * and so we use `unmatchedPatterns` to do that. There is a bit of
292
+ * complexity here because the same file can be matched by more than
293
+ * one pattern. So, when we start, we actually need to test every
294
+ * pattern against every file. Once we know there are no remaining
295
+ * unmatched patterns, then we can switch to just looking for the
296
+ * first matching pattern for improved speed.
297
+ */
298
+ const matchesPattern = unmatchedPatterns.size > 0
299
+ ? matchers.reduce((previousValue, matcher) => {
300
+ const pathMatches = matcher.match(relativePath);
301
+
302
+ /*
303
+ * We updated the unmatched patterns set only if the path
304
+ * matches and the file isn't ignored. If the file is
305
+ * ignored, that means there wasn't a match for the
306
+ * pattern so it should not be removed.
307
+ *
308
+ * Performance note: isFileIgnored() aggressively caches
309
+ * results so there is no performance penalty for calling
310
+ * it twice with the same argument.
311
+ */
312
+ if (pathMatches && !configs.isFileIgnored(entry.path)) {
313
+ unmatchedPatterns.delete(matcher.pattern);
314
+ }
315
+
316
+ return pathMatches || previousValue;
317
+ }, false)
318
+ : matchers.some(matcher => matcher.match(relativePath));
155
319
 
156
320
  return matchesPattern && !configs.isFileIgnored(entry.path);
157
321
  }
322
+
158
323
  })).map(entry => entry.path);
159
324
 
325
+ // now check to see if we have any unmatched patterns
326
+ if (errorOnUnmatchedPattern && unmatchedPatterns.size > 0) {
327
+ throw new UnmatchedSearchPatternsError({
328
+ basePath,
329
+ unmatchedPatterns: [...unmatchedPatterns].map(
330
+ pattern => relativeToPatterns.get(pattern)
331
+ ),
332
+ patterns,
333
+ rawPatterns
334
+ });
335
+ }
336
+
337
+ return filePaths;
338
+ }
339
+
340
+ /**
341
+ * Throws an error for unmatched patterns. The error will only contain information about the first one.
342
+ * Checks to see if there are any ignored results for a given search.
343
+ * @param {Object} options The options for this function.
344
+ * @param {string} options.basePath The directory to search.
345
+ * @param {Array<string>} options.patterns An array of glob patterns
346
+ * that were used in the original search.
347
+ * @param {Array<string>} options.rawPatterns An array of glob patterns
348
+ * as the user inputted them. Used for errors.
349
+ * @param {Array<string>} options.unmatchedPatterns A non-empty array of glob patterns
350
+ * that were unmatched in the original search.
351
+ * @returns {void} Always throws an error.
352
+ * @throws {NoFilesFoundError} If the first unmatched pattern
353
+ * doesn't match any files even when there are no ignores.
354
+ * @throws {AllFilesIgnoredError} If the first unmatched pattern
355
+ * matches some files when there are no ignores.
356
+ */
357
+ async function throwErrorForUnmatchedPatterns({
358
+ basePath,
359
+ patterns,
360
+ rawPatterns,
361
+ unmatchedPatterns
362
+ }) {
363
+
364
+ const pattern = unmatchedPatterns[0];
365
+ const rawPattern = rawPatterns[patterns.indexOf(pattern)];
366
+
367
+ const patternHasMatch = await globMatch({
368
+ basePath,
369
+ pattern
370
+ });
371
+
372
+ if (patternHasMatch) {
373
+ throw new AllFilesIgnoredError(rawPattern);
374
+ }
375
+
376
+ // if we get here there are truly no matches
377
+ throw new NoFilesFoundError(rawPattern, true);
160
378
  }
161
379
 
162
380
  /**
163
381
  * Performs multiple glob searches in parallel.
164
382
  * @param {Object} options The options for this function.
165
- * @param {Array<{patterns:Array<string>,rawPatterns:Array<string>}>} options.searches
383
+ * @param {Map<string,GlobSearch>} options.searches
166
384
  * An array of glob patterns to match.
167
385
  * @param {FlatConfigArray} options.configs The config array to use for
168
386
  * determining what to ignore.
387
+ * @param {boolean} options.errorOnUnmatchedPattern Determines if an
388
+ * unmatched glob pattern should throw an error.
169
389
  * @returns {Promise<Array<string>>} An array of matching file paths
170
390
  * or an empty array if there are no matches.
171
391
  */
172
- async function globMultiSearch({ searches, configs }) {
392
+ async function globMultiSearch({ searches, configs, errorOnUnmatchedPattern }) {
173
393
 
174
- const results = await Promise.all(
175
- [...searches].map(
176
- ([basePath, { patterns }]) => globSearch({ basePath, patterns, configs })
394
+ /*
395
+ * For convenience, we normalized the search map into an array of objects.
396
+ * Next, we filter out all searches that have no patterns. This happens
397
+ * primarily for the cwd, which is prepopulated in the searches map as an
398
+ * optimization. However, if it has no patterns, it means all patterns
399
+ * occur outside of the cwd and we can safely filter out that search.
400
+ */
401
+ const normalizedSearches = [...searches].map(
402
+ ([basePath, { patterns, rawPatterns }]) => ({ basePath, patterns, rawPatterns })
403
+ ).filter(({ patterns }) => patterns.length > 0);
404
+
405
+ const results = await Promise.allSettled(
406
+ normalizedSearches.map(
407
+ ({ basePath, patterns, rawPatterns }) => globSearch({
408
+ basePath,
409
+ patterns,
410
+ rawPatterns,
411
+ configs,
412
+ errorOnUnmatchedPattern
413
+ })
177
414
  )
178
415
  );
179
416
 
180
- return [...new Set(results.flat())];
181
- }
417
+ const filePaths = [];
182
418
 
183
- /**
184
- * Determines if a given glob pattern will return any results.
185
- * Used primarily to help with useful error messages.
186
- * @param {Object} options The options for the function.
187
- * @param {string} options.basePath The directory to search.
188
- * @param {string} options.pattern A glob pattern to match.
189
- * @returns {Promise<boolean>} True if there is a glob match, false if not.
190
- */
191
- function globMatch({ basePath, pattern }) {
192
-
193
- let found = false;
194
- const patternToUse = path.isAbsolute(pattern)
195
- ? normalizeToPosix(path.relative(basePath, pattern))
196
- : pattern;
419
+ for (let i = 0; i < results.length; i++) {
197
420
 
198
- const matcher = new Minimatch(patternToUse);
421
+ const result = results[i];
422
+ const currentSearch = normalizedSearches[i];
199
423
 
200
- const fsWalkSettings = {
201
-
202
- deepFilter(entry) {
203
- const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
424
+ if (result.status === "fulfilled") {
204
425
 
205
- return !found && matcher.match(relativePath, true);
206
- },
207
-
208
- entryFilter(entry) {
209
- if (found || entry.dirent.isDirectory()) {
210
- return false;
426
+ // if the search was successful just add the results
427
+ if (result.value.length > 0) {
428
+ filePaths.push(...result.value);
211
429
  }
212
430
 
213
- const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
431
+ continue;
432
+ }
214
433
 
215
- if (matcher.match(relativePath)) {
216
- found = true;
217
- return true;
218
- }
434
+ // if we make it here then there was an error
435
+ const error = result.reason;
219
436
 
220
- return false;
437
+ // unexpected errors should be re-thrown
438
+ if (!error.basePath) {
439
+ throw error;
221
440
  }
222
- };
223
441
 
224
- return new Promise(resolve => {
442
+ if (errorOnUnmatchedPattern) {
225
443
 
226
- // using a stream so we can exit early because we just need one match
227
- const globStream = fswalk.walkStream(basePath, fsWalkSettings);
444
+ await throwErrorForUnmatchedPatterns({
445
+ ...currentSearch,
446
+ unmatchedPatterns: error.unmatchedPatterns
447
+ });
228
448
 
229
- globStream.on("data", () => {
230
- globStream.destroy();
231
- resolve(true);
232
- });
449
+ }
233
450
 
234
- // swallow errors as they're not important here
235
- globStream.on("error", () => {});
451
+ }
236
452
 
237
- globStream.on("end", () => {
238
- resolve(false);
239
- });
240
- globStream.read();
241
- });
453
+ return [...new Set(filePaths)];
242
454
 
243
455
  }
244
456
 
@@ -335,47 +547,17 @@ async function findFiles({
335
547
  }
336
548
  });
337
549
 
338
- const globbyResults = await globMultiSearch({
339
- searches,
340
- configs
341
- });
342
-
343
- // if there are no results, tell the user why
344
- if (!results.length && !globbyResults.length) {
345
-
346
- for (const [basePath, { patterns: patternsToCheck, rawPatterns: patternsToReport }] of searches) {
347
-
348
- let index = 0;
349
-
350
- // try globby without ignoring anything
351
- for (const patternToCheck of patternsToCheck) {
352
-
353
- // check if there are any matches at all
354
- const patternHasMatch = await globMatch({
355
- basePath,
356
- pattern: patternToCheck
357
- });
358
-
359
- if (patternHasMatch) {
360
- throw new AllFilesIgnoredError(patternsToReport[index]);
361
- }
362
-
363
- // otherwise no files were found
364
- if (errorOnUnmatchedPattern) {
365
- throw new NoFilesFoundError(patternsToReport[index], globInputPaths);
366
- }
367
-
368
- index++;
369
- }
370
- }
371
-
372
- }
373
-
374
550
  // there were patterns that didn't match anything, tell the user
375
551
  if (errorOnUnmatchedPattern && missingPatterns.length) {
376
552
  throw new NoFilesFoundError(missingPatterns[0], globInputPaths);
377
553
  }
378
554
 
555
+ // now we are safe to do the search
556
+ const globbyResults = await globMultiSearch({
557
+ searches,
558
+ configs,
559
+ errorOnUnmatchedPattern
560
+ });
379
561
 
380
562
  return [
381
563
  ...results,
@@ -161,6 +161,16 @@ function createRulesMeta(rules) {
161
161
  }, {});
162
162
  }
163
163
 
164
+ /**
165
+ * Return the absolute path of a file named `"__placeholder__.js"` in a given directory.
166
+ * This is used as a replacement for a missing file path.
167
+ * @param {string} cwd An absolute directory path.
168
+ * @returns {string} The absolute path of a file named `"__placeholder__.js"` in the given directory.
169
+ */
170
+ function getPlaceholderPath(cwd) {
171
+ return path.join(cwd, "__placeholder__.js");
172
+ }
173
+
164
174
  /** @type {WeakMap<ExtractedConfig, DeprecatedRuleInfo[]>} */
165
175
  const usedDeprecatedRulesCache = new WeakMap();
166
176
 
@@ -177,7 +187,7 @@ function getOrFindUsedDeprecatedRules(eslint, maybeFilePath) {
177
187
  } = privateMembers.get(eslint);
178
188
  const filePath = path.isAbsolute(maybeFilePath)
179
189
  ? maybeFilePath
180
- : path.join(cwd, "__placeholder__.js");
190
+ : getPlaceholderPath(cwd);
181
191
  const config = configs.getConfig(filePath);
182
192
 
183
193
  // Most files use the same config, so cache it.
@@ -422,7 +432,7 @@ function verifyText({
422
432
  * `config.extractConfig(filePath)` requires an absolute path, but `linter`
423
433
  * doesn't know CWD, so it gives `linter` an absolute path always.
424
434
  */
425
- const filePathToVerify = filePath === "<text>" ? path.join(cwd, "__placeholder__.js") : filePath;
435
+ const filePathToVerify = filePath === "<text>" ? getPlaceholderPath(cwd) : filePath;
426
436
  const { fixed, messages, output } = linter.verifyAndFix(
427
437
  text,
428
438
  configs,
@@ -519,6 +529,14 @@ function *iterateRuleDeprecationWarnings(configs) {
519
529
  }
520
530
  }
521
531
 
532
+ /**
533
+ * Creates an error to be thrown when an array of results passed to `getRulesMetaForResults` was not created by the current engine.
534
+ * @returns {TypeError} An error object.
535
+ */
536
+ function createExtraneousResultsError() {
537
+ return new TypeError("Results object was not created from this ESLint instance.");
538
+ }
539
+
522
540
  //-----------------------------------------------------------------------------
523
541
  // Main API
524
542
  //-----------------------------------------------------------------------------
@@ -655,7 +673,10 @@ class FlatESLint {
655
673
  }
656
674
 
657
675
  const resultRules = new Map();
658
- const { configs } = privateMembers.get(this);
676
+ const {
677
+ configs,
678
+ options: { cwd }
679
+ } = privateMembers.get(this);
659
680
 
660
681
  /*
661
682
  * We can only accurately return rules meta information for linting results if the
@@ -664,7 +685,7 @@ class FlatESLint {
664
685
  * to let the user know we can't do anything here.
665
686
  */
666
687
  if (!configs) {
667
- throw new TypeError("Results object was not created from this ESLint instance.");
688
+ throw createExtraneousResultsError();
668
689
  }
669
690
 
670
691
  for (const result of results) {
@@ -673,13 +694,7 @@ class FlatESLint {
673
694
  * Normalize filename for <text>.
674
695
  */
675
696
  const filePath = result.filePath === "<text>"
676
- ? "__placeholder__.js" : result.filePath;
677
-
678
- /*
679
- * All of the plugin and rule information is contained within the
680
- * calculated config for the given file.
681
- */
682
- const config = configs.getConfig(filePath);
697
+ ? getPlaceholderPath(cwd) : result.filePath;
683
698
  const allMessages = result.messages.concat(result.suppressedMessages);
684
699
 
685
700
  for (const { ruleId } of allMessages) {
@@ -687,6 +702,15 @@ class FlatESLint {
687
702
  continue;
688
703
  }
689
704
 
705
+ /*
706
+ * All of the plugin and rule information is contained within the
707
+ * calculated config for the given file.
708
+ */
709
+ const config = configs.getConfig(filePath);
710
+
711
+ if (!config) {
712
+ throw createExtraneousResultsError();
713
+ }
690
714
  const rule = getRuleFromConfig(ruleId, config);
691
715
 
692
716
  // ensure the rule exists
@@ -1024,7 +1048,7 @@ class FlatESLint {
1024
1048
  const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter");
1025
1049
 
1026
1050
  // TODO: This is pretty dirty...would be nice to clean up at some point.
1027
- formatterPath = ModuleResolver.resolve(npmFormat, path.join(cwd, "__placeholder__.js"));
1051
+ formatterPath = ModuleResolver.resolve(npmFormat, getPlaceholderPath(cwd));
1028
1052
  } catch {
1029
1053
  formatterPath = path.resolve(__dirname, "../", "cli-engine", "formatters", `${normalizedFormatName}.js`);
1030
1054
  }
@@ -50,7 +50,7 @@ function normalizeOptions(optionValue, ecmaVersion) {
50
50
  objects: optionValue,
51
51
  imports: optionValue,
52
52
  exports: optionValue,
53
- functions: (!ecmaVersion || ecmaVersion < 8) ? "ignore" : optionValue
53
+ functions: ecmaVersion < 2017 ? "ignore" : optionValue
54
54
  };
55
55
  }
56
56
  if (typeof optionValue === "object" && optionValue !== null) {
@@ -134,7 +134,7 @@ module.exports = {
134
134
  },
135
135
 
136
136
  create(context) {
137
- const options = normalizeOptions(context.options[0], context.parserOptions.ecmaVersion);
137
+ const options = normalizeOptions(context.options[0], context.languageOptions.ecmaVersion);
138
138
 
139
139
  const sourceCode = context.getSourceCode();
140
140
 
@@ -15,7 +15,7 @@ module.exports = {
15
15
  type: "problem",
16
16
 
17
17
  docs: {
18
- description: "Enforce \"for\" loop update clause moving the counter in the right direction.",
18
+ description: "Enforce \"for\" loop update clause moving the counter in the right direction",
19
19
  recommended: true,
20
20
  url: "https://eslint.org/docs/rules/for-direction"
21
21
  },
@@ -44,7 +44,7 @@ function isModuleExports(pattern) {
44
44
  * @returns {boolean} True if the string is a valid identifier
45
45
  */
46
46
  function isIdentifier(name, ecmaVersion) {
47
- if (ecmaVersion >= 6) {
47
+ if (ecmaVersion >= 2015) {
48
48
  return esutils.keyword.isIdentifierES6(name);
49
49
  }
50
50
  return esutils.keyword.isIdentifierES5(name);
@@ -104,7 +104,7 @@ module.exports = {
104
104
  const nameMatches = typeof context.options[0] === "string" ? context.options[0] : "always";
105
105
  const considerPropertyDescriptor = options.considerPropertyDescriptor;
106
106
  const includeModuleExports = options.includeCommonJSModuleExports;
107
- const ecmaVersion = context.parserOptions && context.parserOptions.ecmaVersion ? context.parserOptions.ecmaVersion : 5;
107
+ const ecmaVersion = context.languageOptions.ecmaVersion;
108
108
 
109
109
  /**
110
110
  * Check whether node is a certain CallExpression.
@@ -123,6 +123,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({
123
123
  "no-empty-character-class": () => require("./no-empty-character-class"),
124
124
  "no-empty-function": () => require("./no-empty-function"),
125
125
  "no-empty-pattern": () => require("./no-empty-pattern"),
126
+ "no-empty-static-block": () => require("./no-empty-static-block"),
126
127
  "no-eq-null": () => require("./no-eq-null"),
127
128
  "no-eval": () => require("./no-eval"),
128
129
  "no-ex-assign": () => require("./no-ex-assign"),
@@ -167,6 +168,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({
167
168
  "no-nested-ternary": () => require("./no-nested-ternary"),
168
169
  "no-new": () => require("./no-new"),
169
170
  "no-new-func": () => require("./no-new-func"),
171
+ "no-new-native-nonconstructor": () => require("./no-new-native-nonconstructor"),
170
172
  "no-new-object": () => require("./no-new-object"),
171
173
  "no-new-require": () => require("./no-new-require"),
172
174
  "no-new-symbol": () => require("./no-new-symbol"),
@@ -334,6 +334,19 @@ module.exports = {
334
334
 
335
335
  const sourceCode = context.getSourceCode();
336
336
 
337
+ /**
338
+ * Determines if the given property is key-value property.
339
+ * @param {ASTNode} property Property node to check.
340
+ * @returns {boolean} Whether the property is a key-value property.
341
+ */
342
+ function isKeyValueProperty(property) {
343
+ return !(
344
+ (property.method ||
345
+ property.shorthand ||
346
+ property.kind !== "init" || property.type !== "Property") // Could be "ExperimentalSpreadProperty" or "SpreadElement"
347
+ );
348
+ }
349
+
337
350
  /**
338
351
  * Checks whether a property is a member of the property group it follows.
339
352
  * @param {ASTNode} lastMember The last Property known to be in the group.
@@ -342,9 +355,9 @@ module.exports = {
342
355
  */
343
356
  function continuesPropertyGroup(lastMember, candidate) {
344
357
  const groupEndLine = lastMember.loc.start.line,
345
- candidateStartLine = candidate.loc.start.line;
358
+ candidateValueStartLine = (isKeyValueProperty(candidate) ? candidate.value : candidate).loc.start.line;
346
359
 
347
- if (candidateStartLine - groupEndLine <= 1) {
360
+ if (candidateValueStartLine - groupEndLine <= 1) {
348
361
  return true;
349
362
  }
350
363
 
@@ -358,7 +371,7 @@ module.exports = {
358
371
  if (
359
372
  leadingComments.length &&
360
373
  leadingComments[0].loc.start.line - groupEndLine <= 1 &&
361
- candidateStartLine - last(leadingComments).loc.end.line <= 1
374
+ candidateValueStartLine - last(leadingComments).loc.end.line <= 1
362
375
  ) {
363
376
  for (let i = 1; i < leadingComments.length; i++) {
364
377
  if (leadingComments[i].loc.start.line - leadingComments[i - 1].loc.end.line > 1) {
@@ -371,19 +384,6 @@ module.exports = {
371
384
  return false;
372
385
  }
373
386
 
374
- /**
375
- * Determines if the given property is key-value property.
376
- * @param {ASTNode} property Property node to check.
377
- * @returns {boolean} Whether the property is a key-value property.
378
- */
379
- function isKeyValueProperty(property) {
380
- return !(
381
- (property.method ||
382
- property.shorthand ||
383
- property.kind !== "init" || property.type !== "Property") // Could be "ExperimentalSpreadProperty" or "SpreadElement"
384
- );
385
- }
386
-
387
387
  /**
388
388
  * Starting from the given a node (a property.key node here) looks forward
389
389
  * until it finds the last token before a colon punctuator and returns it.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @fileoverview Rule to disallow empty static blocks.
3
+ * @author Sosuke Suzuki
4
+ */
5
+ "use strict";
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Rule Definition
9
+ //------------------------------------------------------------------------------
10
+
11
+ /** @type {import('../shared/types').Rule} */
12
+ module.exports = {
13
+ meta: {
14
+ type: "suggestion",
15
+
16
+ docs: {
17
+ description: "Disallow empty static blocks",
18
+ recommended: false,
19
+ url: "https://eslint.org/docs/rules/no-empty-static-block"
20
+ },
21
+
22
+ schema: [],
23
+
24
+ messages: {
25
+ unexpected: "Unexpected empty static block."
26
+ }
27
+ },
28
+
29
+ create(context) {
30
+ const sourceCode = context.getSourceCode();
31
+
32
+ return {
33
+ StaticBlock(node) {
34
+ if (node.body.length === 0) {
35
+ const closingBrace = sourceCode.getLastToken(node);
36
+
37
+ if (sourceCode.getCommentsBefore(closingBrace).length === 0) {
38
+ context.report({
39
+ node,
40
+ messageId: "unexpected"
41
+ });
42
+ }
43
+ }
44
+ }
45
+ };
46
+ }
47
+ };
@@ -17,6 +17,7 @@ const astUtils = require("./utils/ast-utils");
17
17
  /** @type {import('../shared/types').Rule} */
18
18
  module.exports = {
19
19
  meta: {
20
+ hasSuggestions: true,
20
21
  type: "suggestion",
21
22
 
22
23
  docs: {
@@ -39,7 +40,8 @@ module.exports = {
39
40
  ],
40
41
 
41
42
  messages: {
42
- unexpected: "Empty {{type}} statement."
43
+ unexpected: "Empty {{type}} statement.",
44
+ suggestComment: "Add comment inside empty {{type}} statement."
43
45
  }
44
46
  },
45
47
 
@@ -71,7 +73,22 @@ module.exports = {
71
73
  return;
72
74
  }
73
75
 
74
- context.report({ node, messageId: "unexpected", data: { type: "block" } });
76
+ context.report({
77
+ node,
78
+ messageId: "unexpected",
79
+ data: { type: "block" },
80
+ suggest: [
81
+ {
82
+ messageId: "suggestComment",
83
+ data: { type: "block" },
84
+ fix(fixer) {
85
+ const range = [node.range[0] + 1, node.range[1] - 1];
86
+
87
+ return fixer.replaceTextRange(range, " /* empty */ ");
88
+ }
89
+ }
90
+ ]
91
+ });
75
92
  },
76
93
 
77
94
  SwitchStatement(node) {
@@ -71,6 +71,24 @@ function isMultiplyByOne(node) {
71
71
  );
72
72
  }
73
73
 
74
+ /**
75
+ * Checks whether the given node logically represents multiplication by a fraction of `1`.
76
+ * For example, `a * 1` in `a * 1 / b` is technically multiplication by `1`, but the
77
+ * whole expression can be logically interpreted as `a * (1 / b)` rather than `(a * 1) / b`.
78
+ * @param {BinaryExpression} node A BinaryExpression node to check.
79
+ * @param {SourceCode} sourceCode The source code object.
80
+ * @returns {boolean} Whether or not the node is a multiplying by a fraction of `1`.
81
+ */
82
+ function isMultiplyByFractionOfOne(node, sourceCode) {
83
+ return node.type === "BinaryExpression" &&
84
+ node.operator === "*" &&
85
+ (node.right.type === "Literal" && node.right.value === 1) &&
86
+ node.parent.type === "BinaryExpression" &&
87
+ node.parent.operator === "/" &&
88
+ node.parent.left === node &&
89
+ !astUtils.isParenthesised(sourceCode, node);
90
+ }
91
+
74
92
  /**
75
93
  * Checks whether the result of a node is numeric or not
76
94
  * @param {ASTNode} node The node to test
@@ -290,7 +308,8 @@ module.exports = {
290
308
 
291
309
  // 1 * foo
292
310
  operatorAllowed = options.allow.includes("*");
293
- const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && getNonNumericOperand(node);
311
+ const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && !isMultiplyByFractionOfOne(node, sourceCode) &&
312
+ getNonNumericOperand(node);
294
313
 
295
314
  if (nonNumericOperand) {
296
315
  const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
@@ -65,6 +65,10 @@ module.exports = {
65
65
  ignoreDefaultValues: {
66
66
  type: "boolean",
67
67
  default: false
68
+ },
69
+ ignoreClassFieldInitialValues: {
70
+ type: "boolean",
71
+ default: false
68
72
  }
69
73
  },
70
74
  additionalProperties: false
@@ -82,7 +86,8 @@ module.exports = {
82
86
  enforceConst = !!config.enforceConst,
83
87
  ignore = new Set((config.ignore || []).map(normalizeIgnoreValue)),
84
88
  ignoreArrayIndexes = !!config.ignoreArrayIndexes,
85
- ignoreDefaultValues = !!config.ignoreDefaultValues;
89
+ ignoreDefaultValues = !!config.ignoreDefaultValues,
90
+ ignoreClassFieldInitialValues = !!config.ignoreClassFieldInitialValues;
86
91
 
87
92
  const okTypes = detectObjects ? [] : ["ObjectExpression", "Property", "AssignmentExpression"];
88
93
 
@@ -106,6 +111,17 @@ module.exports = {
106
111
  return parent.type === "AssignmentPattern" && parent.right === fullNumberNode;
107
112
  }
108
113
 
114
+ /**
115
+ * Returns whether the number is the initial value of a class field.
116
+ * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
117
+ * @returns {boolean} true if the number is the initial value of a class field.
118
+ */
119
+ function isClassFieldInitialValue(fullNumberNode) {
120
+ const parent = fullNumberNode.parent;
121
+
122
+ return parent.type === "PropertyDefinition" && parent.value === fullNumberNode;
123
+ }
124
+
109
125
  /**
110
126
  * Returns whether the given node is used as a radix within parseInt() or Number.parseInt()
111
127
  * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
@@ -194,6 +210,7 @@ module.exports = {
194
210
  if (
195
211
  isIgnoredValue(value) ||
196
212
  (ignoreDefaultValues && isDefaultValue(fullNumberNode)) ||
213
+ (ignoreClassFieldInitialValues && isClassFieldInitialValue(fullNumberNode)) ||
197
214
  isParseIntRadix(fullNumberNode) ||
198
215
  isJSXNumber(fullNumberNode) ||
199
216
  (ignoreArrayIndexes && isArrayIndex(fullNumberNode, value))
@@ -193,15 +193,15 @@ module.exports = {
193
193
  * ecmaVersion doesn't support the `u` flag.
194
194
  */
195
195
  function isValidWithUnicodeFlag(pattern) {
196
- const { ecmaVersion } = context.parserOptions;
196
+ const { ecmaVersion } = context.languageOptions;
197
197
 
198
- // ecmaVersion is unknown or it doesn't support the 'u' flag
199
- if (typeof ecmaVersion !== "number" || ecmaVersion <= 5) {
198
+ // ecmaVersion <= 5 doesn't support the 'u' flag
199
+ if (ecmaVersion <= 5) {
200
200
  return false;
201
201
  }
202
202
 
203
203
  const validator = new RegExpValidator({
204
- ecmaVersion: Math.min(ecmaVersion + 2009, REGEXPP_LATEST_ECMA_VERSION)
204
+ ecmaVersion: Math.min(ecmaVersion, REGEXPP_LATEST_ECMA_VERSION)
205
205
  });
206
206
 
207
207
  try {
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @fileoverview Rule to disallow use of the new operator with global non-constructor functions
3
+ * @author Sosuke Suzuki
4
+ */
5
+
6
+ "use strict";
7
+
8
+ //------------------------------------------------------------------------------
9
+ // Helpers
10
+ //------------------------------------------------------------------------------
11
+
12
+ const nonConstructorGlobalFunctionNames = ["Symbol", "BigInt"];
13
+
14
+ //------------------------------------------------------------------------------
15
+ // Rule Definition
16
+ //------------------------------------------------------------------------------
17
+
18
+ /** @type {import('../shared/types').Rule} */
19
+ module.exports = {
20
+ meta: {
21
+ type: "problem",
22
+
23
+ docs: {
24
+ description: "Disallow `new` operators with global non-constructor functions",
25
+ recommended: false,
26
+ url: "https://eslint.org/docs/rules/no-new-native-nonconstructor"
27
+ },
28
+
29
+ schema: [],
30
+
31
+ messages: {
32
+ noNewNonconstructor: "`{{name}}` cannot be called as a constructor."
33
+ }
34
+ },
35
+
36
+ create(context) {
37
+
38
+ return {
39
+ "Program:exit"() {
40
+ const globalScope = context.getScope();
41
+
42
+ for (const nonConstructorName of nonConstructorGlobalFunctionNames) {
43
+ const variable = globalScope.set.get(nonConstructorName);
44
+
45
+ if (variable && variable.defs.length === 0) {
46
+ variable.references.forEach(ref => {
47
+ const node = ref.identifier;
48
+ const parent = node.parent;
49
+
50
+ if (parent && parent.type === "NewExpression" && parent.callee === node) {
51
+ context.report({
52
+ node,
53
+ messageId: "noNewNonconstructor",
54
+ data: { name: nonConstructorName }
55
+ });
56
+ }
57
+ });
58
+ }
59
+ }
60
+ }
61
+ };
62
+
63
+ }
64
+ };
@@ -16,7 +16,7 @@ const getPropertyName = require("./utils/ast-utils").getStaticPropertyName;
16
16
  // Helpers
17
17
  //------------------------------------------------------------------------------
18
18
 
19
- const nonCallableGlobals = ["Atomics", "JSON", "Math", "Reflect"];
19
+ const nonCallableGlobals = ["Atomics", "JSON", "Math", "Reflect", "Intl"];
20
20
 
21
21
  /**
22
22
  * Returns the name of the node to report
@@ -247,7 +247,7 @@ module.exports = {
247
247
 
248
248
  docs: {
249
249
  description:
250
- "Disallow using Object.assign with an object literal as the first argument and prefer the use of object spread instead.",
250
+ "Disallow using Object.assign with an object literal as the first argument and prefer the use of object spread instead",
251
251
  recommended: false,
252
252
  url: "https://eslint.org/docs/rules/prefer-object-spread"
253
253
  },
@@ -248,14 +248,14 @@ module.exports = {
248
248
 
249
249
  /**
250
250
  * Returns a ecmaVersion compatible for regexpp.
251
- * @param {any} ecmaVersion The ecmaVersion to convert.
251
+ * @param {number} ecmaVersion The ecmaVersion to convert.
252
252
  * @returns {import("regexpp/ecma-versions").EcmaVersion} The resulting ecmaVersion compatible for regexpp.
253
253
  */
254
254
  function getRegexppEcmaVersion(ecmaVersion) {
255
- if (typeof ecmaVersion !== "number" || ecmaVersion <= 5) {
255
+ if (ecmaVersion <= 5) {
256
256
  return 5;
257
257
  }
258
- return Math.min(ecmaVersion + 2009, REGEXPP_LATEST_ECMA_VERSION);
258
+ return Math.min(ecmaVersion, REGEXPP_LATEST_ECMA_VERSION);
259
259
  }
260
260
 
261
261
  /**
@@ -320,7 +320,7 @@ module.exports = {
320
320
  flags = getStringValue(node.arguments[1]);
321
321
  }
322
322
 
323
- const regexppEcmaVersion = getRegexppEcmaVersion(context.parserOptions.ecmaVersion);
323
+ const regexppEcmaVersion = getRegexppEcmaVersion(context.languageOptions.ecmaVersion);
324
324
  const RegExpValidatorInstance = new RegExpValidator({ ecmaVersion: regexppEcmaVersion });
325
325
 
326
326
  try {
@@ -97,7 +97,7 @@ function environment() {
97
97
  */
98
98
  function getNpmPackageVersion(pkg, { global = false } = {}) {
99
99
  const npmBinArgs = ["bin", "-g"];
100
- const npmLsArgs = ["ls", "--depth=0", "--json", "eslint"];
100
+ const npmLsArgs = ["ls", "--depth=0", "--json", pkg];
101
101
 
102
102
  if (global) {
103
103
  npmLsArgs.push("-g");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint",
3
- "version": "8.26.0",
3
+ "version": "8.28.0",
4
4
  "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
5
5
  "description": "An AST-based pattern checker for JavaScript.",
6
6
  "bin": {