-
Notifications
You must be signed in to change notification settings - Fork 3.9k
/
Copy pathquery-selector.js
230 lines (202 loc) · 6.69 KB
/
query-selector.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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
/**
* Copyright 2018 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const cssWhat = require('css-what');
module.exports = function (context) {
function callQuerySelector(node) {
const {callee} = node;
// If it's not a querySelector(All) call, I don't care about it.
const {property} = callee;
if (
property.type !== 'Identifier' ||
!property.name.startsWith('querySelector')
) {
return;
}
if (property.leadingComments) {
const ok = property.leadingComments.some((comment) => {
return comment.value === 'OK';
});
if (ok) {
return;
}
}
const selector = getSelector(node, 0);
if (!isValidSelector(selector)) {
return context.report({
node,
message: 'Failed to parse CSS Selector `' + selector + '`',
});
}
// What are we calling querySelector on?
let obj = callee.object;
if (obj.type === 'CallExpression') {
obj = obj.callee;
}
if (obj.type === 'MemberExpression') {
obj = obj.property;
}
// Any query selector is allowed on document
// This check must be done after getting the selector, to ensure the
// selector adheres to escaping requirements.
if (obj.type === 'Identifier' && /[dD]oc|[rR]oot/.test(obj.name)) {
return;
}
if (!selectorNeedsScope(selector)) {
return;
}
context.report({
node,
message:
'querySelector is not scoped to the element, but ' +
'globally and filtered to just the elements inside the element. ' +
'This leads to obscure bugs if you attempt to match a descendant ' +
'of a descendant (ie querySelector("div div")). Instead, use the ' +
'scopedQuerySelector in src/dom.js',
});
}
function callScopedQuerySelector(node) {
const {callee} = node;
if (!callee.name.startsWith('scopedQuerySelector')) {
return;
}
if (node.leadingComments) {
const ok = node.leadingComments.some((comment) => {
return comment.value === 'OK';
});
if (ok) {
return;
}
}
const selector = getSelector(node, 1);
if (!isValidSelector(selector)) {
return context.report({
node,
message: 'Failed to parse CSS Selector `' + selector + '`',
});
}
if (selectorNeedsScope(selector)) {
return;
}
context.report({
node,
message:
'using scopedQuerySelector here is actually ' +
"unnecessary, since you don't use child selector semantics.",
});
}
function getSelector(node, argIndex) {
const arg = node.arguments[argIndex];
let selector;
if (!arg) {
context.report({node, message: 'no argument to query selector'});
selector = 'dynamic value';
} else if (arg.type === 'Literal') {
selector = arg.value;
} else if (arg.type === 'TemplateLiteral') {
// Ensure all template variables are properly escaped.
let accumulator = '';
const quasis = arg.quasis.map((v) => v.value.raw);
for (let i = 0; i < arg.expressions.length; i++) {
const expression = arg.expressions[i];
accumulator += quasis[i];
if (expression.type === 'CallExpression') {
const {callee} = expression;
if (callee.type === 'Identifier') {
const inNthChild = /:nth-(last-)?(child|of-type|col)\([^)]*$/.test(
accumulator
);
if (callee.name === 'escapeCssSelectorIdent') {
// Add in a basic identifier to represent the call.
accumulator += 'foo';
if (inNthChild) {
context.report({
node: expression,
message:
'escapeCssSelectorIdent may not ' +
'be used inside an :nth-X psuedo-class. Please use ' +
'escapeCssSelectorNth instead.',
});
}
continue;
} else if (callee.name === 'escapeCssSelectorNth') {
// Add in a basic nth-selector to represent the call.
accumulator += '1';
if (!inNthChild) {
context.report({
node: expression,
message:
'escapeCssSelectorNth may only be ' +
'used inside an :nth-X psuedo-class. Please use ' +
'escapeCssSelectorIdent instead.',
});
}
continue;
}
}
}
context.report({
node: expression,
message:
'Each selector value must be escaped by ' +
'escapeCssSelectorIdent in src/css.js',
});
}
selector = accumulator + quasis[quasis.length - 1];
} else {
if (arg.type === 'BinaryExpression') {
context.report({node: arg, message: 'Use a template literal string'});
}
selector = 'dynamic value';
}
return selector;
}
function isValidSelector(selector) {
try {
cssWhat.parse(selector);
return true;
} catch (e) {
return false;
}
}
// Checks if the selector is using grandchild selector semantics
// `node.querySelector('child grandchild')` or `'child>grandchild'` But,
// specifically allow multi-selectors `'div, span'`.
function selectorNeedsScope(selector) {
// strip out things that can't affect children selection
selector = selector.replace(/\(.*\)|\[.*\]/, function (match) {
return match[0] + match[match.length - 1];
});
// This regex actually verifies there is no whitespace (implicit child
// semantics) or `>` chars (direct child semantics). The one exception is
// for `,` multi-selectors, which can have whitespace.
const noChildSemantics = /^(\s*,\s*|(?!\s|>).)*$/.test(selector);
return !noChildSemantics;
}
return {
CallExpression(node) {
if (/test-/.test(context.getFilename())) {
return;
}
const {callee} = node;
if (callee.type === 'MemberExpression') {
callQuerySelector(node);
} else if (callee.type === 'Identifier') {
callScopedQuerySelector(node);
}
},
};
};