@sgfe/eslint-plugin-sg 1.0.3

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.

Potentially problematic release.


This version of @sgfe/eslint-plugin-sg might be problematic. Click here for more details.

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
+ };