@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,108 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview Enforces the order of meta properties
|
3
|
+
*/
|
4
|
+
|
5
|
+
'use strict';
|
6
|
+
|
7
|
+
const { getKeyName, getRuleInfo } = require('../utils');
|
8
|
+
|
9
|
+
// ------------------------------------------------------------------------------
|
10
|
+
// Rule Definition
|
11
|
+
// ------------------------------------------------------------------------------
|
12
|
+
|
13
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
14
|
+
module.exports = {
|
15
|
+
meta: {
|
16
|
+
type: 'suggestion',
|
17
|
+
docs: {
|
18
|
+
description: 'enforce the order of meta properties',
|
19
|
+
category: 'Rules',
|
20
|
+
recommended: false,
|
21
|
+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/meta-property-ordering.md',
|
22
|
+
},
|
23
|
+
fixable: 'code',
|
24
|
+
schema: [
|
25
|
+
{
|
26
|
+
type: 'array',
|
27
|
+
elements: { type: 'string' },
|
28
|
+
},
|
29
|
+
],
|
30
|
+
messages: {
|
31
|
+
inconsistentOrder:
|
32
|
+
'The meta properties should be placed in a consistent order: [{{order}}].',
|
33
|
+
},
|
34
|
+
},
|
35
|
+
|
36
|
+
create(context) {
|
37
|
+
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
|
38
|
+
const ruleInfo = getRuleInfo(sourceCode);
|
39
|
+
if (!ruleInfo) {
|
40
|
+
return {};
|
41
|
+
}
|
42
|
+
|
43
|
+
const order = context.options[0] || [
|
44
|
+
'type',
|
45
|
+
'docs',
|
46
|
+
'fixable',
|
47
|
+
'hasSuggestions',
|
48
|
+
'deprecated',
|
49
|
+
'replacedBy',
|
50
|
+
'schema',
|
51
|
+
'defaultOptions', // https://github.com/eslint/rfcs/tree/main/designs/2023-rule-options-defaults
|
52
|
+
'messages',
|
53
|
+
];
|
54
|
+
|
55
|
+
const orderMap = new Map(order.map((name, i) => [name, i]));
|
56
|
+
|
57
|
+
return {
|
58
|
+
Program() {
|
59
|
+
if (!ruleInfo.meta || ruleInfo.meta.properties.length < 2) {
|
60
|
+
return;
|
61
|
+
}
|
62
|
+
|
63
|
+
const props = ruleInfo.meta.properties;
|
64
|
+
|
65
|
+
let last;
|
66
|
+
|
67
|
+
const violatingProps = props.filter((prop) => {
|
68
|
+
const curr = orderMap.has(getKeyName(prop))
|
69
|
+
? orderMap.get(getKeyName(prop))
|
70
|
+
: Number.POSITIVE_INFINITY;
|
71
|
+
return last > (last = curr);
|
72
|
+
});
|
73
|
+
|
74
|
+
if (violatingProps.length === 0) {
|
75
|
+
return;
|
76
|
+
}
|
77
|
+
|
78
|
+
const knownProps = props
|
79
|
+
.filter((prop) => orderMap.has(getKeyName(prop)))
|
80
|
+
.sort(
|
81
|
+
(a, b) => orderMap.get(getKeyName(a)) - orderMap.get(getKeyName(b))
|
82
|
+
);
|
83
|
+
const unknownProps = props.filter(
|
84
|
+
(prop) => !orderMap.has(getKeyName(prop))
|
85
|
+
);
|
86
|
+
|
87
|
+
for (const violatingProp of violatingProps) {
|
88
|
+
context.report({
|
89
|
+
node: violatingProp,
|
90
|
+
messageId: 'inconsistentOrder',
|
91
|
+
data: {
|
92
|
+
order: knownProps.map(getKeyName).join(', '),
|
93
|
+
},
|
94
|
+
fix(fixer) {
|
95
|
+
const expectedProps = [...knownProps, ...unknownProps];
|
96
|
+
return props.map((prop, k) => {
|
97
|
+
return fixer.replaceText(
|
98
|
+
prop,
|
99
|
+
sourceCode.getText(expectedProps[k])
|
100
|
+
);
|
101
|
+
});
|
102
|
+
},
|
103
|
+
});
|
104
|
+
}
|
105
|
+
},
|
106
|
+
};
|
107
|
+
},
|
108
|
+
};
|
@@ -0,0 +1,98 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview Disallows usage of deprecated methods on rule context objects
|
3
|
+
* @author Teddy Katz
|
4
|
+
*/
|
5
|
+
|
6
|
+
'use strict';
|
7
|
+
|
8
|
+
const utils = require('../utils');
|
9
|
+
|
10
|
+
const DEPRECATED_PASSTHROUGHS = {
|
11
|
+
getSource: 'getText',
|
12
|
+
getSourceLines: 'getLines',
|
13
|
+
getAllComments: 'getAllComments',
|
14
|
+
getNodeByRangeIndex: 'getNodeByRangeIndex',
|
15
|
+
getComments: 'getComments',
|
16
|
+
getCommentsBefore: 'getCommentsBefore',
|
17
|
+
getCommentsAfter: 'getCommentsAfter',
|
18
|
+
getCommentsInside: 'getCommentsInside',
|
19
|
+
getJSDocComment: 'getJSDocComment',
|
20
|
+
getFirstToken: 'getFirstToken',
|
21
|
+
getFirstTokens: 'getFirstTokens',
|
22
|
+
getLastToken: 'getLastToken',
|
23
|
+
getLastTokens: 'getLastTokens',
|
24
|
+
getTokenAfter: 'getTokenAfter',
|
25
|
+
getTokenBefore: 'getTokenBefore',
|
26
|
+
getTokenByRangeStart: 'getTokenByRangeStart',
|
27
|
+
getTokens: 'getTokens',
|
28
|
+
getTokensAfter: 'getTokensAfter',
|
29
|
+
getTokensBefore: 'getTokensBefore',
|
30
|
+
getTokensBetween: 'getTokensBetween',
|
31
|
+
};
|
32
|
+
|
33
|
+
// ------------------------------------------------------------------------------
|
34
|
+
// Rule Definition
|
35
|
+
// ------------------------------------------------------------------------------
|
36
|
+
|
37
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
38
|
+
module.exports = {
|
39
|
+
meta: {
|
40
|
+
type: 'suggestion',
|
41
|
+
docs: {
|
42
|
+
description:
|
43
|
+
'disallow usage of deprecated methods on rule context objects',
|
44
|
+
category: 'Rules',
|
45
|
+
recommended: true,
|
46
|
+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-deprecated-context-methods.md',
|
47
|
+
},
|
48
|
+
fixable: 'code',
|
49
|
+
schema: [],
|
50
|
+
messages: {
|
51
|
+
newFormat:
|
52
|
+
'Use `{{contextName}}.getSourceCode().{{replacement}}` instead of `{{contextName}}.{{original}}`.',
|
53
|
+
},
|
54
|
+
},
|
55
|
+
|
56
|
+
create(context) {
|
57
|
+
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
|
58
|
+
|
59
|
+
// ----------------------------------------------------------------------
|
60
|
+
// Public
|
61
|
+
// ----------------------------------------------------------------------
|
62
|
+
|
63
|
+
return {
|
64
|
+
'Program:exit'(ast) {
|
65
|
+
[...utils.getContextIdentifiers(sourceCode.scopeManager, ast)]
|
66
|
+
.filter(
|
67
|
+
(contextId) =>
|
68
|
+
contextId.parent.type === 'MemberExpression' &&
|
69
|
+
contextId === contextId.parent.object &&
|
70
|
+
contextId.parent.property.type === 'Identifier' &&
|
71
|
+
Object.prototype.hasOwnProperty.call(
|
72
|
+
DEPRECATED_PASSTHROUGHS,
|
73
|
+
contextId.parent.property.name
|
74
|
+
)
|
75
|
+
)
|
76
|
+
.forEach((contextId) =>
|
77
|
+
context.report({
|
78
|
+
node: contextId.parent,
|
79
|
+
messageId: 'newFormat',
|
80
|
+
data: {
|
81
|
+
contextName: contextId.name,
|
82
|
+
original: contextId.parent.property.name,
|
83
|
+
replacement:
|
84
|
+
DEPRECATED_PASSTHROUGHS[contextId.parent.property.name],
|
85
|
+
},
|
86
|
+
fix: (fixer) => [
|
87
|
+
fixer.insertTextAfter(contextId, '.getSourceCode()'),
|
88
|
+
fixer.replaceText(
|
89
|
+
contextId.parent.property,
|
90
|
+
DEPRECATED_PASSTHROUGHS[contextId.parent.property.name]
|
91
|
+
),
|
92
|
+
],
|
93
|
+
})
|
94
|
+
);
|
95
|
+
},
|
96
|
+
};
|
97
|
+
},
|
98
|
+
};
|
@@ -0,0 +1,83 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview Disallow the version of `context.report()` with multiple arguments
|
3
|
+
* @author Teddy Katz
|
4
|
+
*/
|
5
|
+
|
6
|
+
'use strict';
|
7
|
+
|
8
|
+
const utils = require('../utils');
|
9
|
+
|
10
|
+
// ------------------------------------------------------------------------------
|
11
|
+
// Rule Definition
|
12
|
+
// ------------------------------------------------------------------------------
|
13
|
+
|
14
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
15
|
+
module.exports = {
|
16
|
+
meta: {
|
17
|
+
type: 'suggestion',
|
18
|
+
docs: {
|
19
|
+
description:
|
20
|
+
'disallow the version of `context.report()` with multiple arguments',
|
21
|
+
category: 'Rules',
|
22
|
+
recommended: true,
|
23
|
+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-deprecated-report-api.md',
|
24
|
+
},
|
25
|
+
fixable: 'code', // or "code" or "whitespace"
|
26
|
+
schema: [],
|
27
|
+
messages: {
|
28
|
+
useNewAPI: 'Use the new-style context.report() API.',
|
29
|
+
},
|
30
|
+
},
|
31
|
+
|
32
|
+
create(context) {
|
33
|
+
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
|
34
|
+
let contextIdentifiers;
|
35
|
+
|
36
|
+
// ----------------------------------------------------------------------
|
37
|
+
// Public
|
38
|
+
// ----------------------------------------------------------------------
|
39
|
+
|
40
|
+
return {
|
41
|
+
Program(ast) {
|
42
|
+
contextIdentifiers = utils.getContextIdentifiers(
|
43
|
+
sourceCode.scopeManager,
|
44
|
+
ast
|
45
|
+
);
|
46
|
+
},
|
47
|
+
CallExpression(node) {
|
48
|
+
if (
|
49
|
+
node.callee.type === 'MemberExpression' &&
|
50
|
+
contextIdentifiers.has(node.callee.object) &&
|
51
|
+
node.callee.property.type === 'Identifier' &&
|
52
|
+
node.callee.property.name === 'report' &&
|
53
|
+
(node.arguments.length > 1 ||
|
54
|
+
(node.arguments.length === 1 &&
|
55
|
+
node.arguments[0].type === 'SpreadElement'))
|
56
|
+
) {
|
57
|
+
context.report({
|
58
|
+
node: node.callee.property,
|
59
|
+
messageId: 'useNewAPI',
|
60
|
+
fix(fixer) {
|
61
|
+
const openingParen = sourceCode.getTokenBefore(node.arguments[0]);
|
62
|
+
const closingParen = sourceCode.getLastToken(node);
|
63
|
+
const reportInfo = utils.getReportInfo(node, context);
|
64
|
+
|
65
|
+
if (!reportInfo) {
|
66
|
+
return null;
|
67
|
+
}
|
68
|
+
|
69
|
+
return fixer.replaceTextRange(
|
70
|
+
[openingParen.range[1], closingParen.range[0]],
|
71
|
+
`{${Object.keys(reportInfo)
|
72
|
+
.map(
|
73
|
+
(key) => `${key}: ${sourceCode.getText(reportInfo[key])}`
|
74
|
+
)
|
75
|
+
.join(', ')}}`
|
76
|
+
);
|
77
|
+
},
|
78
|
+
});
|
79
|
+
}
|
80
|
+
},
|
81
|
+
};
|
82
|
+
},
|
83
|
+
};
|
@@ -0,0 +1,87 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview disallow identical tests
|
3
|
+
* @author 薛定谔的猫<hh_2013@foxmail.com>
|
4
|
+
*/
|
5
|
+
|
6
|
+
'use strict';
|
7
|
+
|
8
|
+
const utils = require('../utils');
|
9
|
+
|
10
|
+
// ------------------------------------------------------------------------------
|
11
|
+
// Rule Definition
|
12
|
+
// ------------------------------------------------------------------------------
|
13
|
+
|
14
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
15
|
+
module.exports = {
|
16
|
+
meta: {
|
17
|
+
type: 'problem',
|
18
|
+
docs: {
|
19
|
+
description: 'disallow identical tests',
|
20
|
+
category: 'Tests',
|
21
|
+
recommended: true,
|
22
|
+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-identical-tests.md',
|
23
|
+
},
|
24
|
+
fixable: 'code',
|
25
|
+
schema: [],
|
26
|
+
messages: {
|
27
|
+
identical: 'This test case is identical to another case.',
|
28
|
+
},
|
29
|
+
},
|
30
|
+
|
31
|
+
create(context) {
|
32
|
+
// ----------------------------------------------------------------------
|
33
|
+
// Public
|
34
|
+
// ----------------------------------------------------------------------
|
35
|
+
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
|
36
|
+
|
37
|
+
// ----------------------------------------------------------------------
|
38
|
+
// Helpers
|
39
|
+
// ----------------------------------------------------------------------
|
40
|
+
/**
|
41
|
+
* Create a unique cache key
|
42
|
+
* @param {object} test
|
43
|
+
* @returns {string}
|
44
|
+
*/
|
45
|
+
function toKey(test) {
|
46
|
+
if (test.type !== 'ObjectExpression') {
|
47
|
+
return JSON.stringify([test.type, sourceCode.getText(test)]);
|
48
|
+
}
|
49
|
+
return JSON.stringify([
|
50
|
+
test.type,
|
51
|
+
...test.properties.map((p) => sourceCode.getText(p)).sort(),
|
52
|
+
]);
|
53
|
+
}
|
54
|
+
|
55
|
+
return {
|
56
|
+
Program(ast) {
|
57
|
+
utils.getTestInfo(context, ast).forEach((testRun) => {
|
58
|
+
[testRun.valid, testRun.invalid].forEach((tests) => {
|
59
|
+
const cache = new Set();
|
60
|
+
tests.forEach((test) => {
|
61
|
+
const key = toKey(test);
|
62
|
+
if (cache.has(key)) {
|
63
|
+
context.report({
|
64
|
+
node: test,
|
65
|
+
messageId: 'identical',
|
66
|
+
fix(fixer) {
|
67
|
+
const start = sourceCode.getTokenBefore(test);
|
68
|
+
const end = sourceCode.getTokenAfter(test);
|
69
|
+
return fixer.removeRange(
|
70
|
+
// should remove test's trailing comma
|
71
|
+
[
|
72
|
+
start.range[1],
|
73
|
+
end.value === ',' ? end.range[1] : test.range[1],
|
74
|
+
]
|
75
|
+
);
|
76
|
+
},
|
77
|
+
});
|
78
|
+
} else {
|
79
|
+
cache.add(key);
|
80
|
+
}
|
81
|
+
});
|
82
|
+
});
|
83
|
+
});
|
84
|
+
},
|
85
|
+
};
|
86
|
+
},
|
87
|
+
};
|
@@ -0,0 +1,101 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
const utils = require('../utils');
|
4
|
+
|
5
|
+
// ------------------------------------------------------------------------------
|
6
|
+
// Rule Definition
|
7
|
+
// ------------------------------------------------------------------------------
|
8
|
+
|
9
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
10
|
+
module.exports = {
|
11
|
+
meta: {
|
12
|
+
type: 'problem',
|
13
|
+
docs: {
|
14
|
+
description:
|
15
|
+
'disallow `messageId`s that are missing from `meta.messages`',
|
16
|
+
category: 'Rules',
|
17
|
+
recommended: true,
|
18
|
+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-missing-message-ids.md',
|
19
|
+
},
|
20
|
+
fixable: null,
|
21
|
+
schema: [],
|
22
|
+
messages: {
|
23
|
+
missingMessage:
|
24
|
+
'`meta.messages` is missing the messageId "{{messageId}}".',
|
25
|
+
},
|
26
|
+
},
|
27
|
+
|
28
|
+
create(context) {
|
29
|
+
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
|
30
|
+
const { scopeManager } = sourceCode;
|
31
|
+
const ruleInfo = utils.getRuleInfo(sourceCode);
|
32
|
+
if (!ruleInfo) {
|
33
|
+
return {};
|
34
|
+
}
|
35
|
+
|
36
|
+
const messagesNode = utils.getMessagesNode(ruleInfo, scopeManager);
|
37
|
+
|
38
|
+
let contextIdentifiers;
|
39
|
+
|
40
|
+
if (!messagesNode || messagesNode.type !== 'ObjectExpression') {
|
41
|
+
// If we can't find `meta.messages`, disable the rule.
|
42
|
+
return {};
|
43
|
+
}
|
44
|
+
|
45
|
+
return {
|
46
|
+
Program(ast) {
|
47
|
+
contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
|
48
|
+
},
|
49
|
+
|
50
|
+
CallExpression(node) {
|
51
|
+
const scope = sourceCode.getScope?.(node) || context.getScope(); // TODO: just use sourceCode.getScope() when we drop support for ESLint < 9.0.0
|
52
|
+
// Check for messageId properties used in known calls to context.report();
|
53
|
+
if (
|
54
|
+
node.callee.type === 'MemberExpression' &&
|
55
|
+
contextIdentifiers.has(node.callee.object) &&
|
56
|
+
node.callee.property.type === 'Identifier' &&
|
57
|
+
node.callee.property.name === 'report'
|
58
|
+
) {
|
59
|
+
const reportInfo = utils.getReportInfo(node, context);
|
60
|
+
if (!reportInfo) {
|
61
|
+
return;
|
62
|
+
}
|
63
|
+
|
64
|
+
const reportMessagesAndDataArray =
|
65
|
+
utils.collectReportViolationAndSuggestionData(reportInfo);
|
66
|
+
for (const { messageId } of reportMessagesAndDataArray.filter(
|
67
|
+
(obj) => obj.messageId
|
68
|
+
)) {
|
69
|
+
const values =
|
70
|
+
messageId.type === 'Literal'
|
71
|
+
? [messageId]
|
72
|
+
: utils.findPossibleVariableValues(messageId, scopeManager);
|
73
|
+
|
74
|
+
// Look for any possible string values we found for this messageId.
|
75
|
+
values.forEach((val) => {
|
76
|
+
if (
|
77
|
+
val.type === 'Literal' &&
|
78
|
+
typeof val.value === 'string' &&
|
79
|
+
val.value !== '' &&
|
80
|
+
!utils.getMessageIdNodeById(
|
81
|
+
val.value,
|
82
|
+
ruleInfo,
|
83
|
+
scopeManager,
|
84
|
+
scope
|
85
|
+
)
|
86
|
+
)
|
87
|
+
// Couldn't find this messageId in `meta.messages`.
|
88
|
+
context.report({
|
89
|
+
node: val,
|
90
|
+
messageId: 'missingMessage',
|
91
|
+
data: {
|
92
|
+
messageId: val.value,
|
93
|
+
},
|
94
|
+
});
|
95
|
+
});
|
96
|
+
}
|
97
|
+
}
|
98
|
+
},
|
99
|
+
};
|
100
|
+
},
|
101
|
+
};
|
@@ -0,0 +1,131 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview Disallow missing placeholders in rule report messages
|
3
|
+
* @author Teddy Katz
|
4
|
+
*/
|
5
|
+
|
6
|
+
'use strict';
|
7
|
+
|
8
|
+
const utils = require('../utils');
|
9
|
+
const { getStaticValue } = require('eslint-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: 'disallow missing placeholders in rule report messages',
|
21
|
+
category: 'Rules',
|
22
|
+
recommended: true,
|
23
|
+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-missing-placeholders.md',
|
24
|
+
},
|
25
|
+
fixable: null,
|
26
|
+
schema: [],
|
27
|
+
messages: {
|
28
|
+
placeholderDoesNotExist:
|
29
|
+
"The placeholder {{{{missingKey}}}} is missing (must provide it in the report's `data` object).",
|
30
|
+
},
|
31
|
+
},
|
32
|
+
|
33
|
+
create(context) {
|
34
|
+
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
|
35
|
+
const { scopeManager } = sourceCode;
|
36
|
+
|
37
|
+
let contextIdentifiers;
|
38
|
+
|
39
|
+
const ruleInfo = utils.getRuleInfo(sourceCode);
|
40
|
+
if (!ruleInfo) {
|
41
|
+
return {};
|
42
|
+
}
|
43
|
+
|
44
|
+
const messagesNode = utils.getMessagesNode(ruleInfo, scopeManager);
|
45
|
+
|
46
|
+
return {
|
47
|
+
Program(ast) {
|
48
|
+
contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
|
49
|
+
},
|
50
|
+
CallExpression(node) {
|
51
|
+
const scope = sourceCode.getScope?.(node) || context.getScope(); // TODO: just use sourceCode.getScope() when we drop support for ESLint < 9.0.0
|
52
|
+
if (
|
53
|
+
node.callee.type === 'MemberExpression' &&
|
54
|
+
contextIdentifiers.has(node.callee.object) &&
|
55
|
+
node.callee.property.type === 'Identifier' &&
|
56
|
+
node.callee.property.name === 'report'
|
57
|
+
) {
|
58
|
+
const reportInfo = utils.getReportInfo(node, context);
|
59
|
+
if (!reportInfo) {
|
60
|
+
return;
|
61
|
+
}
|
62
|
+
|
63
|
+
const reportMessagesAndDataArray =
|
64
|
+
utils.collectReportViolationAndSuggestionData(reportInfo);
|
65
|
+
|
66
|
+
if (messagesNode) {
|
67
|
+
// Check for any potential instances where we can use the messageId to fill in the message for convenience.
|
68
|
+
reportMessagesAndDataArray.forEach((obj) => {
|
69
|
+
if (
|
70
|
+
!obj.message &&
|
71
|
+
obj.messageId &&
|
72
|
+
obj.messageId.type === 'Literal' &&
|
73
|
+
typeof obj.messageId.value === 'string'
|
74
|
+
) {
|
75
|
+
const correspondingMessage = utils.getMessageIdNodeById(
|
76
|
+
obj.messageId.value,
|
77
|
+
ruleInfo,
|
78
|
+
scopeManager,
|
79
|
+
scope
|
80
|
+
);
|
81
|
+
if (correspondingMessage) {
|
82
|
+
obj.message = correspondingMessage.value;
|
83
|
+
}
|
84
|
+
}
|
85
|
+
});
|
86
|
+
}
|
87
|
+
|
88
|
+
for (const {
|
89
|
+
message,
|
90
|
+
messageId,
|
91
|
+
data,
|
92
|
+
} of reportMessagesAndDataArray.filter((obj) => obj.message)) {
|
93
|
+
const messageStaticValue = getStaticValue(message, scope);
|
94
|
+
if (
|
95
|
+
((message.type === 'Literal' &&
|
96
|
+
typeof message.value === 'string') ||
|
97
|
+
(messageStaticValue &&
|
98
|
+
typeof messageStaticValue.value === 'string')) &&
|
99
|
+
(!data || data.type === 'ObjectExpression')
|
100
|
+
) {
|
101
|
+
// Same regex as the one ESLint uses
|
102
|
+
// https://github.com/eslint/eslint/blob/e5446449d93668ccbdb79d78cc69f165ce4fde07/lib/eslint.js#L990
|
103
|
+
const PLACEHOLDER_MATCHER = /{{\s*([^{}]+?)\s*}}/g;
|
104
|
+
let match;
|
105
|
+
|
106
|
+
while (
|
107
|
+
(match = PLACEHOLDER_MATCHER.exec(
|
108
|
+
message.value || messageStaticValue.value
|
109
|
+
))
|
110
|
+
) {
|
111
|
+
const matchingProperty =
|
112
|
+
data &&
|
113
|
+
data.properties.find(
|
114
|
+
(prop) => utils.getKeyName(prop) === match[1]
|
115
|
+
);
|
116
|
+
|
117
|
+
if (!matchingProperty) {
|
118
|
+
context.report({
|
119
|
+
node: data || messageId || message,
|
120
|
+
messageId: 'placeholderDoesNotExist',
|
121
|
+
data: { missingKey: match[1] },
|
122
|
+
});
|
123
|
+
}
|
124
|
+
}
|
125
|
+
}
|
126
|
+
}
|
127
|
+
}
|
128
|
+
},
|
129
|
+
};
|
130
|
+
},
|
131
|
+
};
|