@sgfe/eslint-plugin-sg 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. package/LICENSE.md +25 -0
  2. package/README.md +188 -0
  3. package/configs/all-type-checked.js +10 -0
  4. package/configs/all.js +11 -0
  5. package/configs/recommended.js +11 -0
  6. package/configs/rules-recommended.js +11 -0
  7. package/configs/rules.js +11 -0
  8. package/configs/tests-recommended.js +11 -0
  9. package/configs/tests.js +11 -0
  10. package/lib/index.js +90 -0
  11. package/lib/rules/consistent-output.js +70 -0
  12. package/lib/rules/fixer-return.js +170 -0
  13. package/lib/rules/meta-property-ordering.js +108 -0
  14. package/lib/rules/no-deprecated-context-methods.js +98 -0
  15. package/lib/rules/no-deprecated-report-api.js +83 -0
  16. package/lib/rules/no-identical-tests.js +87 -0
  17. package/lib/rules/no-missing-message-ids.js +101 -0
  18. package/lib/rules/no-missing-placeholders.js +131 -0
  19. package/lib/rules/no-only-tests.js +99 -0
  20. package/lib/rules/no-property-in-node.js +86 -0
  21. package/lib/rules/no-unused-message-ids.js +139 -0
  22. package/lib/rules/no-unused-placeholders.js +127 -0
  23. package/lib/rules/no-useless-token-range.js +174 -0
  24. package/lib/rules/prefer-message-ids.js +109 -0
  25. package/lib/rules/prefer-object-rule.js +83 -0
  26. package/lib/rules/prefer-output-null.js +77 -0
  27. package/lib/rules/prefer-placeholders.js +102 -0
  28. package/lib/rules/prefer-replace-text.js +91 -0
  29. package/lib/rules/report-message-format.js +133 -0
  30. package/lib/rules/require-meta-docs-description.js +110 -0
  31. package/lib/rules/require-meta-docs-url.js +175 -0
  32. package/lib/rules/require-meta-fixable.js +137 -0
  33. package/lib/rules/require-meta-has-suggestions.js +168 -0
  34. package/lib/rules/require-meta-schema.js +162 -0
  35. package/lib/rules/require-meta-type.js +77 -0
  36. package/lib/rules/test-case-property-ordering.js +107 -0
  37. package/lib/rules/test-case-shorthand-strings.js +124 -0
  38. package/lib/utils.js +936 -0
  39. package/package.json +76 -0
