forked from eslint-community/eslint-plugin-eslint-plugin
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathno-useless-token-range.js
138 lines (126 loc) · 5.91 KB
/
no-useless-token-range.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/**
* @fileoverview Disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken
* @author Teddy Katz
*/
'use strict';
const utils = require('../utils');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken',
category: 'Rules',
recommended: true,
},
type: 'suggestion',
fixable: 'code',
schema: [],
},
create (context) {
const sourceCode = context.getSourceCode();
// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------
/**
* Determines whether a second argument to getFirstToken or getLastToken changes the output of the function.
* This occurs when the second argument exists and is not an object literal, or has keys other than `includeComments`.
* @param {ASTNode} arg The second argument to `sourceCode.getFirstToken` or `sourceCode.getLastToken`
* @returns {boolean} `true` if the argument affects the output of getFirstToken or getLastToken
*/
function affectsGetTokenOutput (arg) {
if (!arg) {
return false;
}
if (arg.type !== 'ObjectExpression') {
return true;
}
return arg.properties.length >= 2 || (
arg.properties.length === 1 && (
utils.getKeyName(arg.properties[0]) !== 'includeComments' ||
arg.properties[0].value.type !== 'Literal'
));
}
/**
* Determines whether a node is a MemberExpression that accesses the `range` property
* @param {ASTNode} node The node
* @returns {boolean} `true` if the node is a MemberExpression that accesses the `range` property
*/
function isRangeAccess (node) {
return node.type === 'MemberExpression' && node.property.type === 'Identifier' && node.property.name === 'range';
}
/**
* Determines whether a MemberExpression accesses the `start` property (either `.range[0]` or `.start`).
* Note that this will also work correctly if the `.range` MemberExpression is passed.
* @param {ASTNode} memberExpression The MemberExpression node to check
* @returns {boolean} `true` if this node accesses either `.range[0]` or `.start`
*/
function isStartAccess (memberExpression) {
if (isRangeAccess(memberExpression) && memberExpression.parent.type === 'MemberExpression') {
return isStartAccess(memberExpression.parent);
}
return (
(memberExpression.property.type === 'Identifier' && memberExpression.property.name === 'start') ||
(
memberExpression.computed && memberExpression.property.type === 'Literal' && memberExpression.property.value === 0 &&
isRangeAccess(memberExpression.object)
)
);
}
/**
* Determines whether a MemberExpression accesses the `start` property (either `.range[1]` or `.end`).
* Note that this will also work correctly if the `.range` MemberExpression is passed.
* @param {ASTNode} memberExpression The MemberExpression node to check
* @returns {boolean} `true` if this node accesses either `.range[1]` or `.end`
*/
function isEndAccess (memberExpression) {
if (isRangeAccess(memberExpression) && memberExpression.parent.type === 'MemberExpression') {
return isEndAccess(memberExpression.parent);
}
return (
(memberExpression.property.type === 'Identifier' && memberExpression.property.name === 'end') ||
(
memberExpression.computed && memberExpression.property.type === 'Literal' && memberExpression.property.value === 1 &&
isRangeAccess(memberExpression.object)
)
);
}
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return {
'Program:exit' (ast) {
Array.from(utils.getSourceCodeIdentifiers(context, ast))
.filter(identifier => identifier.parent.type === 'MemberExpression' &&
identifier.parent.object === identifier &&
identifier.parent.property.type === 'Identifier' &&
identifier.parent.parent.type === 'CallExpression' &&
identifier.parent === identifier.parent.parent.callee &&
identifier.parent.parent.arguments.length <= 2 &&
!affectsGetTokenOutput(identifier.parent.parent.arguments[1]) &&
identifier.parent.parent.parent.type === 'MemberExpression' &&
identifier.parent.parent === identifier.parent.parent.parent.object && (
(isStartAccess(identifier.parent.parent.parent) && identifier.parent.property.name === 'getFirstToken') ||
(isEndAccess(identifier.parent.parent.parent) && identifier.parent.property.name === 'getLastToken'))
)
.forEach(identifier => {
const fullRangeAccess = isRangeAccess(identifier.parent.parent.parent)
? identifier.parent.parent.parent.parent
: identifier.parent.parent.parent;
const replacementText = sourceCode.text.slice(fullRangeAccess.range[0], identifier.parent.parent.range[0]) +
sourceCode.getText(identifier.parent.parent.arguments[0]) +
sourceCode.text.slice(identifier.parent.parent.range[1], fullRangeAccess.range[1]);
context.report({
node: identifier.parent.parent,
message: "Use '{{replacementText}}' instead.",
data: { replacementText },
fix (fixer) {
return fixer.replaceText(identifier.parent.parent, sourceCode.getText(identifier.parent.parent.arguments[0]));
},
});
});
},
};
},
};