@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
@@ -0,0 +1,99 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
const utils = require('../utils');
|
4
|
+
const {
|
5
|
+
isCommaToken,
|
6
|
+
isOpeningBraceToken,
|
7
|
+
isClosingBraceToken,
|
8
|
+
} = require('eslint-utils');
|
9
|
+
|
10
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
11
|
+
module.exports = {
|
12
|
+
meta: {
|
13
|
+
type: 'problem',
|
14
|
+
docs: {
|
15
|
+
description: 'disallow the test case property `only`',
|
16
|
+
category: 'Tests',
|
17
|
+
recommended: true,
|
18
|
+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-only-tests.md',
|
19
|
+
},
|
20
|
+
hasSuggestions: true,
|
21
|
+
schema: [],
|
22
|
+
messages: {
|
23
|
+
foundOnly:
|
24
|
+
'The test case property `only` can be used during development, but should not be checked-in, since it prevents all the tests from running.',
|
25
|
+
removeOnly: 'Remove `only`.',
|
26
|
+
},
|
27
|
+
},
|
28
|
+
|
29
|
+
create(context) {
|
30
|
+
return {
|
31
|
+
Program(ast) {
|
32
|
+
for (const testRun of utils.getTestInfo(context, ast)) {
|
33
|
+
for (const test of [...testRun.valid, ...testRun.invalid]) {
|
34
|
+
if (test.type === 'ObjectExpression') {
|
35
|
+
// Test case object: { code: 'const x = 123;', ... }
|
36
|
+
|
37
|
+
const onlyProperty = test.properties.find(
|
38
|
+
(property) =>
|
39
|
+
property.type === 'Property' &&
|
40
|
+
property.key.type === 'Identifier' &&
|
41
|
+
property.key.name === 'only' &&
|
42
|
+
property.value.type === 'Literal' &&
|
43
|
+
property.value.value
|
44
|
+
);
|
45
|
+
|
46
|
+
if (onlyProperty) {
|
47
|
+
context.report({
|
48
|
+
node: onlyProperty,
|
49
|
+
messageId: 'foundOnly',
|
50
|
+
suggest: [
|
51
|
+
{
|
52
|
+
messageId: 'removeOnly',
|
53
|
+
*fix(fixer) {
|
54
|
+
const sourceCode =
|
55
|
+
context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
|
56
|
+
|
57
|
+
const tokenBefore =
|
58
|
+
sourceCode.getTokenBefore(onlyProperty);
|
59
|
+
const tokenAfter =
|
60
|
+
sourceCode.getTokenAfter(onlyProperty);
|
61
|
+
if (
|
62
|
+
(isCommaToken(tokenBefore) &&
|
63
|
+
isCommaToken(tokenAfter)) || // In middle of properties
|
64
|
+
(isOpeningBraceToken(tokenBefore) &&
|
65
|
+
isCommaToken(tokenAfter)) // At beginning of properties
|
66
|
+
) {
|
67
|
+
yield fixer.remove(tokenAfter); // Remove extra comma.
|
68
|
+
}
|
69
|
+
if (
|
70
|
+
isCommaToken(tokenBefore) &&
|
71
|
+
isClosingBraceToken(tokenAfter)
|
72
|
+
) {
|
73
|
+
// At end of properties
|
74
|
+
yield fixer.remove(tokenBefore); // Remove extra comma.
|
75
|
+
}
|
76
|
+
|
77
|
+
yield fixer.remove(onlyProperty);
|
78
|
+
},
|
79
|
+
},
|
80
|
+
],
|
81
|
+
});
|
82
|
+
}
|
83
|
+
} else if (
|
84
|
+
test.type === 'CallExpression' &&
|
85
|
+
test.callee.type === 'MemberExpression' &&
|
86
|
+
test.callee.object.type === 'Identifier' &&
|
87
|
+
test.callee.object.name === 'RuleTester' &&
|
88
|
+
test.callee.property.type === 'Identifier' &&
|
89
|
+
test.callee.property.name === 'only'
|
90
|
+
) {
|
91
|
+
// RuleTester.only('const x = 123;');
|
92
|
+
context.report({ node: test.callee, messageId: 'foundOnly' });
|
93
|
+
}
|
94
|
+
}
|
95
|
+
}
|
96
|
+
},
|
97
|
+
};
|
98
|
+
},
|
99
|
+
};
|
@@ -0,0 +1,86 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
const typedNodeSourceFileTesters = [
|
4
|
+
/@types[/\\]estree[/\\]index\.d\.ts/,
|
5
|
+
/@typescript-eslint[/\\]types[/\\]dist[/\\]generated[/\\]ast-spec\.d\.ts/,
|
6
|
+
];
|
7
|
+
|
8
|
+
/**
|
9
|
+
* Given a TypeScript type, determines whether the type appears to be for a known
|
10
|
+
* AST type from the typings of @typescript-eslint/types or estree.
|
11
|
+
* We check based on two rough conditions:
|
12
|
+
* - The type has a 'kind' property (as AST node types all do)
|
13
|
+
* - The type is declared in one of those package's .d.ts types
|
14
|
+
*
|
15
|
+
* @example
|
16
|
+
* ```
|
17
|
+
* module.exports = {
|
18
|
+
* create(context) {
|
19
|
+
* BinaryExpression(node) {
|
20
|
+
* const type = services.getTypeAtLocation(node.right);
|
21
|
+
* // ^^^^
|
22
|
+
* // This variable's type will be TSESTree.BinaryExpression
|
23
|
+
* }
|
24
|
+
* }
|
25
|
+
* }
|
26
|
+
* ```
|
27
|
+
*
|
28
|
+
* @param {import('typescript').Type} type
|
29
|
+
* @returns Whether the type seems to include a known ESTree or TSESTree AST node.
|
30
|
+
*/
|
31
|
+
function isAstNodeType(type) {
|
32
|
+
return (type.types || [type])
|
33
|
+
.filter((typePart) => typePart.getProperty('type'))
|
34
|
+
.flatMap(
|
35
|
+
(typePart) => (typePart.symbol && typePart.symbol.declarations) || []
|
36
|
+
)
|
37
|
+
.some((declaration) => {
|
38
|
+
const fileName = declaration.getSourceFile().fileName;
|
39
|
+
return (
|
40
|
+
fileName &&
|
41
|
+
typedNodeSourceFileTesters.some((tester) => tester.test(fileName))
|
42
|
+
);
|
43
|
+
});
|
44
|
+
}
|
45
|
+
|
46
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
47
|
+
module.exports = {
|
48
|
+
meta: {
|
49
|
+
type: 'suggestion',
|
50
|
+
docs: {
|
51
|
+
description:
|
52
|
+
'disallow using `in` to narrow node types instead of looking at properties',
|
53
|
+
category: 'Rules',
|
54
|
+
recommended: false,
|
55
|
+
requiresTypeChecking: true,
|
56
|
+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-property-in-node.md',
|
57
|
+
},
|
58
|
+
schema: [],
|
59
|
+
messages: {
|
60
|
+
in: 'Prefer checking specific node properties instead of a broad `in`.',
|
61
|
+
},
|
62
|
+
},
|
63
|
+
|
64
|
+
create(context) {
|
65
|
+
return {
|
66
|
+
'BinaryExpression[operator=in]'(node) {
|
67
|
+
// TODO: Switch this to ESLintUtils.getParserServices with typescript-eslint@>=6
|
68
|
+
// https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/269
|
69
|
+
const services = (context.sourceCode || context).parserServices;
|
70
|
+
if (!services.program) {
|
71
|
+
throw new Error(
|
72
|
+
'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.'
|
73
|
+
);
|
74
|
+
}
|
75
|
+
|
76
|
+
const checker = services.program.getTypeChecker();
|
77
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(node.right);
|
78
|
+
const type = checker.getTypeAtLocation(tsNode);
|
79
|
+
|
80
|
+
if (isAstNodeType(type)) {
|
81
|
+
context.report({ messageId: 'in', node });
|
82
|
+
}
|
83
|
+
},
|
84
|
+
};
|
85
|
+
},
|
86
|
+
};
|
@@ -0,0 +1,139 @@
|
|
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: 'disallow unused `messageId`s in `meta.messages`',
|
15
|
+
category: 'Rules',
|
16
|
+
recommended: true,
|
17
|
+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-unused-message-ids.md',
|
18
|
+
},
|
19
|
+
fixable: null,
|
20
|
+
schema: [],
|
21
|
+
messages: {
|
22
|
+
unusedMessage: 'The messageId "{{messageId}}" is never used.',
|
23
|
+
},
|
24
|
+
},
|
25
|
+
|
26
|
+
create(context) {
|
27
|
+
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
|
28
|
+
const { scopeManager } = sourceCode;
|
29
|
+
const ruleInfo = utils.getRuleInfo(sourceCode);
|
30
|
+
if (!ruleInfo) {
|
31
|
+
return {};
|
32
|
+
}
|
33
|
+
|
34
|
+
const messageIdsUsed = new Set();
|
35
|
+
let contextIdentifiers;
|
36
|
+
let hasSeenUnknownMessageId = false;
|
37
|
+
let hasSeenViolationReport = false;
|
38
|
+
|
39
|
+
const messageIdNodes = utils.getMessageIdNodes(ruleInfo, scopeManager);
|
40
|
+
if (!messageIdNodes) {
|
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
|
+
'Program:exit'(ast) {
|
51
|
+
if (hasSeenUnknownMessageId || !hasSeenViolationReport) {
|
52
|
+
/*
|
53
|
+
Bail out when the rule is likely to have false positives.
|
54
|
+
- If we saw a dynamic/unknown messageId
|
55
|
+
- If we couldn't find any violation reporting code, likely because a helper function from an external file is handling this
|
56
|
+
*/
|
57
|
+
return;
|
58
|
+
}
|
59
|
+
|
60
|
+
const scope = sourceCode.getScope?.(ast) || context.getScope(); // TODO: just use sourceCode.getScope() when we drop support for ESLint < 9.0.0
|
61
|
+
|
62
|
+
const messageIdNodesUnused = messageIdNodes.filter(
|
63
|
+
(node) => !messageIdsUsed.has(utils.getKeyName(node, scope))
|
64
|
+
);
|
65
|
+
|
66
|
+
// Report any messageIds that were never used.
|
67
|
+
for (const messageIdNode of messageIdNodesUnused) {
|
68
|
+
context.report({
|
69
|
+
node: messageIdNode,
|
70
|
+
messageId: 'unusedMessage',
|
71
|
+
data: {
|
72
|
+
messageId: utils.getKeyName(messageIdNode, scope),
|
73
|
+
},
|
74
|
+
});
|
75
|
+
}
|
76
|
+
},
|
77
|
+
|
78
|
+
CallExpression(node) {
|
79
|
+
// Check for messageId properties used in known calls to context.report();
|
80
|
+
if (
|
81
|
+
node.callee.type === 'MemberExpression' &&
|
82
|
+
contextIdentifiers.has(node.callee.object) &&
|
83
|
+
node.callee.property.type === 'Identifier' &&
|
84
|
+
node.callee.property.name === 'report'
|
85
|
+
) {
|
86
|
+
const reportInfo = utils.getReportInfo(node, context);
|
87
|
+
if (!reportInfo) {
|
88
|
+
return;
|
89
|
+
}
|
90
|
+
|
91
|
+
hasSeenViolationReport = true;
|
92
|
+
|
93
|
+
const reportMessagesAndDataArray =
|
94
|
+
utils.collectReportViolationAndSuggestionData(reportInfo);
|
95
|
+
for (const { messageId } of reportMessagesAndDataArray.filter(
|
96
|
+
(obj) => obj.messageId
|
97
|
+
)) {
|
98
|
+
const values =
|
99
|
+
messageId.type === 'Literal'
|
100
|
+
? [messageId]
|
101
|
+
: utils.findPossibleVariableValues(messageId, scopeManager);
|
102
|
+
if (
|
103
|
+
values.length === 0 ||
|
104
|
+
values.some((val) => val.type !== 'Literal')
|
105
|
+
) {
|
106
|
+
// When a dynamic messageId is used and we can't detect its value, disable the rule to avoid false positives.
|
107
|
+
hasSeenUnknownMessageId = true;
|
108
|
+
}
|
109
|
+
values.forEach((val) => messageIdsUsed.add(val.value));
|
110
|
+
}
|
111
|
+
}
|
112
|
+
},
|
113
|
+
|
114
|
+
Property(node) {
|
115
|
+
// In order to reduce false positives, we will also check for messageId properties anywhere in the file.
|
116
|
+
// This is helpful especially in the event that helper functions are used for reporting violations.
|
117
|
+
if (node.key.type === 'Identifier' && node.key.name === 'messageId') {
|
118
|
+
hasSeenViolationReport = true;
|
119
|
+
|
120
|
+
const values =
|
121
|
+
node.value.type === 'Literal'
|
122
|
+
? [node.value]
|
123
|
+
: utils.findPossibleVariableValues(node.value, scopeManager);
|
124
|
+
|
125
|
+
if (
|
126
|
+
values.length === 0 ||
|
127
|
+
values.some((val) => val.type !== 'Literal') ||
|
128
|
+
utils.isVariableFromParameter(node.value, scopeManager)
|
129
|
+
) {
|
130
|
+
// When a dynamic messageId is used and we can't detect its value, disable the rule to avoid false positives.
|
131
|
+
hasSeenUnknownMessageId = true;
|
132
|
+
}
|
133
|
+
|
134
|
+
values.forEach((val) => messageIdsUsed.add(val.value));
|
135
|
+
}
|
136
|
+
},
|
137
|
+
};
|
138
|
+
},
|
139
|
+
};
|
@@ -0,0 +1,127 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview Disallow unused placeholders in rule report messages
|
3
|
+
* @author 薛定谔的猫<hh_2013@foxmail.com>
|
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 unused 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-unused-placeholders.md',
|
24
|
+
},
|
25
|
+
fixable: null,
|
26
|
+
schema: [],
|
27
|
+
messages: {
|
28
|
+
placeholderUnused:
|
29
|
+
'The placeholder {{{{unusedKey}}}} is unused (does not exist in the actual message).',
|
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
|
+
const messagesNode = utils.getMessagesNode(ruleInfo, scopeManager);
|
44
|
+
|
45
|
+
return {
|
46
|
+
Program(ast) {
|
47
|
+
contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
|
48
|
+
},
|
49
|
+
CallExpression(node) {
|
50
|
+
const scope = sourceCode.getScope?.(node) || context.getScope(); // TODO: just use sourceCode.getScope() when we drop support for ESLint < 9.0.0
|
51
|
+
if (
|
52
|
+
node.callee.type === 'MemberExpression' &&
|
53
|
+
contextIdentifiers.has(node.callee.object) &&
|
54
|
+
node.callee.property.type === 'Identifier' &&
|
55
|
+
node.callee.property.name === 'report'
|
56
|
+
) {
|
57
|
+
const reportInfo = utils.getReportInfo(node, context);
|
58
|
+
if (!reportInfo) {
|
59
|
+
return;
|
60
|
+
}
|
61
|
+
|
62
|
+
const reportMessagesAndDataArray =
|
63
|
+
utils.collectReportViolationAndSuggestionData(reportInfo);
|
64
|
+
|
65
|
+
if (messagesNode) {
|
66
|
+
// Check for any potential instances where we can use the messageId to fill in the message for convenience.
|
67
|
+
reportMessagesAndDataArray.forEach((obj) => {
|
68
|
+
if (
|
69
|
+
!obj.message &&
|
70
|
+
obj.messageId &&
|
71
|
+
obj.messageId.type === 'Literal' &&
|
72
|
+
typeof obj.messageId.value === 'string'
|
73
|
+
) {
|
74
|
+
const correspondingMessage = utils.getMessageIdNodeById(
|
75
|
+
obj.messageId.value,
|
76
|
+
ruleInfo,
|
77
|
+
scopeManager,
|
78
|
+
scope
|
79
|
+
);
|
80
|
+
if (correspondingMessage) {
|
81
|
+
obj.message = correspondingMessage.value;
|
82
|
+
}
|
83
|
+
}
|
84
|
+
});
|
85
|
+
}
|
86
|
+
|
87
|
+
for (const { message, data } of reportMessagesAndDataArray.filter(
|
88
|
+
(obj) => obj.message
|
89
|
+
)) {
|
90
|
+
const messageStaticValue = getStaticValue(message, scope);
|
91
|
+
if (
|
92
|
+
((message.type === 'Literal' &&
|
93
|
+
typeof message.value === 'string') ||
|
94
|
+
(messageStaticValue &&
|
95
|
+
typeof messageStaticValue.value === 'string')) &&
|
96
|
+
data &&
|
97
|
+
data.type === 'ObjectExpression'
|
98
|
+
) {
|
99
|
+
const messageValue = message.value || messageStaticValue.value;
|
100
|
+
// https://github.com/eslint/eslint/blob/2874d75ed8decf363006db25aac2d5f8991bd969/lib/linter.js#L986
|
101
|
+
const PLACEHOLDER_MATCHER = /{{\s*([^{}]+?)\s*}}/g;
|
102
|
+
const placeholdersInMessage = new Set();
|
103
|
+
|
104
|
+
messageValue.replaceAll(
|
105
|
+
PLACEHOLDER_MATCHER,
|
106
|
+
(fullMatch, term) => {
|
107
|
+
placeholdersInMessage.add(term);
|
108
|
+
}
|
109
|
+
);
|
110
|
+
|
111
|
+
data.properties.forEach((prop) => {
|
112
|
+
const key = utils.getKeyName(prop);
|
113
|
+
if (!placeholdersInMessage.has(key)) {
|
114
|
+
context.report({
|
115
|
+
node: prop,
|
116
|
+
messageId: 'placeholderUnused',
|
117
|
+
data: { unusedKey: key },
|
118
|
+
});
|
119
|
+
}
|
120
|
+
});
|
121
|
+
}
|
122
|
+
}
|
123
|
+
}
|
124
|
+
},
|
125
|
+
};
|
126
|
+
},
|
127
|
+
};
|
@@ -0,0 +1,174 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview Disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()`
|
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 unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()`',
|
21
|
+
category: 'Rules',
|
22
|
+
recommended: true,
|
23
|
+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-useless-token-range.md',
|
24
|
+
},
|
25
|
+
fixable: 'code',
|
26
|
+
schema: [],
|
27
|
+
messages: {
|
28
|
+
useReplacement: "Use '{{replacementText}}' instead.",
|
29
|
+
},
|
30
|
+
},
|
31
|
+
|
32
|
+
create(context) {
|
33
|
+
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
|
34
|
+
|
35
|
+
// ----------------------------------------------------------------------
|
36
|
+
// Helpers
|
37
|
+
// ----------------------------------------------------------------------
|
38
|
+
|
39
|
+
/**
|
40
|
+
* Determines whether a second argument to getFirstToken or getLastToken changes the output of the function.
|
41
|
+
* This occurs when the second argument exists and is not an object literal, or has keys other than `includeComments`.
|
42
|
+
* @param {ASTNode} arg The second argument to `sourceCode.getFirstToken()` or `sourceCode.getLastToken()`
|
43
|
+
* @returns {boolean} `true` if the argument affects the output of getFirstToken or getLastToken
|
44
|
+
*/
|
45
|
+
function affectsGetTokenOutput(arg) {
|
46
|
+
if (!arg) {
|
47
|
+
return false;
|
48
|
+
}
|
49
|
+
if (arg.type !== 'ObjectExpression') {
|
50
|
+
return true;
|
51
|
+
}
|
52
|
+
return (
|
53
|
+
arg.properties.length >= 2 ||
|
54
|
+
(arg.properties.length === 1 &&
|
55
|
+
(utils.getKeyName(arg.properties[0]) !== 'includeComments' ||
|
56
|
+
arg.properties[0].value.type !== 'Literal'))
|
57
|
+
);
|
58
|
+
}
|
59
|
+
|
60
|
+
/**
|
61
|
+
* Determines whether a node is a MemberExpression that accesses the `range` property
|
62
|
+
* @param {ASTNode} node The node
|
63
|
+
* @returns {boolean} `true` if the node is a MemberExpression that accesses the `range` property
|
64
|
+
*/
|
65
|
+
function isRangeAccess(node) {
|
66
|
+
return (
|
67
|
+
node.type === 'MemberExpression' &&
|
68
|
+
node.property.type === 'Identifier' &&
|
69
|
+
node.property.name === 'range'
|
70
|
+
);
|
71
|
+
}
|
72
|
+
|
73
|
+
/**
|
74
|
+
* Determines whether a MemberExpression accesses the `start` property (either `.range[0]` or `.start`).
|
75
|
+
* Note that this will also work correctly if the `.range` MemberExpression is passed.
|
76
|
+
* @param {ASTNode} memberExpression The MemberExpression node to check
|
77
|
+
* @returns {boolean} `true` if this node accesses either `.range[0]` or `.start`
|
78
|
+
*/
|
79
|
+
function isStartAccess(memberExpression) {
|
80
|
+
if (
|
81
|
+
isRangeAccess(memberExpression) &&
|
82
|
+
memberExpression.parent.type === 'MemberExpression'
|
83
|
+
) {
|
84
|
+
return isStartAccess(memberExpression.parent);
|
85
|
+
}
|
86
|
+
return (
|
87
|
+
(memberExpression.property.type === 'Identifier' &&
|
88
|
+
memberExpression.property.name === 'start') ||
|
89
|
+
(memberExpression.computed &&
|
90
|
+
memberExpression.property.type === 'Literal' &&
|
91
|
+
memberExpression.property.value === 0 &&
|
92
|
+
isRangeAccess(memberExpression.object))
|
93
|
+
);
|
94
|
+
}
|
95
|
+
|
96
|
+
/**
|
97
|
+
* Determines whether a MemberExpression accesses the `start` property (either `.range[1]` or `.end`).
|
98
|
+
* Note that this will also work correctly if the `.range` MemberExpression is passed.
|
99
|
+
* @param {ASTNode} memberExpression The MemberExpression node to check
|
100
|
+
* @returns {boolean} `true` if this node accesses either `.range[1]` or `.end`
|
101
|
+
*/
|
102
|
+
function isEndAccess(memberExpression) {
|
103
|
+
if (
|
104
|
+
isRangeAccess(memberExpression) &&
|
105
|
+
memberExpression.parent.type === 'MemberExpression'
|
106
|
+
) {
|
107
|
+
return isEndAccess(memberExpression.parent);
|
108
|
+
}
|
109
|
+
return (
|
110
|
+
(memberExpression.property.type === 'Identifier' &&
|
111
|
+
memberExpression.property.name === 'end') ||
|
112
|
+
(memberExpression.computed &&
|
113
|
+
memberExpression.property.type === 'Literal' &&
|
114
|
+
memberExpression.property.value === 1 &&
|
115
|
+
isRangeAccess(memberExpression.object))
|
116
|
+
);
|
117
|
+
}
|
118
|
+
|
119
|
+
// ----------------------------------------------------------------------
|
120
|
+
// Public
|
121
|
+
// ----------------------------------------------------------------------
|
122
|
+
|
123
|
+
return {
|
124
|
+
'Program:exit'(ast) {
|
125
|
+
[...utils.getSourceCodeIdentifiers(sourceCode.scopeManager, ast)]
|
126
|
+
.filter(
|
127
|
+
(identifier) =>
|
128
|
+
identifier.parent.type === 'MemberExpression' &&
|
129
|
+
identifier.parent.object === identifier &&
|
130
|
+
identifier.parent.property.type === 'Identifier' &&
|
131
|
+
identifier.parent.parent.type === 'CallExpression' &&
|
132
|
+
identifier.parent === identifier.parent.parent.callee &&
|
133
|
+
identifier.parent.parent.arguments.length <= 2 &&
|
134
|
+
!affectsGetTokenOutput(identifier.parent.parent.arguments[1]) &&
|
135
|
+
identifier.parent.parent.parent.type === 'MemberExpression' &&
|
136
|
+
identifier.parent.parent ===
|
137
|
+
identifier.parent.parent.parent.object &&
|
138
|
+
((isStartAccess(identifier.parent.parent.parent) &&
|
139
|
+
identifier.parent.property.name === 'getFirstToken') ||
|
140
|
+
(isEndAccess(identifier.parent.parent.parent) &&
|
141
|
+
identifier.parent.property.name === 'getLastToken'))
|
142
|
+
)
|
143
|
+
.forEach((identifier) => {
|
144
|
+
const fullRangeAccess = isRangeAccess(
|
145
|
+
identifier.parent.parent.parent
|
146
|
+
)
|
147
|
+
? identifier.parent.parent.parent.parent
|
148
|
+
: identifier.parent.parent.parent;
|
149
|
+
const replacementText =
|
150
|
+
sourceCode.text.slice(
|
151
|
+
fullRangeAccess.range[0],
|
152
|
+
identifier.parent.parent.range[0]
|
153
|
+
) +
|
154
|
+
sourceCode.getText(identifier.parent.parent.arguments[0]) +
|
155
|
+
sourceCode.text.slice(
|
156
|
+
identifier.parent.parent.range[1],
|
157
|
+
fullRangeAccess.range[1]
|
158
|
+
);
|
159
|
+
context.report({
|
160
|
+
node: identifier.parent.parent,
|
161
|
+
messageId: 'useReplacement',
|
162
|
+
data: { replacementText },
|
163
|
+
fix(fixer) {
|
164
|
+
return fixer.replaceText(
|
165
|
+
identifier.parent.parent,
|
166
|
+
sourceCode.getText(identifier.parent.parent.arguments[0])
|
167
|
+
);
|
168
|
+
},
|
169
|
+
});
|
170
|
+
});
|
171
|
+
},
|
172
|
+
};
|
173
|
+
},
|
174
|
+
};
|