package/lib/utils.js ADDED
@@ -0,0 +1,936 @@
1
+ 'use strict';
2
+
3
+ const { getStaticValue, findVariable } = require('eslint-utils');
4
+ const estraverse = require('estraverse');
5
+
6
+ const functionTypes = new Set([
7
+ 'FunctionExpression',
8
+ 'ArrowFunctionExpression',
9
+ 'FunctionDeclaration',
10
+ ]);
11
+
12
+ /**
13
+ * Determines whether a node is a 'normal' (i.e. non-async, non-generator) function expression.
14
+ * @param {ASTNode} node The node in question
15
+ * @returns {boolean} `true` if the node is a normal function expression
16
+ */
17
+ function isNormalFunctionExpression(node) {
18
+ return functionTypes.has(node.type) && !node.generator && !node.async;
19
+ }
20
+
21
+ /**
22
+ * Determines whether a node is constructing a RuleTester instance
23
+ * @param {ASTNode} node The node in question
24
+ * @returns {boolean} `true` if the node is probably constructing a RuleTester instance
25
+ */
26
+ function isRuleTesterConstruction(node) {
27
+ return (
28
+ node.type === 'NewExpression' &&
29
+ ((node.callee.type === 'Identifier' && node.callee.name === 'RuleTester') ||
30
+ (node.callee.type === 'MemberExpression' &&
31
+ node.callee.property.type === 'Identifier' &&
32
+ node.callee.property.name === 'RuleTester'))
33
+ );
34
+ }
35
+
36
+ const INTERESTING_RULE_KEYS = new Set(['create', 'meta']);
37
+
38
+ /**
39
+ * Collect properties from an object that have interesting key names into a new object
40
+ * @param {Node[]} properties
41
+ * @param {Set<String>} interestingKeys
42
+ * @returns Object
43
+ */
44
+ function collectInterestingProperties(properties, interestingKeys) {
45
+ return properties.reduce((parsedProps, prop) => {
46
+ const keyValue = module.exports.getKeyName(prop);
47
+ if (interestingKeys.has(keyValue)) {
48
+ // In TypeScript, unwrap any usage of `{} as const`.
49
+ parsedProps[keyValue] =
50
+ prop.value.type === 'TSAsExpression'
51
+ ? prop.value.expression
52
+ : prop.value;
53
+ }
54
+ return parsedProps;
55
+ }, {});
56
+ }
57
+
58
+ /**
59
+ * Check if there is a return statement that returns an object somewhere inside the given node.
60
+ * @param {Node} node
61
+ * @returns {boolean}
62
+ */
63
+ function hasObjectReturn(node) {
64
+ let foundMatch = false;
65
+ estraverse.traverse(node, {
66
+ enter(child) {
67
+ if (
68
+ child.type === 'ReturnStatement' &&
69
+ child.argument &&
70
+ child.argument.type === 'ObjectExpression'
71
+ ) {
72
+ foundMatch = true;
73
+ }
74
+ },
75
+ fallback: 'iteration', // Don't crash on unexpected node types.
76
+ });
77
+ return foundMatch;
78
+ }
79
+
80
+ /**
81
+ * Determine if the given node is likely to be a function-style rule.
82
+ * @param {*} node
83
+ * @returns {boolean}
84
+ */
85
+ function isFunctionRule(node) {
86
+ return (
87
+ isNormalFunctionExpression(node) && // Is a function definition.
88
+ node.params.length === 1 && // The function has a single `context` argument.
89
+ hasObjectReturn(node) // Returns an object containing the visitor functions.
90
+ );
91
+ }
92
+
93
+ /**
94
+ * Check if the given node is a function call representing a known TypeScript rule creator format.
95
+ * @param {Node} node
96
+ * @returns {boolean}
97
+ */
98
+ function isTypeScriptRuleHelper(node) {
99
+ return (
100
+ node.type === 'CallExpression' &&
101
+ node.arguments.length === 1 &&
102
+ node.arguments[0].type === 'ObjectExpression' &&
103
+ // Check various TypeScript rule helper formats.
104
+ // createESLintRule({ ... })
105
+ (node.callee.type === 'Identifier' ||
106
+ // util.createRule({ ... })
107
+ (node.callee.type === 'MemberExpression' &&
108
+ node.callee.object.type === 'Identifier' &&
109
+ node.callee.property.type === 'Identifier') ||
110
+ // ESLintUtils.RuleCreator(docsUrl)({ ... })
111
+ (node.callee.type === 'CallExpression' &&
112
+ node.callee.callee.type === 'MemberExpression' &&
113
+ node.callee.callee.object.type === 'Identifier' &&
114
+ node.callee.callee.property.type === 'Identifier'))
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Helper for `getRuleInfo`. Handles ESM and TypeScript rules.
120
+ */
121
+ function getRuleExportsESM(ast, scopeManager) {
122
+ const possibleNodes = [];
123
+
124
+ for (const statement of ast.body) {
125
+ switch (statement.type) {
126
+ // export default rule;
127
+ case 'ExportDefaultDeclaration': {
128
+ possibleNodes.push(statement.declaration);
129
+ break;
130
+ }
131
+ // export = rule;
132
+ case 'TSExportAssignment': {
133
+ possibleNodes.push(statement.expression);
134
+ break;
135
+ }
136
+ // export const rule = { ... };
137
+ // or export {rule};
138
+ case 'ExportNamedDeclaration': {
139
+ for (const specifier of statement.specifiers) {
140
+ possibleNodes.push(specifier.local);
141
+ }
142
+ if (statement.declaration) {
143
+ const nodes =
144
+ statement.declaration.type === 'VariableDeclaration'
145
+ ? statement.declaration.declarations.map(
146
+ (declarator) => declarator.init
147
+ )
148
+ : [statement.declaration];
149
+
150
+ // named exports like `export const rule = { ... };`
151
+ // skip if it's function-style to avoid false positives
152
+ // refs: https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/450
153
+ possibleNodes.push(
154
+ ...nodes.filter((node) => node && !functionTypes.has(node.type))
155
+ );
156
+ }
157
+ break;
158
+ }
159
+ }
160
+ }
161
+
162
+ return possibleNodes.reduce((currentExports, node) => {
163
+ if (node.type === 'ObjectExpression') {
164
+ // Check `export default { create() {}, meta: {} }`
165
+ return collectInterestingProperties(
166
+ node.properties,
167
+ INTERESTING_RULE_KEYS
168
+ );
169
+ } else if (isFunctionRule(node)) {
170
+ // Check `export default function(context) { return { ... }; }`
171
+ return { create: node, meta: null, isNewStyle: false };
172
+ } else if (isTypeScriptRuleHelper(node)) {
173
+ // Check `export default someTypeScriptHelper({ create() {}, meta: {} });
174
+ return collectInterestingProperties(
175
+ node.arguments[0].properties,
176
+ INTERESTING_RULE_KEYS
177
+ );
178
+ } else if (node.type === 'Identifier') {
179
+ // Rule could be stored in a variable before being exported.
180
+ const possibleRule = findVariableValue(node, scopeManager);
181
+ if (possibleRule) {
182
+ if (possibleRule.type === 'ObjectExpression') {
183
+ // Check `const possibleRule = { ... }; export default possibleRule;
184
+ return collectInterestingProperties(
185
+ possibleRule.properties,
186
+ INTERESTING_RULE_KEYS
187
+ );
188
+ } else if (isFunctionRule(possibleRule)) {
189
+ // Check `const possibleRule = function(context) { return { ... } }; export default possibleRule;`
190
+ return { create: possibleRule, meta: null, isNewStyle: false };
191
+ } else if (isTypeScriptRuleHelper(possibleRule)) {
192
+ // Check `const possibleRule = someTypeScriptHelper({ ... }); export default possibleRule;
193
+ return collectInterestingProperties(
194
+ possibleRule.arguments[0].properties,
195
+ INTERESTING_RULE_KEYS
196
+ );
197
+ }
198
+ }
199
+ }
200
+ return currentExports;
201
+ }, {});
202
+ }
203
+
204
+ /**
205
+ * Helper for `getRuleInfo`. Handles CJS rules.
206
+ */
207
+ function getRuleExportsCJS(ast, scopeManager) {
208
+ let exportsVarOverridden = false;
209
+ let exportsIsFunction = false;
210
+ return ast.body
211
+ .filter((statement) => statement.type === 'ExpressionStatement')
212
+ .map((statement) => statement.expression)
213
+ .filter((expression) => expression.type === 'AssignmentExpression')
214
+ .filter((expression) => expression.left.type === 'MemberExpression')
215
+
216
+ .reduce((currentExports, node) => {
217
+ if (
218
+ node.left.object.type === 'Identifier' &&
219
+ node.left.object.name === 'module' &&
220
+ node.left.property.type === 'Identifier' &&
221
+ node.left.property.name === 'exports'
222
+ ) {
223
+ exportsVarOverridden = true;
224
+ if (isFunctionRule(node.right)) {
225
+ // Check `module.exports = function (context) { return { ... }; }`
226
+
227
+ exportsIsFunction = true;
228
+ return { create: node.right, meta: null, isNewStyle: false };
229
+ } else if (node.right.type === 'ObjectExpression') {
230
+ // Check `module.exports = { create: function () {}, meta: {} }`
231
+
232
+ return collectInterestingProperties(
233
+ node.right.properties,
234
+ INTERESTING_RULE_KEYS
235
+ );
236
+ } else if (node.right.type === 'Identifier') {
237
+ // Rule could be stored in a variable before being exported.
238
+ const possibleRule = findVariableValue(node.right, scopeManager);
239
+ if (possibleRule) {
240
+ if (possibleRule.type === 'ObjectExpression') {
241
+ // Check `const possibleRule = { ... }; module.exports = possibleRule;
242
+ return collectInterestingProperties(
243
+ possibleRule.properties,
244
+ INTERESTING_RULE_KEYS
245
+ );
246
+ } else if (isFunctionRule(possibleRule)) {
247
+ // Check `const possibleRule = function(context) { return { ... } }; module.exports = possibleRule;`
248
+ return { create: possibleRule, meta: null, isNewStyle: false };
249
+ }
250
+ }
251
+ }
252
+ return {};
253
+ } else if (
254
+ !exportsIsFunction &&
255
+ node.left.object.type === 'MemberExpression' &&
256
+ node.left.object.object.type === 'Identifier' &&
257
+ node.left.object.object.name === 'module' &&
258
+ node.left.object.property.type === 'Identifier' &&
259
+ node.left.object.property.name === 'exports' &&
260
+ node.left.property.type === 'Identifier' &&
261
+ INTERESTING_RULE_KEYS.has(node.left.property.name)
262
+ ) {
263
+ // Check `module.exports.create = () => {}`
264
+
265
+ currentExports[node.left.property.name] = node.right;
266
+ } else if (
267
+ !exportsVarOverridden &&
268
+ node.left.object.type === 'Identifier' &&
269
+ node.left.object.name === 'exports' &&
270
+ node.left.property.type === 'Identifier' &&
271
+ INTERESTING_RULE_KEYS.has(node.left.property.name)
272
+ ) {
273
+ // Check `exports.create = () => {}`
274
+
275
+ currentExports[node.left.property.name] = node.right;
276
+ }
277
+ return currentExports;
278
+ }, {});
279
+ }
280
+
281
+ /**
282
+ * Find the value of a property in an object by its property key name.
283
+ * @param {Object} obj
284
+ * @param {String} keyName
285
+ * @returns property value
286
+ */
287
+ function findObjectPropertyValueByKeyName(obj, keyName) {
288
+ const property = obj.properties.find(
289
+ (prop) => prop.key.type === 'Identifier' && prop.key.name === keyName
290
+ );
291
+ return property ? property.value : undefined;
292
+ }
293
+
294
+ /**
295
+ * Get the first value (or function) that a variable is initialized to.
296
+ * @param {Node} node - the Identifier node for the variable.
297
+ * @param {ScopeManager} scopeManager
298
+ * @returns the first value (or function) that the given variable is initialized to.
299
+ */
300
+ function findVariableValue(node, scopeManager) {
301
+ const variable = findVariable(
302
+ scopeManager.acquire(node) || scopeManager.globalScope,
303
+ node
304
+ );
305
+ if (variable && variable.defs && variable.defs[0] && variable.defs[0].node) {
306
+ if (
307
+ variable.defs[0].node.type === 'VariableDeclarator' &&
308
+ variable.defs[0].node.init
309
+ ) {
310
+ // Given node `x`, get `123` from `const x = 123;`.
311
+ return variable.defs[0].node.init;
312
+ } else if (variable.defs[0].node.type === 'FunctionDeclaration') {
313
+ // Given node `foo`, get `function foo() {}` from `function foo() {}`.
314
+ return variable.defs[0].node;
315
+ }
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Retrieve all possible elements from an array.
321
+ * If a ternary conditional expression is involved, retrieve the elements that may exist on both sides of it.
322
+ * Ex: [a, b, c] will return [a, b, c]
323
+ * Ex: foo ? [a, b, c] : [d, e, f] will return [a, b, c, d, e, f]
324
+ * @param {Node} node
325
+ * @returns {Node[]} the list of elements
326
+ */
327
+ function collectArrayElements(node) {
328
+ if (!node) {
329
+ return [];
330
+ }
331
+ if (node.type === 'ArrayExpression') {
332
+ return node.elements;
333
+ }
334
+ if (node.type === 'ConditionalExpression') {
335
+ return [
336
+ ...collectArrayElements(node.consequent),
337
+ ...collectArrayElements(node.alternate),
338
+ ];
339
+ }
340
+ return [];
341
+ }
342
+
343
+ module.exports = {
344
+ /**
345
+ * Performs static analysis on an AST to try to determine the final value of `module.exports`.
346
+ * @param {{ast: ASTNode, scopeManager?: ScopeManager}} sourceCode The object contains `Program` AST node, and optional `scopeManager`
347
+ * @returns {Object} An object with keys `meta`, `create`, and `isNewStyle`. `meta` and `create` correspond to the AST nodes
348
+ for the final values of `module.exports.meta` and `module.exports.create`. `isNewStyle` will be `true` if `module.exports`
349
+ is an object, and `false` if module.exports is just the `create` function. If no valid ESLint rule info can be extracted
350
+ from the file, the return value will be `null`.
351
+ */
352
+ getRuleInfo({ ast, scopeManager }) {
353
+ const exportNodes =
354
+ ast.sourceType === 'module'
355
+ ? getRuleExportsESM(ast, scopeManager)
356
+ : getRuleExportsCJS(ast, scopeManager);
357
+
358
+ const createExists = Object.prototype.hasOwnProperty.call(
359
+ exportNodes,
360
+ 'create'
361
+ );
362
+ if (!createExists) {
363
+ return null;
364
+ }
365
+
366
+ // If create/meta are defined in variables, get their values.
367
+ for (const key of Object.keys(exportNodes)) {
368
+ if (exportNodes[key] && exportNodes[key].type === 'Identifier') {
369
+ const value = findVariableValue(exportNodes[key], scopeManager);
370
+ if (value) {
371
+ exportNodes[key] = value;
372
+ }
373
+ }
374
+ }
375
+
376
+ const createIsFunction = isNormalFunctionExpression(exportNodes.create);
377
+ if (!createIsFunction) {
378
+ return null;
379
+ }
380
+
381
+ return Object.assign({ isNewStyle: true, meta: null }, exportNodes);
382
+ },
383
+
384
+ /**
385
+ * Gets all the identifiers referring to the `context` variable in a rule source file. Note that this function will
386
+ * only work correctly after traversing the AST has started (e.g. in the first `Program` node).
387
+ * @param {RuleContext} scopeManager
388
+ * @param {ASTNode} ast The `Program` node for the file
389
+ * @returns {Set<ASTNode>} A Set of all `Identifier` nodes that are references to the `context` value for the file
390
+ */
391
+ getContextIdentifiers(scopeManager, ast) {
392
+ const ruleInfo = module.exports.getRuleInfo({ ast, scopeManager });
393
+
394
+ if (
395
+ !ruleInfo ||
396
+ ruleInfo.create.params.length === 0 ||
397
+ ruleInfo.create.params[0].type !== 'Identifier'
398
+ ) {
399
+ return new Set();
400
+ }
401
+
402
+ return new Set(
403
+ scopeManager
404
+ .getDeclaredVariables(ruleInfo.create)
405
+ .find((variable) => variable.name === ruleInfo.create.params[0].name)
406
+ .references.map((ref) => ref.identifier)
407
+ );
408
+ },
409
+
410
+ /**
411
+ * Gets the key name of a Property, if it can be determined statically.
412
+ * @param {ASTNode} node The `Property` node
413
+ * @param {Scope} scope
414
+ * @returns {string|null} The key name, or `null` if the name cannot be determined statically.
415
+ */
416
+ getKeyName(property, scope) {
417
+ if (!property.key) {
418
+ // likely a SpreadElement or another non-standard node
419
+ return null;
420
+ }
421
+ if (property.key.type === 'Identifier') {
422
+ if (property.computed) {
423
+ // Variable key: { [myVariable]: 'hello world' }
424
+ if (scope) {
425
+ const staticValue = getStaticValue(property.key, scope);
426
+ return staticValue ? staticValue.value : null;
427
+ }
428
+ // TODO: ensure scope is always passed to getKeyName() so we don't need to handle the case where it's not passed.
429
+ return null;
430
+ }
431
+ return property.key.name;
432
+ }
433
+ if (property.key.type === 'Literal') {
434
+ return '' + property.key.value;
435
+ }
436
+ if (
437
+ property.key.type === 'TemplateLiteral' &&
438
+ property.key.quasis.length === 1
439
+ ) {
440
+ return property.key.quasis[0].value.cooked;
441
+ }
442
+ return null;
443
+ },
444
+
445
+ /**
446
+ * Extracts the body of a function if the given node is a function
447
+ *
448
+ * @param {ASTNode} node
449
+ * @returns {ExpressionStatement[]}
450
+ */
451
+ extractFunctionBody(node) {
452
+ if (
453
+ node.type === 'ArrowFunctionExpression' ||
454
+ node.type === 'FunctionExpression'
455
+ ) {
456
+ if (node.body.type === 'BlockStatement') {
457
+ return node.body.body;
458
+ }
459
+
460
+ return [node.body];
461
+ }
462
+
463
+ return [];
464
+ },
465
+
466
+ /**
467
+ * Checks the given statements for possible test info
468
+ *
469
+ * @param {RuleContext} context The `context` variable for the source file itself
470
+ * @param {ASTNode[]} statements The statements to check
471
+ * @param {Set<ASTNode>} variableIdentifiers
472
+ * @returns {CallExpression[]}
473
+ */
474
+ checkStatementsForTestInfo(
475
+ context,
476
+ statements,
477
+ variableIdentifiers = new Set()
478
+ ) {
479
+ const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
480
+ const runCalls = [];
481
+
482
+ for (const statement of statements) {
483
+ if (statement.type === 'VariableDeclaration') {
484
+ for (const declarator of statement.declarations) {
485
+ if (!declarator.init) {
486
+ continue;
487
+ }
488
+
489
+ const extracted = module.exports.extractFunctionBody(declarator.init);
490
+
491
+ runCalls.push(
492
+ ...module.exports.checkStatementsForTestInfo(
493
+ context,
494
+ extracted,
495
+ variableIdentifiers
496
+ )
497
+ );
498
+
499
+ if (
500
+ isRuleTesterConstruction(declarator.init) &&
501
+ declarator.id.type === 'Identifier'
502
+ ) {
503
+ const vars = sourceCode.getDeclaredVariables
504
+ ? sourceCode.getDeclaredVariables(declarator)
505
+ : context.getDeclaredVariables(declarator);
506
+ vars.forEach((variable) => {
507
+ variable.references
508
+ .filter((ref) => ref.isRead())
509
+ .forEach((ref) => variableIdentifiers.add(ref.identifier));
510
+ });
511
+ }
512
+ }
513
+ }
514
+
515
+ if (statement.type === 'FunctionDeclaration') {
516
+ runCalls.push(
517
+ ...module.exports.checkStatementsForTestInfo(
518
+ context,
519
+ statement.body.body,
520
+ variableIdentifiers
521
+ )
522
+ );
523
+ }
524
+
525
+ if (statement.type === 'IfStatement') {
526
+ const body =
527
+ statement.consequent.type === 'BlockStatement'
528
+ ? statement.consequent.body
529
+ : [statement.consequent];
530
+
531
+ runCalls.push(
532
+ ...module.exports.checkStatementsForTestInfo(
533
+ context,
534
+ body,
535
+ variableIdentifiers
536
+ )
537
+ );
538
+
539
+ continue;
540
+ }
541
+
542
+ const expression =
543
+ statement.type === 'ExpressionStatement'
544
+ ? statement.expression
545
+ : statement;
546
+
547
+ if (expression.type !== 'CallExpression') {
548
+ continue;
549
+ }
550
+
551
+ for (const arg of expression.arguments) {
552
+ const extracted = module.exports.extractFunctionBody(arg);
553
+
554
+ runCalls.push(
555
+ ...module.exports.checkStatementsForTestInfo(
556
+ context,
557
+ extracted,
558
+ variableIdentifiers
559
+ )
560
+ );
561
+ }
562
+
563
+ if (
564
+ expression.callee.type === 'MemberExpression' &&
565
+ (isRuleTesterConstruction(expression.callee.object) ||
566
+ variableIdentifiers.has(expression.callee.object)) &&
567
+ expression.callee.property.type === 'Identifier' &&
568
+ expression.callee.property.name === 'run'
569
+ ) {
570
+ runCalls.push(expression);
571
+ }
572
+ }
573
+
574
+ return runCalls;
575
+ },
576
+
577
+ /**
578
+ * Performs static analysis on an AST to try to find test cases
579
+ * @param {RuleContext} context The `context` variable for the source file itself
580
+ * @param {ASTNode} ast The `Program` node for the file.
581
+ * @returns {object} An object with `valid` and `invalid` keys containing a list of AST nodes corresponding to tests
582
+ */
583
+ getTestInfo(context, ast) {
584
+ const runCalls = module.exports.checkStatementsForTestInfo(
585
+ context,
586
+ ast.body
587
+ );
588
+
589
+ return runCalls
590
+ .filter(
591
+ (call) =>
592
+ call.arguments.length >= 3 &&
593
+ call.arguments[2].type === 'ObjectExpression'
594
+ )
595
+ .map((call) => call.arguments[2])
596
+ .map((run) => {
597
+ const validProperty = run.properties.find(
598
+ (prop) => module.exports.getKeyName(prop) === 'valid'
599
+ );
600
+ const invalidProperty = run.properties.find(
601
+ (prop) => module.exports.getKeyName(prop) === 'invalid'
602
+ );
603
+
604
+ return {
605
+ valid:
606
+ validProperty && validProperty.value.type === 'ArrayExpression'
607
+ ? validProperty.value.elements.filter(Boolean)
608
+ : [],
609
+ invalid:
610
+ invalidProperty && invalidProperty.value.type === 'ArrayExpression'
611
+ ? invalidProperty.value.elements.filter(Boolean)
612
+ : [],
613
+ };
614
+ });
615
+ },
616
+
617
+ /**
618
+ * Gets information on a report, given the ASTNode of context.report().
619
+ * @param {ASTNode} node The ASTNode of context.report()
620
+ * @param {Context} context
621
+ */
622
+ getReportInfo(node, context) {
623
+ const reportArgs = node.arguments;
624
+
625
+ // If there is exactly one argument, the API expects an object.
626
+ // Otherwise, if the second argument is a string, the arguments are interpreted as
627
+ // ['node', 'message', 'data', 'fix'].
628
+ // Otherwise, the arguments are interpreted as ['node', 'loc', 'message', 'data', 'fix'].
629
+
630
+ if (reportArgs.length === 0) {
631
+ return null;
632
+ }
633
+
634
+ if (reportArgs.length === 1) {
635
+ if (reportArgs[0].type === 'ObjectExpression') {
636
+ return reportArgs[0].properties.reduce((reportInfo, property) => {
637
+ const propName = module.exports.getKeyName(property);
638
+
639
+ if (propName !== null) {
640
+ return Object.assign(reportInfo, { [propName]: property.value });
641
+ }
642
+ return reportInfo;
643
+ }, {});
644
+ }
645
+ return null;
646
+ }
647
+
648
+ let keys;
649
+ const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: use context.sourceCode when dropping eslint < v9
650
+ const scope = sourceCode.getScope?.(node) || context.getScope(); // TODO: just use sourceCode.getScope() when dropping eslint < v9
651
+ const secondArgStaticValue = getStaticValue(reportArgs[1], scope);
652
+
653
+ if (
654
+ (secondArgStaticValue &&
655
+ typeof secondArgStaticValue.value === 'string') ||
656
+ reportArgs[1].type === 'TemplateLiteral'
657
+ ) {
658
+ keys = ['node', 'message', 'data', 'fix'];
659
+ } else if (
660
+ reportArgs[1].type === 'ObjectExpression' ||
661
+ reportArgs[1].type === 'ArrayExpression' ||
662
+ (reportArgs[1].type === 'Literal' &&
663
+ typeof reportArgs[1].value !== 'string') ||
664
+ (secondArgStaticValue &&
665
+ ['object', 'number'].includes(typeof secondArgStaticValue.value))
666
+ ) {
667
+ keys = ['node', 'loc', 'message', 'data', 'fix'];
668
+ } else {
669
+ // Otherwise, we can't statically determine what argument means what, so no safe fix is possible.
670
+ return null;
671
+ }
672
+
673
+ return Object.fromEntries(
674
+ keys
675
+ .slice(0, reportArgs.length)
676
+ .map((key, index) => [key, reportArgs[index]])
677
+ );
678
+ },
679
+
680
+ /**
681
+ * Gets a set of all `sourceCode` identifiers.
682
+ * @param {ScopeManager} scopeManager
683
+ * @param {ASTNode} ast The AST of the file. This must have `parent` properties.
684
+ * @returns {Set<ASTNode>} A set of all identifiers referring to the `SourceCode` object.
685
+ */
686
+ getSourceCodeIdentifiers(scopeManager, ast) {
687
+ return new Set(
688
+ [...module.exports.getContextIdentifiers(scopeManager, ast)]
689
+ .filter(
690
+ (identifier) =>
691
+ identifier.parent &&
692
+ identifier.parent.type === 'MemberExpression' &&
693
+ identifier === identifier.parent.object &&
694
+ identifier.parent.property.type === 'Identifier' &&
695
+ identifier.parent.property.name === 'getSourceCode' &&
696
+ identifier.parent.parent.type === 'CallExpression' &&
697
+ identifier.parent === identifier.parent.parent.callee &&
698
+ identifier.parent.parent.parent.type === 'VariableDeclarator' &&
699
+ identifier.parent.parent === identifier.parent.parent.parent.init &&
700
+ identifier.parent.parent.parent.id.type === 'Identifier'
701
+ )
702
+ .flatMap((identifier) =>
703
+ scopeManager.getDeclaredVariables(identifier.parent.parent.parent)
704
+ )
705
+ .flatMap((variable) => variable.references)
706
+ .map((ref) => ref.identifier)
707
+ );
708
+ },
709
+
710
+ /**
711
+ * Insert a given property into a given object literal.
712
+ * @param {SourceCodeFixer} fixer The fixer.
713
+ * @param {Node} node The ObjectExpression node to insert a property.
714
+ * @param {string} propertyText The property code to insert.
715
+ * @returns {void}
716
+ */
717
+ insertProperty(fixer, node, propertyText, sourceCode) {
718
+ if (node.properties.length === 0) {
719
+ return fixer.replaceText(node, `{\n${propertyText}\n}`);
720
+ }
721
+ return fixer.insertTextAfter(
722
+ sourceCode.getLastToken(node.properties.at(-1)),
723
+ `,\n${propertyText}`
724
+ );
725
+ },
726
+
727
+ /**
728
+ * Collect all context.report({...}) violation/suggestion-related nodes into a standardized array for convenience.
729
+ * @param {Object} reportInfo - Result of getReportInfo().
730
+ * @returns {messageId?: String, message?: String, data?: Object, fix?: Function}[]
731
+ */
732
+ collectReportViolationAndSuggestionData(reportInfo) {
733
+ return [
734
+ // Violation message
735
+ {
736
+ messageId: reportInfo.messageId,
737
+ message: reportInfo.message,
738
+ data: reportInfo.data,
739
+ fix: reportInfo.fix,
740
+ },
741
+ // Suggestion messages
742
+ ...collectArrayElements(reportInfo.suggest)
743
+ .map((suggestObjNode) => {
744
+ if (suggestObjNode.type !== 'ObjectExpression') {
745
+ // Ignore non-objects (like variables or function calls).
746
+ return null;
747
+ }
748
+ return {
749
+ messageId: findObjectPropertyValueByKeyName(
750
+ suggestObjNode,
751
+ 'messageId'
752
+ ),
753
+ message: findObjectPropertyValueByKeyName(suggestObjNode, 'desc'), // Note: suggestion message named `desc`
754
+ data: findObjectPropertyValueByKeyName(suggestObjNode, 'data'),
755
+ fix: findObjectPropertyValueByKeyName(suggestObjNode, 'fix'),
756
+ };
757
+ })
758
+ .filter((item) => item !== null),
759
+ ];
760
+ },
761
+
762
+ /**
763
+ * Whether the provided node represents an autofixer function.
764
+ * @param {Node} node
765
+ * @param {Node[]} contextIdentifiers
766
+ * @returns {boolean}
767
+ */
768
+ isAutoFixerFunction(node, contextIdentifiers) {
769
+ const parent = node.parent;
770
+ return (
771
+ ['FunctionExpression', 'ArrowFunctionExpression'].includes(node.type) &&
772
+ parent.parent.type === 'ObjectExpression' &&
773
+ parent.parent.parent.type === 'CallExpression' &&
774
+ contextIdentifiers.has(parent.parent.parent.callee.object) &&
775
+ parent.parent.parent.callee.property.name === 'report' &&
776
+ module.exports.getReportInfo(parent.parent.parent).fix === node
777
+ );
778
+ },
779
+
780
+ /**
781
+ * Whether the provided node represents a suggestion fixer function.
782
+ * @param {Node} node
783
+ * @param {Node[]} contextIdentifiers
784
+ * @returns {boolean}
785
+ */
786
+ isSuggestionFixerFunction(node, contextIdentifiers) {
787
+ const parent = node.parent;
788
+ return (
789
+ (node.type === 'FunctionExpression' ||
790
+ node.type === 'ArrowFunctionExpression') &&
791
+ parent.type === 'Property' &&
792
+ parent.key.type === 'Identifier' &&
793
+ parent.key.name === 'fix' &&
794
+ parent.parent.type === 'ObjectExpression' &&
795
+ parent.parent.parent.type === 'ArrayExpression' &&
796
+ parent.parent.parent.parent.type === 'Property' &&
797
+ parent.parent.parent.parent.key.type === 'Identifier' &&
798
+ parent.parent.parent.parent.key.name === 'suggest' &&
799
+ parent.parent.parent.parent.parent.type === 'ObjectExpression' &&
800
+ parent.parent.parent.parent.parent.parent.type === 'CallExpression' &&
801
+ contextIdentifiers.has(
802
+ parent.parent.parent.parent.parent.parent.callee.object
803
+ ) &&
804
+ parent.parent.parent.parent.parent.parent.callee.property.name ===
805
+ 'report' &&
806
+ module.exports.getReportInfo(parent.parent.parent.parent.parent.parent)
807
+ .suggest === parent.parent.parent
808
+ );
809
+ },
810
+
811
+ /**
812
+ * List all properties contained in an object.
813
+ * Evaluates and includes any properties that may be behind spreads.
814
+ * @param {Node} objectNode
815
+ * @param {ScopeManager} scopeManager
816
+ * @returns {Node[]} the list of all properties that could be found
817
+ */
818
+ evaluateObjectProperties(objectNode, scopeManager) {
819
+ if (!objectNode || objectNode.type !== 'ObjectExpression') {
820
+ return [];
821
+ }
822
+
823
+ return objectNode.properties.flatMap((property) => {
824
+ if (property.type === 'SpreadElement') {
825
+ const value = findVariableValue(property.argument, scopeManager);
826
+ if (value && value.type === 'ObjectExpression') {
827
+ return value.properties;
828
+ }
829
+ return [];
830
+ }
831
+ return [property];
832
+ });
833
+ },
834
+
835
+ /**
836
+ * Get the `meta.messages` node from a rule.
837
+ * @param {RuleInfo} ruleInfo
838
+ * @param {ScopeManager} scopeManager
839
+ * @returns {Node|undefined}
840
+ */
841
+ getMessagesNode(ruleInfo, scopeManager) {
842
+ if (!ruleInfo) {
843
+ return;
844
+ }
845
+
846
+ const metaNode = ruleInfo.meta;
847
+ const messagesNode = module.exports
848
+ .evaluateObjectProperties(metaNode, scopeManager)
849
+ .find(
850
+ (p) =>
851
+ p.type === 'Property' && module.exports.getKeyName(p) === 'messages'
852
+ );
853
+
854
+ if (messagesNode) {
855
+ if (messagesNode.value.type === 'ObjectExpression') {
856
+ return messagesNode.value;
857
+ }
858
+ const value = findVariableValue(messagesNode.value, scopeManager);
859
+ if (value && value.type === 'ObjectExpression') {
860
+ return value;
861
+ }
862
+ }
863
+ },
864
+
865
+ /**
866
+ * Get the list of messageId properties from `meta.messages` for a rule.
867
+ * @param {RuleInfo} ruleInfo
868
+ * @param {ScopeManager} scopeManager
869
+ * @returns {Node[]|undefined}
870
+ */
871
+ getMessageIdNodes(ruleInfo, scopeManager) {
872
+ const messagesNode = module.exports.getMessagesNode(ruleInfo, scopeManager);
873
+
874
+ return messagesNode && messagesNode.type === 'ObjectExpression'
875
+ ? module.exports.evaluateObjectProperties(messagesNode, scopeManager)
876
+ : undefined;
877
+ },
878
+
879
+ /**
880
+ * Get the messageId property from a rule's `meta.messages` that matches the given `messageId`.
881
+ * @param {String} messageId - the messageId to check for
882
+ * @param {RuleInfo} ruleInfo
883
+ * @param {ScopeManager} scopeManager
884
+ * @param {Scope} scope
885
+ * @returns {Node|undefined} The matching messageId property from `meta.messages`.
886
+ */
887
+ getMessageIdNodeById(messageId, ruleInfo, scopeManager, scope) {
888
+ return module.exports
889
+ .getMessageIdNodes(ruleInfo, scopeManager)
890
+ .find(
891
+ (p) =>
892
+ p.type === 'Property' &&
893
+ module.exports.getKeyName(p, scope) === messageId
894
+ );
895
+ },
896
+
897
+ /**
898
+ * Get the possible values that a variable was initialized to at some point.
899
+ * @param {Node} node - the Identifier node for the variable.
900
+ * @param {ScopeManager} scopeManager
901
+ * @returns {Node[]} the values that the given variable could be initialized to.
902
+ */
903
+ findPossibleVariableValues(node, scopeManager) {
904
+ const variable = findVariable(
905
+ scopeManager.acquire(node) || scopeManager.globalScope,
906
+ node
907
+ );
908
+ return ((variable && variable.references) || []).flatMap((ref) => {
909
+ if (
910
+ ref.writeExpr &&
911
+ (ref.writeExpr.parent.type !== 'AssignmentExpression' ||
912
+ ref.writeExpr.parent.operator === '=')
913
+ ) {
914
+ // Given node `x`, get `123` from `x = 123;`.
915
+ // Ignore assignments with other operators like `x += 'abc';'`;
916
+ return [ref.writeExpr];
917
+ }
918
+ return [];
919
+ });
920
+ },
921
+
922
+ /**
923
+ * Check whether a variable's definition is from a function parameter.
924
+ * @param {Node} node - the Identifier node for the variable.
925
+ * @param {ScopeManager} scopeManager
926
+ * @returns {boolean} whether the variable comes from a function parameter
927
+ */
928
+ isVariableFromParameter(node, scopeManager) {
929
+ const variable = findVariable(
930
+ scopeManager.acquire(node) || scopeManager.globalScope,
931
+ node
932
+ );
933
+
934
+ return variable?.defs[0]?.type === 'Parameter';
935
+ },
936
+ };