/
prefer-modern-dom-apis.js
158 lines (132 loc) · 4.41 KB
/
prefer-modern-dom-apis.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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
'use strict';
const getDocumentationUrl = require('./utils/get-documentation-url');
const getArgumentNameForReplaceChildOrInsertBefore = nodeArguments => {
if (nodeArguments.type === 'Identifier') {
return nodeArguments.name;
}
};
const forbiddenIdentifierNames = new Map([
['replaceChild', 'replaceWith'],
['insertBefore', 'before']
]);
const isPartOfVariableAssignment = nodeParentType => {
if (nodeParentType === 'VariableDeclarator' || nodeParentType === 'AssignmentExpression') {
return true;
}
return false;
};
const checkForReplaceChildOrInsertBefore = (context, node) => {
const identifierName = node.callee.property.name;
// Return early when specified methods don't exist in forbiddenIdentifierNames
if (!forbiddenIdentifierNames.has(identifierName)) {
return;
}
const nodeArguments = node.arguments;
const newChildNodeArgument = getArgumentNameForReplaceChildOrInsertBefore(
nodeArguments[0]
);
const oldChildNodeArgument = getArgumentNameForReplaceChildOrInsertBefore(
nodeArguments[1]
);
// Return early in case that one of the provided arguments is not a node
if (!newChildNodeArgument || !oldChildNodeArgument) {
return;
}
const parentNode = node.callee.object.name;
// This check makes sure that only the first method of chained methods with same identifier name e.g: parentNode.insertBefore(alfa, beta).insertBefore(charlie, delta); gets transformed
if (!parentNode) {
return;
}
const preferredSelector = forbiddenIdentifierNames.get(identifierName);
let fix = fixer => fixer.replaceText(
node,
`${oldChildNodeArgument}.${preferredSelector}(${newChildNodeArgument})`
);
// Report error when the method is part of a variable assignment
// but don't offer to autofix `.replaceWith()` and `.before()`
// which don't have a return value.
if (isPartOfVariableAssignment(node.parent.type)) {
fix = undefined;
}
return context.report({
node,
message: `Prefer \`${oldChildNodeArgument}.${preferredSelector}(${newChildNodeArgument})\` over \`${parentNode}.${identifierName}(${newChildNodeArgument}, ${oldChildNodeArgument})\`.`,
fix
});
};
// Handle both `Identifier` and `Literal` because the preferred selectors support nodes and DOMString.
const getArgumentNameForInsertAdjacentMethods = nodeArguments => {
if (nodeArguments.type === 'Identifier') {
return nodeArguments.name;
}
if (nodeArguments.type === 'Literal') {
return nodeArguments.raw;
}
};
const positionReplacers = new Map([
['beforebegin', 'before'],
['afterbegin', 'prepend'],
['beforeend', 'append'],
['afterend', 'after']
]);
const checkForInsertAdjacentTextOrInsertAdjacentElement = (context, node) => {
const identifierName = node.callee.property.name;
// Return early when method name is not one of the targeted ones.
if (
identifierName !== 'insertAdjacentText' &&
identifierName !== 'insertAdjacentElement'
) {
return;
}
const nodeArguments = node.arguments;
const positionArgument = getArgumentNameForInsertAdjacentMethods(nodeArguments[0]);
const positionAsValue = nodeArguments[0].value;
// Return early when specified position value of first argument is not a recognized value.
if (!positionReplacers.has(positionAsValue)) {
return;
}
const referenceNode = node.callee.object.name;
const preferredSelector = positionReplacers.get(positionAsValue);
const insertedTextArgument = getArgumentNameForInsertAdjacentMethods(
nodeArguments[1]
);
let fix = fixer =>
fixer.replaceText(
node,
`${referenceNode}.${preferredSelector}(${insertedTextArgument})`
);
// Report error when the method is part of a variable assignment
// but don't offer to autofix `.insertAdjacentElement()`
// which don't have a return value.
if (identifierName === 'insertAdjacentElement' && isPartOfVariableAssignment(node.parent.type)) {
fix = undefined;
}
return context.report({
node,
message: `Prefer \`${referenceNode}.${preferredSelector}(${insertedTextArgument})\` over \`${referenceNode}.${identifierName}(${positionArgument}, ${insertedTextArgument})\`.`,
fix
});
};
const create = context => {
return {
CallExpression(node) {
if (
node.callee.type === 'MemberExpression' &&
node.arguments.length === 2
) {
checkForReplaceChildOrInsertBefore(context, node);
checkForInsertAdjacentTextOrInsertAdjacentElement(context, node);
}
}
};
};
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocumentationUrl(__filename)
},
fixable: 'code'
}
};