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