@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.
- package/LICENSE.md +25 -0
- package/README.md +188 -0
- package/configs/all-type-checked.js +10 -0
- package/configs/all.js +11 -0
- package/configs/recommended.js +11 -0
- package/configs/rules-recommended.js +11 -0
- package/configs/rules.js +11 -0
- package/configs/tests-recommended.js +11 -0
- package/configs/tests.js +11 -0
- package/lib/index.js +90 -0
- package/lib/rules/consistent-output.js +70 -0
- package/lib/rules/fixer-return.js +170 -0
- package/lib/rules/meta-property-ordering.js +108 -0
- package/lib/rules/no-deprecated-context-methods.js +98 -0
- package/lib/rules/no-deprecated-report-api.js +83 -0
- package/lib/rules/no-identical-tests.js +87 -0
- package/lib/rules/no-missing-message-ids.js +101 -0
- package/lib/rules/no-missing-placeholders.js +131 -0
- package/lib/rules/no-only-tests.js +99 -0
- package/lib/rules/no-property-in-node.js +86 -0
- package/lib/rules/no-unused-message-ids.js +139 -0
- package/lib/rules/no-unused-placeholders.js +127 -0
- package/lib/rules/no-useless-token-range.js +174 -0
- package/lib/rules/prefer-message-ids.js +109 -0
- package/lib/rules/prefer-object-rule.js +83 -0
- package/lib/rules/prefer-output-null.js +77 -0
- package/lib/rules/prefer-placeholders.js +102 -0
- package/lib/rules/prefer-replace-text.js +91 -0
- package/lib/rules/report-message-format.js +133 -0
- package/lib/rules/require-meta-docs-description.js +110 -0
- package/lib/rules/require-meta-docs-url.js +175 -0
- package/lib/rules/require-meta-fixable.js +137 -0
- package/lib/rules/require-meta-has-suggestions.js +168 -0
- package/lib/rules/require-meta-schema.js +162 -0
- package/lib/rules/require-meta-type.js +77 -0
- package/lib/rules/test-case-property-ordering.js +107 -0
- package/lib/rules/test-case-shorthand-strings.js +124 -0
- package/lib/utils.js +936 -0
- 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
|
+
};
|