@sgfe/eslint-plugin-sg 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,110 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
const { getStaticValue } = require('eslint-utils');
|
4
|
+
const utils = require('../utils');
|
5
|
+
|
6
|
+
// ------------------------------------------------------------------------------
|
7
|
+
// Rule Definition
|
8
|
+
// ------------------------------------------------------------------------------
|
9
|
+
|
10
|
+
const DEFAULT_PATTERN = new RegExp('^(enforce|require|disallow)');
|
11
|
+
|
12
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
13
|
+
module.exports = {
|
14
|
+
meta: {
|
15
|
+
type: 'suggestion',
|
16
|
+
docs: {
|
17
|
+
description:
|
18
|
+
'require rules to implement a `meta.docs.description` property with the correct format',
|
19
|
+
category: 'Rules',
|
20
|
+
recommended: false,
|
21
|
+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/require-meta-docs-description.md',
|
22
|
+
},
|
23
|
+
fixable: null,
|
24
|
+
schema: [
|
25
|
+
{
|
26
|
+
type: 'object',
|
27
|
+
properties: {
|
28
|
+
pattern: {
|
29
|
+
type: 'string',
|
30
|
+
description:
|
31
|
+
"A regular expression that the description must match. Use `'.+'` to allow anything.",
|
32
|
+
default: '^(enforce|require|disallow)',
|
33
|
+
},
|
34
|
+
},
|
35
|
+
additionalProperties: false,
|
36
|
+
},
|
37
|
+
],
|
38
|
+
messages: {
|
39
|
+
extraWhitespace:
|
40
|
+
'`meta.docs.description` must not have leading nor trailing whitespace.',
|
41
|
+
mismatch: '`meta.docs.description` must match the regexp {{pattern}}.',
|
42
|
+
missing: '`meta.docs.description` is required.',
|
43
|
+
wrongType: '`meta.docs.description` must be a non-empty string.',
|
44
|
+
},
|
45
|
+
},
|
46
|
+
|
47
|
+
create(context) {
|
48
|
+
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
|
49
|
+
const ruleInfo = utils.getRuleInfo(sourceCode);
|
50
|
+
if (!ruleInfo) {
|
51
|
+
return {};
|
52
|
+
}
|
53
|
+
|
54
|
+
return {
|
55
|
+
Program(ast) {
|
56
|
+
const scope = sourceCode.getScope?.(ast) || context.getScope(); // TODO: just use sourceCode.getScope() when we drop support for ESLint < v9.0.0
|
57
|
+
const { scopeManager } = sourceCode;
|
58
|
+
|
59
|
+
const pattern =
|
60
|
+
context.options[0] && context.options[0].pattern
|
61
|
+
? new RegExp(context.options[0].pattern)
|
62
|
+
: DEFAULT_PATTERN;
|
63
|
+
|
64
|
+
const metaNode = ruleInfo.meta;
|
65
|
+
const docsNode = utils
|
66
|
+
.evaluateObjectProperties(metaNode, scopeManager)
|
67
|
+
.find((p) => p.type === 'Property' && utils.getKeyName(p) === 'docs');
|
68
|
+
|
69
|
+
const descriptionNode = utils
|
70
|
+
.evaluateObjectProperties(docsNode && docsNode.value, scopeManager)
|
71
|
+
.find(
|
72
|
+
(p) =>
|
73
|
+
p.type === 'Property' && utils.getKeyName(p) === 'description'
|
74
|
+
);
|
75
|
+
|
76
|
+
if (!descriptionNode) {
|
77
|
+
context.report({
|
78
|
+
node: docsNode || metaNode || ruleInfo.create,
|
79
|
+
messageId: 'missing',
|
80
|
+
});
|
81
|
+
return;
|
82
|
+
}
|
83
|
+
|
84
|
+
const staticValue = getStaticValue(descriptionNode.value, scope);
|
85
|
+
if (!staticValue) {
|
86
|
+
// Ignore non-static values since we can't determine what they look like.
|
87
|
+
return;
|
88
|
+
}
|
89
|
+
|
90
|
+
if (typeof staticValue.value !== 'string' || staticValue.value === '') {
|
91
|
+
context.report({
|
92
|
+
node: descriptionNode.value,
|
93
|
+
messageId: 'wrongType',
|
94
|
+
});
|
95
|
+
} else if (staticValue.value !== staticValue.value.trim()) {
|
96
|
+
context.report({
|
97
|
+
node: descriptionNode.value,
|
98
|
+
messageId: 'extraWhitespace',
|
99
|
+
});
|
100
|
+
} else if (!pattern.test(staticValue.value)) {
|
101
|
+
context.report({
|
102
|
+
node: descriptionNode.value,
|
103
|
+
messageId: 'mismatch',
|
104
|
+
data: { pattern },
|
105
|
+
});
|
106
|
+
}
|
107
|
+
},
|
108
|
+
};
|
109
|
+
},
|
110
|
+
};
|
@@ -0,0 +1,175 @@
|
|
1
|
+
/**
|
2
|
+
* @author Toru Nagashima <https://github.com/mysticatea>
|
3
|
+
*/
|
4
|
+
|
5
|
+
'use strict';
|
6
|
+
|
7
|
+
// -----------------------------------------------------------------------------
|
8
|
+
// Requirements
|
9
|
+
// -----------------------------------------------------------------------------
|
10
|
+
|
11
|
+
const path = require('path');
|
12
|
+
const util = require('../utils');
|
13
|
+
const { getStaticValue } = require('eslint-utils');
|
14
|
+
|
15
|
+
// -----------------------------------------------------------------------------
|
16
|
+
// Rule Definition
|
17
|
+
// -----------------------------------------------------------------------------
|
18
|
+
|
19
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
20
|
+
module.exports = {
|
21
|
+
meta: {
|
22
|
+
type: 'suggestion',
|
23
|
+
docs: {
|
24
|
+
description: 'require rules to implement a `meta.docs.url` property',
|
25
|
+
category: 'Rules',
|
26
|
+
recommended: false,
|
27
|
+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/require-meta-docs-url.md',
|
28
|
+
},
|
29
|
+
fixable: 'code',
|
30
|
+
schema: [
|
31
|
+
{
|
32
|
+
type: 'object',
|
33
|
+
properties: {
|
34
|
+
pattern: {
|
35
|
+
type: 'string',
|
36
|
+
description:
|
37
|
+
"A pattern to enforce rule's document URL. It replaces `{{name}}` placeholder by each rule name. The rule name is the basename of each rule file. Omitting this allows any URL.",
|
38
|
+
},
|
39
|
+
},
|
40
|
+
additionalProperties: false,
|
41
|
+
},
|
42
|
+
],
|
43
|
+
messages: {
|
44
|
+
mismatch: '`meta.docs.url` property must be `{{expectedUrl}}`.',
|
45
|
+
missing: '`meta.docs.url` property is missing.',
|
46
|
+
wrongType: '`meta.docs.url` property must be a string.',
|
47
|
+
},
|
48
|
+
},
|
49
|
+
|
50
|
+
/**
|
51
|
+
* Creates AST event handlers for require-meta-docs-url.
|
52
|
+
* @param {RuleContext} context - The rule context.
|
53
|
+
* @returns {Object} AST event handlers.
|
54
|
+
*/
|
55
|
+
create(context) {
|
56
|
+
const options = context.options[0] || {};
|
57
|
+
const filename = context.filename || context.getFilename(); // TODO: just use context.filename when dropping eslint < v9
|
58
|
+
const ruleName =
|
59
|
+
filename === '<input>'
|
60
|
+
? undefined
|
61
|
+
: path.basename(filename, path.extname(filename));
|
62
|
+
const expectedUrl =
|
63
|
+
!options.pattern || !ruleName
|
64
|
+
? undefined
|
65
|
+
: options.pattern.replaceAll(/{{\s*name\s*}}/g, ruleName);
|
66
|
+
|
67
|
+
/**
|
68
|
+
* Check whether a given URL is the expected URL.
|
69
|
+
* @param {string} url The URL to check.
|
70
|
+
* @returns {boolean} `true` if the node is the expected URL.
|
71
|
+
*/
|
72
|
+
function isExpectedUrl(url) {
|
73
|
+
return Boolean(
|
74
|
+
typeof url === 'string' &&
|
75
|
+
(expectedUrl === undefined || url === expectedUrl)
|
76
|
+
);
|
77
|
+
}
|
78
|
+
|
79
|
+
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
|
80
|
+
const ruleInfo = util.getRuleInfo(sourceCode);
|
81
|
+
if (!ruleInfo) {
|
82
|
+
return {};
|
83
|
+
}
|
84
|
+
|
85
|
+
return {
|
86
|
+
Program(ast) {
|
87
|
+
const scope = sourceCode.getScope?.(ast) || context.getScope(); // TODO: just use sourceCode.getScope() when we drop support for ESLint < v9.0.0
|
88
|
+
const { scopeManager } = sourceCode;
|
89
|
+
|
90
|
+
const metaNode = ruleInfo.meta;
|
91
|
+
const docsPropNode = util
|
92
|
+
.evaluateObjectProperties(metaNode, scopeManager)
|
93
|
+
.find((p) => p.type === 'Property' && util.getKeyName(p) === 'docs');
|
94
|
+
const urlPropNode = util
|
95
|
+
.evaluateObjectProperties(
|
96
|
+
docsPropNode && docsPropNode.value,
|
97
|
+
scopeManager
|
98
|
+
)
|
99
|
+
.find((p) => p.type === 'Property' && util.getKeyName(p) === 'url');
|
100
|
+
|
101
|
+
const staticValue = urlPropNode
|
102
|
+
? getStaticValue(urlPropNode.value, scope)
|
103
|
+
: undefined;
|
104
|
+
if (urlPropNode && !staticValue) {
|
105
|
+
// Ignore non-static values since we can't determine what they look like.
|
106
|
+
return;
|
107
|
+
}
|
108
|
+
|
109
|
+
if (isExpectedUrl(staticValue && staticValue.value)) {
|
110
|
+
return;
|
111
|
+
}
|
112
|
+
|
113
|
+
context.report({
|
114
|
+
node:
|
115
|
+
(urlPropNode && urlPropNode.value) ||
|
116
|
+
(docsPropNode && docsPropNode.value) ||
|
117
|
+
metaNode ||
|
118
|
+
ruleInfo.create,
|
119
|
+
|
120
|
+
// eslint-disable-next-line unicorn/no-negated-condition -- actually more clear like this
|
121
|
+
messageId: !urlPropNode
|
122
|
+
? 'missing'
|
123
|
+
: // eslint-disable-next-line unicorn/no-nested-ternary,unicorn/no-negated-condition -- this is fine for now
|
124
|
+
!expectedUrl
|
125
|
+
? 'wrongType'
|
126
|
+
: /* otherwise */ 'mismatch',
|
127
|
+
|
128
|
+
data: {
|
129
|
+
expectedUrl,
|
130
|
+
},
|
131
|
+
|
132
|
+
fix(fixer) {
|
133
|
+
if (!expectedUrl) {
|
134
|
+
return null;
|
135
|
+
}
|
136
|
+
|
137
|
+
const urlString = JSON.stringify(expectedUrl);
|
138
|
+
if (urlPropNode) {
|
139
|
+
if (
|
140
|
+
urlPropNode.value.type === 'Literal' ||
|
141
|
+
(urlPropNode.value.type === 'Identifier' &&
|
142
|
+
urlPropNode.value.name === 'undefined')
|
143
|
+
) {
|
144
|
+
return fixer.replaceText(urlPropNode.value, urlString);
|
145
|
+
}
|
146
|
+
} else if (
|
147
|
+
docsPropNode &&
|
148
|
+
docsPropNode.value.type === 'ObjectExpression'
|
149
|
+
) {
|
150
|
+
return util.insertProperty(
|
151
|
+
fixer,
|
152
|
+
docsPropNode.value,
|
153
|
+
`url: ${urlString}`,
|
154
|
+
sourceCode
|
155
|
+
);
|
156
|
+
} else if (
|
157
|
+
!docsPropNode &&
|
158
|
+
metaNode &&
|
159
|
+
metaNode.type === 'ObjectExpression'
|
160
|
+
) {
|
161
|
+
return util.insertProperty(
|
162
|
+
fixer,
|
163
|
+
metaNode,
|
164
|
+
`docs: {\nurl: ${urlString}\n}`,
|
165
|
+
sourceCode
|
166
|
+
);
|
167
|
+
}
|
168
|
+
|
169
|
+
return null;
|
170
|
+
},
|
171
|
+
});
|
172
|
+
},
|
173
|
+
};
|
174
|
+
},
|
175
|
+
};
|
@@ -0,0 +1,137 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview require rules to implement a `meta.fixable` property
|
3
|
+
* @author Teddy Katz
|
4
|
+
*/
|
5
|
+
|
6
|
+
'use strict';
|
7
|
+
|
8
|
+
const { getStaticValue } = require('eslint-utils');
|
9
|
+
const utils = require('../utils');
|
10
|
+
|
11
|
+
// ------------------------------------------------------------------------------
|
12
|
+
// Rule Definition
|
13
|
+
// ------------------------------------------------------------------------------
|
14
|
+
|
15
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
16
|
+
module.exports = {
|
17
|
+
meta: {
|
18
|
+
type: 'problem',
|
19
|
+
docs: {
|
20
|
+
description: 'require rules to implement a `meta.fixable` property',
|
21
|
+
category: 'Rules',
|
22
|
+
recommended: true,
|
23
|
+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/require-meta-fixable.md',
|
24
|
+
},
|
25
|
+
schema: [
|
26
|
+
{
|
27
|
+
type: 'object',
|
28
|
+
properties: {
|
29
|
+
catchNoFixerButFixableProperty: {
|
30
|
+
type: 'boolean',
|
31
|
+
default: false,
|
32
|
+
description:
|
33
|
+
"Whether the rule should attempt to detect rules that do not have a fixer but enable the `meta.fixable` property. This option is off by default because it increases the chance of false positives since fixers can't always be detected when helper functions are used.",
|
34
|
+
},
|
35
|
+
},
|
36
|
+
additionalProperties: false,
|
37
|
+
},
|
38
|
+
],
|
39
|
+
messages: {
|
40
|
+
invalid: '`meta.fixable` must be either `code`, `whitespace`, or `null`.',
|
41
|
+
missing:
|
42
|
+
'`meta.fixable` must be either `code` or `whitespace` for fixable rules.',
|
43
|
+
noFixerButFixableValue:
|
44
|
+
'`meta.fixable` is enabled but no fixer detected.',
|
45
|
+
},
|
46
|
+
},
|
47
|
+
|
48
|
+
create(context) {
|
49
|
+
const catchNoFixerButFixableProperty =
|
50
|
+
context.options[0] && context.options[0].catchNoFixerButFixableProperty;
|
51
|
+
|
52
|
+
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
|
53
|
+
const { scopeManager } = sourceCode;
|
54
|
+
const ruleInfo = utils.getRuleInfo(sourceCode);
|
55
|
+
let contextIdentifiers;
|
56
|
+
let usesFixFunctions;
|
57
|
+
|
58
|
+
if (!ruleInfo) {
|
59
|
+
return {};
|
60
|
+
}
|
61
|
+
|
62
|
+
return {
|
63
|
+
Program(ast) {
|
64
|
+
contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
|
65
|
+
},
|
66
|
+
CallExpression(node) {
|
67
|
+
if (
|
68
|
+
node.callee.type === 'MemberExpression' &&
|
69
|
+
contextIdentifiers.has(node.callee.object) &&
|
70
|
+
node.callee.property.type === 'Identifier' &&
|
71
|
+
node.callee.property.name === 'report' &&
|
72
|
+
(node.arguments.length > 4 ||
|
73
|
+
(node.arguments.length === 1 &&
|
74
|
+
utils
|
75
|
+
.evaluateObjectProperties(node.arguments[0], scopeManager)
|
76
|
+
.some((prop) => utils.getKeyName(prop) === 'fix')))
|
77
|
+
) {
|
78
|
+
usesFixFunctions = true;
|
79
|
+
}
|
80
|
+
},
|
81
|
+
'Program:exit'(ast) {
|
82
|
+
const scope = sourceCode.getScope?.(ast) || context.getScope(); // TODO: just use sourceCode.getScope() when we drop support for ESLint < v9.0.0
|
83
|
+
const metaFixableProp =
|
84
|
+
ruleInfo &&
|
85
|
+
utils
|
86
|
+
.evaluateObjectProperties(ruleInfo.meta, scopeManager)
|
87
|
+
.find((prop) => utils.getKeyName(prop) === 'fixable');
|
88
|
+
|
89
|
+
if (metaFixableProp) {
|
90
|
+
const staticValue = getStaticValue(metaFixableProp.value, scope);
|
91
|
+
if (!staticValue) {
|
92
|
+
// Ignore non-static values since we can't determine what they look like.
|
93
|
+
return;
|
94
|
+
}
|
95
|
+
|
96
|
+
if (
|
97
|
+
!['code', 'whitespace', null, undefined].includes(staticValue.value)
|
98
|
+
) {
|
99
|
+
// `fixable` property has an invalid value.
|
100
|
+
context.report({
|
101
|
+
node: metaFixableProp.value,
|
102
|
+
messageId: 'invalid',
|
103
|
+
});
|
104
|
+
return;
|
105
|
+
}
|
106
|
+
|
107
|
+
if (
|
108
|
+
usesFixFunctions &&
|
109
|
+
!['code', 'whitespace'].includes(staticValue.value)
|
110
|
+
) {
|
111
|
+
// Rule is fixable but `fixable` property does not have a fixable value.
|
112
|
+
context.report({
|
113
|
+
node: metaFixableProp.value,
|
114
|
+
messageId: 'missing',
|
115
|
+
});
|
116
|
+
} else if (
|
117
|
+
catchNoFixerButFixableProperty &&
|
118
|
+
!usesFixFunctions &&
|
119
|
+
['code', 'whitespace'].includes(staticValue.value)
|
120
|
+
) {
|
121
|
+
// Rule is NOT fixable but `fixable` property has a fixable value.
|
122
|
+
context.report({
|
123
|
+
node: metaFixableProp.value,
|
124
|
+
messageId: 'noFixerButFixableValue',
|
125
|
+
});
|
126
|
+
}
|
127
|
+
} else if (!metaFixableProp && usesFixFunctions) {
|
128
|
+
// Rule is fixable but is missing the `fixable` property.
|
129
|
+
context.report({
|
130
|
+
node: ruleInfo.meta || ruleInfo.create,
|
131
|
+
messageId: 'missing',
|
132
|
+
});
|
133
|
+
}
|
134
|
+
},
|
135
|
+
};
|
136
|
+
},
|
137
|
+
};
|
@@ -0,0 +1,168 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
const utils = require('../utils');
|
4
|
+
const { getStaticValue } = require('eslint-utils');
|
5
|
+
|
6
|
+
// ------------------------------------------------------------------------------
|
7
|
+
// Rule Definition
|
8
|
+
// ------------------------------------------------------------------------------
|
9
|
+
|
10
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
11
|
+
module.exports = {
|
12
|
+
meta: {
|
13
|
+
type: 'problem',
|
14
|
+
docs: {
|
15
|
+
description:
|
16
|
+
'require suggestable rules to implement a `meta.hasSuggestions` property',
|
17
|
+
category: 'Rules',
|
18
|
+
recommended: true,
|
19
|
+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/require-meta-has-suggestions.md',
|
20
|
+
},
|
21
|
+
fixable: 'code',
|
22
|
+
schema: [],
|
23
|
+
messages: {
|
24
|
+
shouldBeSuggestable:
|
25
|
+
'`meta.hasSuggestions` must be `true` for suggestable rules.',
|
26
|
+
shouldNotBeSuggestable:
|
27
|
+
'`meta.hasSuggestions` cannot be `true` for non-suggestable rules.',
|
28
|
+
},
|
29
|
+
},
|
30
|
+
|
31
|
+
create(context) {
|
32
|
+
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
|
33
|
+
const { scopeManager } = sourceCode;
|
34
|
+
const ruleInfo = utils.getRuleInfo(sourceCode);
|
35
|
+
if (!ruleInfo) {
|
36
|
+
return {};
|
37
|
+
}
|
38
|
+
let contextIdentifiers;
|
39
|
+
let ruleReportsSuggestions;
|
40
|
+
|
41
|
+
/**
|
42
|
+
* Check if a "suggest" object property from a rule violation report should be considered to contain suggestions.
|
43
|
+
* @param {Node} node - the "suggest" object property to check
|
44
|
+
* @returns {boolean} whether this property should be considered to contain suggestions
|
45
|
+
*/
|
46
|
+
function doesPropertyContainSuggestions(node) {
|
47
|
+
const scope = sourceCode.getScope?.(node) || context.getScope(); // TODO: just use sourceCode.getScope() when we drop support for ESLint < v9.0.0
|
48
|
+
const staticValue = getStaticValue(node.value, scope);
|
49
|
+
if (
|
50
|
+
!staticValue ||
|
51
|
+
(Array.isArray(staticValue.value) && staticValue.value.length > 0) ||
|
52
|
+
(Array.isArray(staticValue.value) &&
|
53
|
+
staticValue.value.length === 0 &&
|
54
|
+
node.value.type === 'Identifier') // Array variable can have suggestions pushed to it.
|
55
|
+
) {
|
56
|
+
// These are all considered reporting suggestions:
|
57
|
+
// suggest: [{...}]
|
58
|
+
// suggest: getSuggestions()
|
59
|
+
// suggest: MY_SUGGESTIONS
|
60
|
+
return true;
|
61
|
+
}
|
62
|
+
return false;
|
63
|
+
}
|
64
|
+
|
65
|
+
return {
|
66
|
+
Program(ast) {
|
67
|
+
contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
|
68
|
+
},
|
69
|
+
CallExpression(node) {
|
70
|
+
if (
|
71
|
+
node.callee.type === 'MemberExpression' &&
|
72
|
+
contextIdentifiers.has(node.callee.object) &&
|
73
|
+
node.callee.property.type === 'Identifier' &&
|
74
|
+
node.callee.property.name === 'report' &&
|
75
|
+
(node.arguments.length > 4 ||
|
76
|
+
(node.arguments.length === 1 &&
|
77
|
+
node.arguments[0].type === 'ObjectExpression'))
|
78
|
+
) {
|
79
|
+
const suggestProp = utils
|
80
|
+
.evaluateObjectProperties(node.arguments[0], scopeManager)
|
81
|
+
.find((prop) => utils.getKeyName(prop) === 'suggest');
|
82
|
+
if (suggestProp && doesPropertyContainSuggestions(suggestProp)) {
|
83
|
+
ruleReportsSuggestions = true;
|
84
|
+
}
|
85
|
+
}
|
86
|
+
},
|
87
|
+
Property(node) {
|
88
|
+
// In order to reduce false positives, we will also check for a `suggest` property anywhere in the file.
|
89
|
+
// This is helpful especially in the event that helper functions are used for reporting violations.
|
90
|
+
if (
|
91
|
+
node.key.type === 'Identifier' &&
|
92
|
+
node.key.name === 'suggest' &&
|
93
|
+
doesPropertyContainSuggestions(node)
|
94
|
+
) {
|
95
|
+
ruleReportsSuggestions = true;
|
96
|
+
}
|
97
|
+
},
|
98
|
+
'Program:exit'(node) {
|
99
|
+
const scope = sourceCode.getScope?.(node) || context.getScope(); // TODO: just use sourceCode.getScope() when we drop support for ESLint < v9.0.0
|
100
|
+
const metaNode = ruleInfo && ruleInfo.meta;
|
101
|
+
const hasSuggestionsProperty = utils
|
102
|
+
.evaluateObjectProperties(metaNode, scopeManager)
|
103
|
+
.find((prop) => utils.getKeyName(prop) === 'hasSuggestions');
|
104
|
+
const hasSuggestionsStaticValue =
|
105
|
+
hasSuggestionsProperty &&
|
106
|
+
getStaticValue(hasSuggestionsProperty.value, scope);
|
107
|
+
|
108
|
+
if (ruleReportsSuggestions) {
|
109
|
+
if (!hasSuggestionsProperty) {
|
110
|
+
// Rule reports suggestions but is missing the `meta.hasSuggestions` property altogether.
|
111
|
+
context.report({
|
112
|
+
node: metaNode || ruleInfo.create,
|
113
|
+
messageId: 'shouldBeSuggestable',
|
114
|
+
fix(fixer) {
|
115
|
+
if (metaNode && metaNode.type === 'ObjectExpression') {
|
116
|
+
if (metaNode.properties.length === 0) {
|
117
|
+
// If object is empty, just replace entire object.
|
118
|
+
return fixer.replaceText(
|
119
|
+
metaNode,
|
120
|
+
'{ hasSuggestions: true }'
|
121
|
+
);
|
122
|
+
}
|
123
|
+
// Add new property to start of property list.
|
124
|
+
return fixer.insertTextBefore(
|
125
|
+
metaNode.properties[0],
|
126
|
+
'hasSuggestions: true, '
|
127
|
+
);
|
128
|
+
}
|
129
|
+
},
|
130
|
+
});
|
131
|
+
} else if (
|
132
|
+
hasSuggestionsStaticValue &&
|
133
|
+
hasSuggestionsStaticValue.value !== true
|
134
|
+
) {
|
135
|
+
// Rule reports suggestions but does not have `meta.hasSuggestions` property enabled.
|
136
|
+
context.report({
|
137
|
+
node: hasSuggestionsProperty.value,
|
138
|
+
messageId: 'shouldBeSuggestable',
|
139
|
+
fix(fixer) {
|
140
|
+
if (
|
141
|
+
hasSuggestionsProperty.value.type === 'Literal' ||
|
142
|
+
(hasSuggestionsProperty.value.type === 'Identifier' &&
|
143
|
+
hasSuggestionsProperty.value.name === 'undefined')
|
144
|
+
) {
|
145
|
+
return fixer.replaceText(
|
146
|
+
hasSuggestionsProperty.value,
|
147
|
+
'true'
|
148
|
+
);
|
149
|
+
}
|
150
|
+
},
|
151
|
+
});
|
152
|
+
}
|
153
|
+
} else if (
|
154
|
+
!ruleReportsSuggestions &&
|
155
|
+
hasSuggestionsProperty &&
|
156
|
+
hasSuggestionsStaticValue &&
|
157
|
+
hasSuggestionsStaticValue.value === true
|
158
|
+
) {
|
159
|
+
// Rule does not report suggestions but has the `meta.hasSuggestions` property enabled.
|
160
|
+
context.report({
|
161
|
+
node: hasSuggestionsProperty.value,
|
162
|
+
messageId: 'shouldNotBeSuggestable',
|
163
|
+
});
|
164
|
+
}
|
165
|
+
},
|
166
|
+
};
|
167
|
+
},
|
168
|
+
};
|