eslint 8.19.0 → 8.22.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.
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
 
3
3
  const { ESLint } = require("./eslint");
4
+ const { FlatESLint } = require("./flat-eslint");
4
5
 
5
6
  module.exports = {
6
- ESLint
7
+ ESLint,
8
+ FlatESLint
7
9
  };
@@ -1510,7 +1510,31 @@ class Linter {
1510
1510
  options.filterCodeBlock ||
1511
1511
  (blockFilename => blockFilename.endsWith(".js"));
1512
1512
  const originalExtname = path.extname(filename);
1513
- const messageLists = preprocess(text, filenameToExpose).map((block, i) => {
1513
+
1514
+ let blocks;
1515
+
1516
+ try {
1517
+ blocks = preprocess(text, filenameToExpose);
1518
+ } catch (ex) {
1519
+
1520
+ // If the message includes a leading line number, strip it:
1521
+ const message = `Preprocessing error: ${ex.message.replace(/^line \d+:/iu, "").trim()}`;
1522
+
1523
+ debug("%s\n%s", message, ex.stack);
1524
+
1525
+ return [
1526
+ {
1527
+ ruleId: null,
1528
+ fatal: true,
1529
+ severity: 2,
1530
+ message,
1531
+ line: ex.lineNumber,
1532
+ column: ex.column
1533
+ }
1534
+ ];
1535
+ }
1536
+
1537
+ const messageLists = blocks.map((block, i) => {
1514
1538
  debug("A code block was found: %o", block.filename || "(unnamed)");
1515
1539
 
1516
1540
  // Keep the legacy behavior.
@@ -1584,6 +1608,11 @@ class Linter {
1584
1608
  ...languageOptions.globals
1585
1609
  };
1586
1610
 
1611
+ // double check that there is a parser to avoid mysterious error messages
1612
+ if (!languageOptions.parser) {
1613
+ throw new TypeError(`No parser specified for ${options.filename}`);
1614
+ }
1615
+
1587
1616
  // Espree expects this information to be passed in
1588
1617
  if (isEspree(languageOptions.parser)) {
1589
1618
  const parserOptions = languageOptions.parserOptions;
@@ -1746,12 +1775,24 @@ class Linter {
1746
1775
  debug("With flat config: %s", options.filename);
1747
1776
 
1748
1777
  // we need a filename to match configs against
1749
- const filename = options.filename || "<input>";
1778
+ const filename = options.filename || "__placeholder__.js";
1750
1779
 
1751
1780
  // Store the config array in order to get plugin envs and rules later.
1752
1781
  internalSlotsMap.get(this).lastConfigArray = configArray;
1753
1782
  const config = configArray.getConfig(filename);
1754
1783
 
1784
+ if (!config) {
1785
+ return [
1786
+ {
1787
+ ruleId: null,
1788
+ severity: 1,
1789
+ message: `No matching configuration found for ${filename}.`,
1790
+ line: 0,
1791
+ column: 0
1792
+ }
1793
+ ];
1794
+ }
1795
+
1755
1796
  // Verify.
1756
1797
  if (config.processor) {
1757
1798
  debug("Apply the processor: %o", config.processor);
@@ -1788,13 +1829,36 @@ class Linter {
1788
1829
  const physicalFilename = options.physicalFilename || filenameToExpose;
1789
1830
  const text = ensureText(textOrSourceCode);
1790
1831
  const preprocess = options.preprocess || (rawText => [rawText]);
1791
-
1792
1832
  const postprocess = options.postprocess || (messagesList => messagesList.flat());
1793
1833
  const filterCodeBlock =
1794
1834
  options.filterCodeBlock ||
1795
1835
  (blockFilename => blockFilename.endsWith(".js"));
1796
1836
  const originalExtname = path.extname(filename);
1797
- const messageLists = preprocess(text, filenameToExpose).map((block, i) => {
1837
+
1838
+ let blocks;
1839
+
1840
+ try {
1841
+ blocks = preprocess(text, filenameToExpose);
1842
+ } catch (ex) {
1843
+
1844
+ // If the message includes a leading line number, strip it:
1845
+ const message = `Preprocessing error: ${ex.message.replace(/^line \d+:/iu, "").trim()}`;
1846
+
1847
+ debug("%s\n%s", message, ex.stack);
1848
+
1849
+ return [
1850
+ {
1851
+ ruleId: null,
1852
+ fatal: true,
1853
+ severity: 2,
1854
+ message,
1855
+ line: ex.lineNumber,
1856
+ column: ex.column
1857
+ }
1858
+ ];
1859
+ }
1860
+
1861
+ const messageLists = blocks.map((block, i) => {
1798
1862
  debug("A code block was found: %o", block.filename || "(unnamed)");
1799
1863
 
1800
1864
  // Keep the legacy behavior.
@@ -480,51 +480,54 @@ class FlatRuleTester {
480
480
  ].concat(scenarioErrors).join("\n"));
481
481
  }
482
482
 
483
- const baseConfig = {
484
- plugins: {
485
-
486
- // copy root plugin over
487
- "@": {
488
-
489
- /*
490
- * Parsers are wrapped to detect more errors, so this needs
491
- * to be a new object for each call to run(), otherwise the
492
- * parsers will be wrapped multiple times.
493
- */
494
- parsers: {
495
- ...defaultConfig[0].plugins["@"].parsers
496
- },
483
+ const baseConfig = [
484
+ {
485
+ plugins: {
497
486
 
498
- /*
499
- * The rules key on the default plugin is a proxy to lazy-load
500
- * just the rules that are needed. So, don't create a new object
501
- * here, just use the default one to keep that performance
502
- * enhancement.
503
- */
504
- rules: defaultConfig[0].plugins["@"].rules
505
- },
506
- "rule-to-test": {
507
- rules: {
508
- [ruleName]: Object.assign({}, rule, {
487
+ // copy root plugin over
488
+ "@": {
489
+
490
+ /*
491
+ * Parsers are wrapped to detect more errors, so this needs
492
+ * to be a new object for each call to run(), otherwise the
493
+ * parsers will be wrapped multiple times.
494
+ */
495
+ parsers: {
496
+ ...defaultConfig[0].plugins["@"].parsers
497
+ },
498
+
499
+ /*
500
+ * The rules key on the default plugin is a proxy to lazy-load
501
+ * just the rules that are needed. So, don't create a new object
502
+ * here, just use the default one to keep that performance
503
+ * enhancement.
504
+ */
505
+ rules: defaultConfig[0].plugins["@"].rules
506
+ },
507
+ "rule-to-test": {
508
+ rules: {
509
+ [ruleName]: Object.assign({}, rule, {
509
510
 
510
- // Create a wrapper rule that freezes the `context` properties.
511
- create(context) {
512
- freezeDeeply(context.options);
513
- freezeDeeply(context.settings);
514
- freezeDeeply(context.parserOptions);
511
+ // Create a wrapper rule that freezes the `context` properties.
512
+ create(context) {
513
+ freezeDeeply(context.options);
514
+ freezeDeeply(context.settings);
515
+ freezeDeeply(context.parserOptions);
515
516
 
516
- // freezeDeeply(context.languageOptions);
517
+ // freezeDeeply(context.languageOptions);
517
518
 
518
- return (typeof rule === "function" ? rule : rule.create)(context);
519
- }
520
- })
519
+ return (typeof rule === "function" ? rule : rule.create)(context);
520
+ }
521
+ })
522
+ }
521
523
  }
524
+ },
525
+ languageOptions: {
526
+ ...defaultConfig[0].languageOptions
522
527
  }
523
528
  },
524
- languageOptions: {
525
- ...defaultConfig[0].languageOptions
526
- }
527
- };
529
+ ...defaultConfig.slice(1)
530
+ ];
528
531
 
529
532
  /**
530
533
  * Run the rule for the given item
@@ -305,6 +305,36 @@ function getCommentsDeprecation() {
305
305
  );
306
306
  }
307
307
 
308
+ /**
309
+ * Emit a deprecation warning if function-style format is being used.
310
+ * @param {string} ruleName Name of the rule.
311
+ * @returns {void}
312
+ */
313
+ function emitLegacyRuleAPIWarning(ruleName) {
314
+ if (!emitLegacyRuleAPIWarning[`warned-${ruleName}`]) {
315
+ emitLegacyRuleAPIWarning[`warned-${ruleName}`] = true;
316
+ process.emitWarning(
317
+ `"${ruleName}" rule is using the deprecated function-style format and will stop working in ESLint v9. Please use object-style format: https://eslint.org/docs/developer-guide/working-with-rules`,
318
+ "DeprecationWarning"
319
+ );
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Emit a deprecation warning if rule has options but is missing the "meta.schema" property
325
+ * @param {string} ruleName Name of the rule.
326
+ * @returns {void}
327
+ */
328
+ function emitMissingSchemaWarning(ruleName) {
329
+ if (!emitMissingSchemaWarning[`warned-${ruleName}`]) {
330
+ emitMissingSchemaWarning[`warned-${ruleName}`] = true;
331
+ process.emitWarning(
332
+ `"${ruleName}" rule has options but is missing the "meta.schema" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/developer-guide/working-with-rules#options-schemas`,
333
+ "DeprecationWarning"
334
+ );
335
+ }
336
+ }
337
+
308
338
  //------------------------------------------------------------------------------
309
339
  // Public Interface
310
340
  //------------------------------------------------------------------------------
@@ -521,6 +551,9 @@ class RuleTester {
521
551
  ].concat(scenarioErrors).join("\n"));
522
552
  }
523
553
 
554
+ if (typeof rule === "function") {
555
+ emitLegacyRuleAPIWarning(ruleName);
556
+ }
524
557
 
525
558
  linter.defineRule(ruleName, Object.assign({}, rule, {
526
559
 
@@ -578,6 +611,15 @@ class RuleTester {
578
611
 
579
612
  if (hasOwnProperty(item, "options")) {
580
613
  assert(Array.isArray(item.options), "options must be an array");
614
+ if (
615
+ item.options.length > 0 &&
616
+ typeof rule === "object" &&
617
+ (
618
+ !rule.meta || (rule.meta && (typeof rule.meta.schema === "undefined" || rule.meta.schema === null))
619
+ )
620
+ ) {
621
+ emitMissingSchemaWarning(ruleName);
622
+ }
581
623
  config.rules[ruleName] = [1].concat(item.options);
582
624
  } else {
583
625
  config.rules[ruleName] = 1;
@@ -103,38 +103,6 @@ module.exports = {
103
103
  });
104
104
  }
105
105
 
106
- /**
107
- * Validates the spacing around a comma token.
108
- * @param {Object} tokens The tokens to be validated.
109
- * @param {Token} tokens.comma The token representing the comma.
110
- * @param {Token} [tokens.left] The last token before the comma.
111
- * @param {Token} [tokens.right] The first token after the comma.
112
- * @param {Token|ASTNode} reportItem The item to use when reporting an error.
113
- * @returns {void}
114
- * @private
115
- */
116
- function validateCommaItemSpacing(tokens, reportItem) {
117
- if (tokens.left && astUtils.isTokenOnSameLine(tokens.left, tokens.comma) &&
118
- (options.before !== sourceCode.isSpaceBetweenTokens(tokens.left, tokens.comma))
119
- ) {
120
- report(reportItem, "before", tokens.left);
121
- }
122
-
123
- if (tokens.right && astUtils.isClosingParenToken(tokens.right)) {
124
- return;
125
- }
126
-
127
- if (tokens.right && !options.after && tokens.right.type === "Line") {
128
- return;
129
- }
130
-
131
- if (tokens.right && astUtils.isTokenOnSameLine(tokens.comma, tokens.right) &&
132
- (options.after !== sourceCode.isSpaceBetweenTokens(tokens.comma, tokens.right))
133
- ) {
134
- report(reportItem, "after", tokens.right);
135
- }
136
- }
137
-
138
106
  /**
139
107
  * Adds null elements of the given ArrayExpression or ArrayPattern node to the ignore list.
140
108
  * @param {ASTNode} node An ArrayExpression or ArrayPattern node.
@@ -172,18 +140,44 @@ module.exports = {
172
140
  return;
173
141
  }
174
142
 
175
- if (token && token.type === "JSXText") {
176
- return;
177
- }
178
-
179
143
  const previousToken = tokensAndComments[i - 1];
180
144
  const nextToken = tokensAndComments[i + 1];
181
145
 
182
- validateCommaItemSpacing({
183
- comma: token,
184
- left: astUtils.isCommaToken(previousToken) || commaTokensToIgnore.includes(token) ? null : previousToken,
185
- right: astUtils.isCommaToken(nextToken) ? null : nextToken
186
- }, token);
146
+ if (
147
+ previousToken &&
148
+ !astUtils.isCommaToken(previousToken) && // ignore spacing between two commas
149
+
150
+ /*
151
+ * `commaTokensToIgnore` are ending commas of `null` elements (array holes/elisions).
152
+ * In addition to spacing between two commas, this can also ignore:
153
+ *
154
+ * - Spacing after `[` (controlled by array-bracket-spacing)
155
+ * Example: [ , ]
156
+ * ^
157
+ * - Spacing after a comment (for backwards compatibility, this was possibly unintentional)
158
+ * Example: [a, /* * / ,]
159
+ * ^
160
+ */
161
+ !commaTokensToIgnore.includes(token) &&
162
+
163
+ astUtils.isTokenOnSameLine(previousToken, token) &&
164
+ options.before !== sourceCode.isSpaceBetweenTokens(previousToken, token)
165
+ ) {
166
+ report(token, "before", previousToken);
167
+ }
168
+
169
+ if (
170
+ nextToken &&
171
+ !astUtils.isCommaToken(nextToken) && // ignore spacing between two commas
172
+ !astUtils.isClosingParenToken(nextToken) && // controlled by space-in-parens
173
+ !astUtils.isClosingBracketToken(nextToken) && // controlled by array-bracket-spacing
174
+ !astUtils.isClosingBraceToken(nextToken) && // controlled by object-curly-spacing
175
+ !(!options.after && nextToken.type === "Line") && // special case, allow space before line comment
176
+ astUtils.isTokenOnSameLine(token, nextToken) &&
177
+ options.after !== sourceCode.isSpaceBetweenTokens(token, nextToken)
178
+ ) {
179
+ report(token, "after", nextToken);
180
+ }
187
181
  });
188
182
  },
189
183
  ArrayExpression: addNullElementsToIgnoreList,
@@ -9,6 +9,9 @@
9
9
  //------------------------------------------------------------------------------
10
10
 
11
11
  const astUtils = require("./utils/ast-utils");
12
+ const GraphemeSplitter = require("grapheme-splitter");
13
+
14
+ const splitter = new GraphemeSplitter();
12
15
 
13
16
  //------------------------------------------------------------------------------
14
17
  // Helpers
@@ -508,7 +511,7 @@ module.exports = {
508
511
  const startToken = sourceCode.getFirstToken(property);
509
512
  const endToken = getLastTokenBeforeColon(property.key);
510
513
 
511
- return endToken.range[1] - startToken.range[0];
514
+ return splitter.countGraphemes(sourceCode.getText().slice(startToken.range[0], endToken.range[1]));
512
515
  }
513
516
 
514
517
  /**
@@ -231,9 +231,15 @@ module.exports = {
231
231
  const parent = getParentNodeOfToken(token);
232
232
 
233
233
  if (parent && isParentNodeType(parent, nodeType)) {
234
- const parentStartNodeOrToken = parent.type === "StaticBlock"
235
- ? sourceCode.getFirstToken(parent, { skip: 1 }) // opening brace of the static block
236
- : parent;
234
+ let parentStartNodeOrToken = parent;
235
+
236
+ if (parent.type === "StaticBlock") {
237
+ parentStartNodeOrToken = sourceCode.getFirstToken(parent, { skip: 1 }); // opening brace of the static block
238
+ } else if (parent.type === "SwitchStatement") {
239
+ parentStartNodeOrToken = sourceCode.getTokenAfter(parent.discriminant, {
240
+ filter: astUtils.isOpeningBraceToken
241
+ }); // opening brace of the switch statement
242
+ }
237
243
 
238
244
  return token.loc.start.line - parentStartNodeOrToken.loc.start.line === 1;
239
245
  }
@@ -264,7 +270,8 @@ module.exports = {
264
270
  isCommentAtParentStart(token, "ClassBody") ||
265
271
  isCommentAtParentStart(token, "BlockStatement") ||
266
272
  isCommentAtParentStart(token, "StaticBlock") ||
267
- isCommentAtParentStart(token, "SwitchCase")
273
+ isCommentAtParentStart(token, "SwitchCase") ||
274
+ isCommentAtParentStart(token, "SwitchStatement")
268
275
  );
269
276
  }
270
277
 
@@ -64,59 +64,45 @@ module.exports = {
64
64
  */
65
65
  function convertToRegExp(term) {
66
66
  const escaped = escapeRegExp(term);
67
- const wordBoundary = "\\b";
68
- const eitherOrWordBoundary = `|${wordBoundary}`;
69
- let prefix;
70
67
 
71
68
  /*
72
- * If the term ends in a word character (a-z0-9_), ensure a word
73
- * boundary at the end, so that substrings do not get falsely
74
- * matched. eg "todo" in a string such as "mastodon".
75
- * If the term ends in a non-word character, then \b won't match on
76
- * the boundary to the next non-word character, which would likely
77
- * be a space. For example `/\bFIX!\b/.test('FIX! blah') === false`.
78
- * In these cases, use no bounding match. Same applies for the
79
- * prefix, handled below.
69
+ * When matching at the start, ignore leading whitespace, and
70
+ * there's no need to worry about word boundaries.
71
+ *
72
+ * These expressions for the prefix and suffix are designed as follows:
73
+ * ^ handles any terms at the beginning of a comment.
74
+ * e.g. terms ["TODO"] matches `//TODO something`
75
+ * $ handles any terms at the end of a comment
76
+ * e.g. terms ["TODO"] matches `// something TODO`
77
+ * \s* handles optional leading spaces (for "start" location only)
78
+ * e.g. terms ["TODO"] matches `// TODO something`
79
+ * \b handles terms preceded/followed by word boundary
80
+ * e.g. terms: ["!FIX", "FIX!"] matches `// FIX!something` or `// something!FIX`
81
+ * terms: ["FIX"] matches `// FIX!` or `// !FIX`, but not `// fixed or affix`
80
82
  */
81
- const suffix = /\w$/u.test(term) ? "\\b" : "";
83
+ const wordBoundary = "\\b";
82
84
 
83
- if (location === "start") {
85
+ let prefix = "";
84
86
 
85
- /*
86
- * When matching at the start, ignore leading whitespace, and
87
- * there's no need to worry about word boundaries.
88
- */
87
+ if (location === "start") {
89
88
  prefix = "^\\s*";
90
89
  } else if (/^\w/u.test(term)) {
91
90
  prefix = wordBoundary;
92
- } else {
93
- prefix = "";
94
91
  }
95
92
 
96
- if (location === "start") {
97
-
98
- /*
99
- * For location "start" the regex should be
100
- * ^\s*TERM\b. This checks the word boundary
101
- * at the beginning of the comment.
102
- */
103
- return new RegExp(prefix + escaped + suffix, "iu");
104
- }
93
+ const suffix = /\w$/u.test(term) ? wordBoundary : "";
94
+ const flags = "iu"; // Case-insensitive with Unicode case folding.
105
95
 
106
96
  /*
107
- * For location "anywhere" the regex should be
108
- * \bTERM\b|\bTERM\b, this checks the entire comment
109
- * for the term.
97
+ * For location "start", the typical regex is:
98
+ * /^\s*ESCAPED_TERM\b/iu.
99
+ *
100
+ * For location "anywhere" the typical regex is
101
+ * /\bESCAPED_TERM\b/iu
102
+ *
103
+ * If it starts or ends with non-word character, the prefix and suffix empty, respectively.
110
104
  */
111
- return new RegExp(
112
- prefix +
113
- escaped +
114
- suffix +
115
- eitherOrWordBoundary +
116
- term +
117
- wordBoundary,
118
- "iu"
119
- );
105
+ return new RegExp(`${prefix}${escaped}${suffix}`, flags);
120
106
  }
121
107
 
122
108
  const warningRegExps = warningTerms.map(convertToRegExp);
@@ -78,6 +78,9 @@ module.exports = {
78
78
  ignoreConstructors: {
79
79
  type: "boolean"
80
80
  },
81
+ methodsIgnorePattern: {
82
+ type: "string"
83
+ },
81
84
  avoidQuotes: {
82
85
  type: "boolean"
83
86
  },
@@ -115,6 +118,9 @@ module.exports = {
115
118
 
116
119
  const PARAMS = context.options[1] || {};
117
120
  const IGNORE_CONSTRUCTORS = PARAMS.ignoreConstructors;
121
+ const METHODS_IGNORE_PATTERN = PARAMS.methodsIgnorePattern
122
+ ? new RegExp(PARAMS.methodsIgnorePattern, "u")
123
+ : null;
118
124
  const AVOID_QUOTES = PARAMS.avoidQuotes;
119
125
  const AVOID_EXPLICIT_RETURN_ARROWS = !!PARAMS.avoidExplicitReturnArrows;
120
126
  const sourceCode = context.getSourceCode();
@@ -457,6 +463,15 @@ module.exports = {
457
463
  if (IGNORE_CONSTRUCTORS && node.key.type === "Identifier" && isConstructor(node.key.name)) {
458
464
  return;
459
465
  }
466
+
467
+ if (METHODS_IGNORE_PATTERN) {
468
+ const propertyName = astUtils.getStaticPropertyName(node);
469
+
470
+ if (propertyName !== null && METHODS_IGNORE_PATTERN.test(propertyName)) {
471
+ return;
472
+ }
473
+ }
474
+
460
475
  if (AVOID_QUOTES && isStringLiteral(node.key)) {
461
476
  return;
462
477
  }
@@ -105,6 +105,10 @@ module.exports = {
105
105
  type: "integer",
106
106
  minimum: 2,
107
107
  default: 2
108
+ },
109
+ allowLineSeparatedGroups: {
110
+ type: "boolean",
111
+ default: false
108
112
  }
109
113
  },
110
114
  additionalProperties: false
@@ -124,17 +128,21 @@ module.exports = {
124
128
  const insensitive = options && options.caseSensitive === false;
125
129
  const natural = options && options.natural;
126
130
  const minKeys = options && options.minKeys;
131
+ const allowLineSeparatedGroups = options && options.allowLineSeparatedGroups || false;
127
132
  const isValidOrder = isValidOrders[
128
133
  order + (insensitive ? "I" : "") + (natural ? "N" : "")
129
134
  ];
130
135
 
131
136
  // The stack to save the previous property's name for each object literals.
132
137
  let stack = null;
138
+ const sourceCode = context.getSourceCode();
133
139
 
134
140
  return {
135
141
  ObjectExpression(node) {
136
142
  stack = {
137
143
  upper: stack,
144
+ prevNode: null,
145
+ prevBlankLine: false,
138
146
  prevName: null,
139
147
  numKeys: node.properties.length
140
148
  };
@@ -159,10 +167,45 @@ module.exports = {
159
167
  const numKeys = stack.numKeys;
160
168
  const thisName = getPropertyName(node);
161
169
 
170
+ // Get tokens between current node and previous node
171
+ const tokens = stack.prevNode && sourceCode
172
+ .getTokensBetween(stack.prevNode, node, { includeComments: true });
173
+
174
+ let isBlankLineBetweenNodes = stack.prevBlankLine;
175
+
176
+ if (tokens) {
177
+
178
+ // check blank line between tokens
179
+ tokens.forEach((token, index) => {
180
+ const previousToken = tokens[index - 1];
181
+
182
+ if (previousToken && (token.loc.start.line - previousToken.loc.end.line > 1)) {
183
+ isBlankLineBetweenNodes = true;
184
+ }
185
+ });
186
+
187
+ // check blank line between the current node and the last token
188
+ if (!isBlankLineBetweenNodes && (node.loc.start.line - tokens[tokens.length - 1].loc.end.line > 1)) {
189
+ isBlankLineBetweenNodes = true;
190
+ }
191
+
192
+ // check blank line between the first token and the previous node
193
+ if (!isBlankLineBetweenNodes && (tokens[0].loc.start.line - stack.prevNode.loc.end.line > 1)) {
194
+ isBlankLineBetweenNodes = true;
195
+ }
196
+ }
197
+
198
+ stack.prevNode = node;
199
+
162
200
  if (thisName !== null) {
163
201
  stack.prevName = thisName;
164
202
  }
165
203
 
204
+ if (allowLineSeparatedGroups && isBlankLineBetweenNodes) {
205
+ stack.prevBlankLine = thisName === null;
206
+ return;
207
+ }
208
+
166
209
  if (prevName === null || thisName === null || numKeys < minKeys) {
167
210
  return;
168
211
  }
@@ -12,6 +12,8 @@
12
12
  //-----------------------------------------------------------------------------
13
13
 
14
14
  const { FileEnumerator } = require("./cli-engine/file-enumerator");
15
+ const { FlatESLint } = require("./eslint/flat-eslint");
16
+ const FlatRuleTester = require("./rule-tester/flat-rule-tester");
15
17
 
16
18
  //-----------------------------------------------------------------------------
17
19
  // Exports
@@ -19,5 +21,7 @@ const { FileEnumerator } = require("./cli-engine/file-enumerator");
19
21
 
20
22
  module.exports = {
21
23
  builtinRules: require("./rules"),
24
+ FlatESLint,
25
+ FlatRuleTester,
22
26
  FileEnumerator
23
27
  };