Skip to content

Commit 555cc4b

Browse files
danilsomsikovDevtools-frontend LUCI CQ
authored andcommitted
Extract "subrules" out of preferTemplateLiterals
Bug: 400353541 Change-Id: I068797f4ed37071a655b767f2d22e65b3dd8e72b Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6394682 Reviewed-by: Nikolay Vitkov <[email protected]> Commit-Queue: Danil Somsikov <[email protected]>
1 parent c25ce13 commit 555cc4b

File tree

7 files changed

+421
-202
lines changed

7 files changed

+421
-202
lines changed

scripts/eslint_rules/lib/no-imperative-dom-api.js

Lines changed: 46 additions & 201 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@
99
*/
1010
'use strict';
1111

12-
const {isIdentifier, isMemberExpression, getEnclosingExpression} = require('./no-imperative-dom-api/ast.js');
12+
const adorner = require('./no-imperative-dom-api/adorner.js');
13+
const {isIdentifier, getEnclosingExpression} = require('./no-imperative-dom-api/ast.js');
14+
const domApiDevtoolsExtensions = require('./no-imperative-dom-api/dom-api-devtools-extensions.js');
15+
const domApi = require('./no-imperative-dom-api/dom-api.js');
1316
const {DomFragment} = require('./no-imperative-dom-api/dom-fragment.js');
17+
const toolbar = require('./no-imperative-dom-api/toolbar.js');
18+
const widget = require('./no-imperative-dom-api/widget.js');
1419

1520
/** @typedef {import('eslint').Rule.Node} Node */
1621
/** @typedef {import('eslint').AST.SourceLocation} SourceLocation */
@@ -33,22 +38,29 @@ module.exports = {
3338
create : function(context) {
3439
const sourceCode = context.getSourceCode();
3540

41+
const subrules = [
42+
adorner.create(context),
43+
domApi.create(context),
44+
domApiDevtoolsExtensions.create(context),
45+
toolbar.create(context),
46+
widget.create(context),
47+
];
48+
3649
/**
3750
* @param {Node} event
3851
* @return {string|null}
3952
*/
4053
function getEvent(event) {
41-
switch (sourceCode.getText(event)) {
42-
case 'UI.Toolbar.ToolbarInput.Event.TEXT_CHANGED':
43-
return 'change';
44-
case 'UI.Toolbar.ToolbarInput.Event.ENTER_PRESSED':
45-
return 'submit';
46-
default:
47-
if (event.type === 'Literal') {
48-
return event.value.toString();
49-
}
50-
return null;
54+
for (const rule of subrules) {
55+
const result = rule.getEvent?.(event);
56+
if (result) {
57+
return result;
58+
}
59+
}
60+
if (event.type === 'Literal') {
61+
return event.value.toString();
5162
}
63+
return null;
5264
}
5365

5466
/**
@@ -77,76 +89,24 @@ module.exports = {
7789
const subproperty =
7890
isSubpropertyAssignment && grandParent.property.type === 'Identifier' ? grandParent.property : null;
7991
const subpropertyValue = isSubpropertyAssignment ? /** @type {Node} */ (grandGrandParent.right) : null;
80-
if (isPropertyAssignment && isIdentifier(property, 'className')) {
81-
domFragment.classList.push(propertyValue);
82-
} else if (isPropertyAssignment && isIdentifier(property, ['textContent', 'innerHTML'])) {
83-
domFragment.textContent = propertyValue;
84-
} else if (
85-
isPropertyAssignment && domFragment.tagName === 'devtools-adorner' && isIdentifier(property, 'data') &&
86-
propertyValue.type === 'ObjectExpression') {
87-
for (const property of propertyValue.properties) {
88-
if (property.type !== 'Property') {
89-
continue;
90-
}
91-
const key = /** @type {Node} */ (property.key);
92-
if (isIdentifier(key, 'name')) {
93-
domFragment.attributes.push({
94-
key: 'aria-label',
95-
value: /** @type {Node} */ (property.value),
96-
});
97-
}
98-
if (isIdentifier(key, 'jslogContext')) {
99-
domFragment.attributes.push(
100-
{key: 'jslog', value: '${VisualLogging.adorner(' + sourceCode.getText(property.value) + ')}'});
101-
}
102-
if (isIdentifier(key, 'content')) {
103-
const childFragment = DomFragment.getOrCreate(/** @type {Node} */ (property.value), sourceCode);
104-
childFragment.parent = domFragment;
105-
domFragment.children.push(childFragment);
106-
}
107-
}
108-
} else if (isMethodCall && isIdentifier(property, 'setAttribute')) {
109-
const attribute = firstArg;
110-
const value = secondArg;
111-
if (attribute.type === 'Literal' && value.type !== 'SpreadElement') {
112-
domFragment.attributes.push({key: attribute.value.toString(), value});
113-
}
114-
} else if (isMethodCall && isIdentifier(property, 'addEventListener')) {
115-
const event = getEvent(firstArg);
116-
const value = secondArg;
117-
if (event && value.type !== 'SpreadElement') {
118-
domFragment.eventListeners.push({key: event, value});
119-
}
120-
} else if (isMethodCall && isIdentifier(property, 'appendToolbarItem')) {
121-
const childFragment = DomFragment.getOrCreate(firstArg, sourceCode);
122-
childFragment.parent = domFragment;
123-
domFragment.children.push(childFragment);
124-
} else if (
125-
isPropertyMethodCall && isIdentifier(property, 'classList') &&
126-
isIdentifier(/** @type {Node} */ (grandParent.property), 'add')) {
127-
domFragment.classList.push(propertyMethodArgument);
128-
} else if (isSubpropertyAssignment && isIdentifier(property, 'style')) {
129-
const property = subproperty.name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
130-
if (subpropertyValue.type !== 'SpreadElement') {
131-
domFragment.style.push({
132-
key: property,
133-
value: subpropertyValue,
134-
});
135-
}
136-
} else if (isMethodCall && isIdentifier(property, 'createChild')) {
137-
if (firstArg?.type === 'Literal') {
138-
const childFragment = DomFragment.getOrCreate(grandParent, sourceCode);
139-
childFragment.tagName = String(firstArg.value);
140-
childFragment.parent = domFragment;
141-
domFragment.children.push(childFragment);
142-
if (secondArg) {
143-
childFragment.classList.push(secondArg);
92+
for (const rule of subrules) {
93+
if (isPropertyAssignment) {
94+
rule.propertyAssignment?.(property, propertyValue, domFragment);
95+
} else if (isMethodCall) {
96+
if (isIdentifier(property, 'addEventListener')) {
97+
const event = getEvent(firstArg);
98+
const value = secondArg;
99+
if (event && value.type !== 'SpreadElement') {
100+
domFragment.eventListeners.push({key: event, value});
101+
}
102+
return;
144103
}
104+
rule.methodCall?.(property, firstArg, secondArg, domFragment, grandParent);
105+
} else if (isPropertyMethodCall) {
106+
rule.propertyMethodCall?.(property, grandParent.property, propertyMethodArgument, domFragment);
107+
} else if (isSubpropertyAssignment) {
108+
rule.subpropertyAssignment?.(property, subproperty, subpropertyValue, domFragment);
145109
}
146-
} else if (isMethodCall && isIdentifier(property, 'appendChild')) {
147-
const childFragment = DomFragment.getOrCreate(firstArg, sourceCode);
148-
childFragment.parent = domFragment;
149-
domFragment.children.push(childFragment);
150110
}
151111
}
152112

@@ -214,133 +174,18 @@ export const DEFAULT_VIEW = (input, _output, target) => {
214174
return {
215175
MemberExpression(node) {
216176
if (node.object.type === 'ThisExpression') {
217-
const domFragment = DomFragment.getOrCreate(node, sourceCode);
218-
if (isIdentifier(node.property, 'contentElement')) {
219-
domFragment.tagName = 'div';
220-
}
177+
DomFragment.getOrCreate(node, sourceCode);
221178
}
222-
if (isIdentifier(node.object, 'document') && isIdentifier(node.property, 'createElement')
223-
&& node.parent.type === 'CallExpression' && node.parent.callee === node) {
224-
const domFragment = DomFragment.getOrCreate(node.parent, sourceCode);
225-
if (node.parent.arguments.length >= 1 && node.parent.arguments[0].type === 'Literal') {
226-
domFragment.tagName = node.parent.arguments[0].value;
179+
for (const rule of subrules) {
180+
if ('MemberExpression' in rule) {
181+
rule.MemberExpression(node);
227182
}
228183
}
229184
},
230185
NewExpression(node) {
231-
if (isMemberExpression(
232-
node.callee, n => isMemberExpression(n, n => isIdentifier(n, 'UI'), n => isIdentifier(n, 'Toolbar')),
233-
n => isIdentifier(n, ['ToolbarFilter', 'ToolbarInput']))) {
234-
const domFragment = DomFragment.getOrCreate(node, sourceCode);
235-
domFragment.tagName = 'devtools-toolbar-input';
236-
const type = isIdentifier(node.callee.property, 'ToolbarFilter') ? 'filter' : 'text';
237-
domFragment.attributes.push({
238-
key: 'type',
239-
value: type,
240-
});
241-
const args = [...node.arguments];
242-
const placeholder = args.shift();
243-
if (placeholder && !isIdentifier(placeholder, 'undefined')) {
244-
domFragment.attributes.push({
245-
key: 'placeholder',
246-
value: placeholder,
247-
});
248-
}
249-
if (type === 'text') {
250-
const accesiblePlaceholder = args.shift();
251-
if (accesiblePlaceholder && !isIdentifier(accesiblePlaceholder, 'undefined')) {
252-
domFragment.attributes.push({
253-
key: 'aria-label',
254-
value: accesiblePlaceholder,
255-
});
256-
}
257-
}
258-
const flexGrow = args.shift();
259-
if (flexGrow && !isIdentifier(flexGrow, 'undefined')) {
260-
domFragment.style.push({
261-
key: 'flex-grow',
262-
value: flexGrow,
263-
});
264-
}
265-
const flexShrink = args.shift();
266-
if (flexShrink && !isIdentifier(flexShrink, 'undefined')) {
267-
domFragment.style.push({
268-
key: 'flex-shrink',
269-
value: flexShrink,
270-
});
271-
}
272-
const title = args.shift();
273-
if (title && !isIdentifier(title, 'undefined')) {
274-
domFragment.attributes.push({
275-
key: 'title',
276-
value: title,
277-
});
278-
}
279-
const completions = args.shift();
280-
if (completions && !isIdentifier(completions, 'undefined')) {
281-
domFragment.attributes.push({
282-
key: 'list',
283-
value: 'completions',
284-
});
285-
const dataList = DomFragment.getOrCreate(completions, sourceCode);
286-
dataList.tagName = 'datalist';
287-
dataList.attributes.push({
288-
key: 'id',
289-
value: 'completions',
290-
});
291-
dataList.textContent = completions;
292-
domFragment.children.push(dataList);
293-
dataList.parent = domFragment;
294-
}
295-
args.shift(); // dynamicCompletions is not supported
296-
const jslogContext = args.shift();
297-
if (jslogContext && !isIdentifier(jslogContext, 'undefined')) {
298-
domFragment.attributes.push({
299-
key: 'id',
300-
value: jslogContext,
301-
});
302-
}
303-
}
304-
if (isMemberExpression(
305-
node.callee,
306-
n => isMemberExpression(n, n => isIdentifier(n, 'Adorners'), n => isIdentifier(n, 'Adorner')),
307-
n => isIdentifier(n, 'Adorner'))) {
308-
const domFragment = DomFragment.getOrCreate(node, sourceCode);
309-
domFragment.tagName = 'devtools-adorner';
310-
}
311-
if (isMemberExpression(
312-
node.callee, n => isMemberExpression(n, n => isIdentifier(n, 'UI'), n => isIdentifier(n, 'Toolbar')),
313-
n => isIdentifier(n, 'ToolbarButton'))) {
314-
const domFragment = DomFragment.getOrCreate(node, sourceCode);
315-
domFragment.tagName = 'devtools-button';
316-
const title = node.arguments[0];
317-
domFragment.bindings.push({
318-
key: 'variant',
319-
value: '${Buttons.Button.Variant.TOOLBAR}',
320-
});
321-
if (title && !isIdentifier(title, 'undefined')) {
322-
domFragment.attributes.push({
323-
key: 'title',
324-
value: title,
325-
});
326-
}
327-
const glyph = node.arguments[1];
328-
if (glyph && !isIdentifier(glyph, 'undefined')) {
329-
domFragment.bindings.push({
330-
key: 'iconName',
331-
value: glyph,
332-
});
333-
}
334-
const text = node.arguments[2];
335-
if (text && !isIdentifier(text, 'undefined')) {
336-
domFragment.textContent = text;
337-
}
338-
const jslogContext = node.arguments[3];
339-
if (jslogContext && !isIdentifier(jslogContext, 'undefined')) {
340-
domFragment.bindings.push({
341-
key: 'jslogContext',
342-
value: jslogContext,
343-
});
186+
for (const rule of subrules) {
187+
if ('NewExpression' in rule) {
188+
rule.NewExpression(node);
344189
}
345190
}
346191
},
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
/**
5+
* @fileoverview A library to identify and templatize manually constructed Adorner.
6+
*/
7+
'use strict';
8+
9+
const {isIdentifier, isMemberExpression} = require('./ast.js');
10+
const {DomFragment} = require('./dom-fragment.js');
11+
12+
/** @typedef {import('eslint').Rule.Node} Node */
13+
14+
module.exports = {
15+
create(context) {
16+
const sourceCode = context.getSourceCode();
17+
return {
18+
/**
19+
* @param {Node} property
20+
* @param {Node} propertyValue
21+
* @param {DomFragment} domFragment
22+
*/
23+
propertyAssignment(property, propertyValue, domFragment) {
24+
if (domFragment.tagName === 'devtools-adorner' && isIdentifier(property, 'data') &&
25+
propertyValue.type === 'ObjectExpression') {
26+
for (const property of propertyValue.properties) {
27+
if (property.type !== 'Property') {
28+
continue;
29+
}
30+
const key = /** @type {Node} */ (property.key);
31+
if (isIdentifier(key, 'name')) {
32+
domFragment.attributes.push({
33+
key: 'aria-label',
34+
value: /** @type {Node} */ (property.value),
35+
});
36+
}
37+
if (isIdentifier(key, 'jslogContext')) {
38+
domFragment.attributes.push(
39+
{key: 'jslog', value: '${VisualLogging.adorner(' + sourceCode.getText(property.value) + ')}'});
40+
}
41+
if (isIdentifier(key, 'content')) {
42+
const childFragment = DomFragment.getOrCreate(/** @type {Node} */ (property.value), sourceCode);
43+
childFragment.parent = domFragment;
44+
domFragment.children.push(childFragment);
45+
}
46+
}
47+
}
48+
},
49+
NewExpression(node) {
50+
if (isMemberExpression(
51+
node.callee,
52+
n => isMemberExpression(n, n => isIdentifier(n, 'Adorners'), n => isIdentifier(n, 'Adorner')),
53+
n => isIdentifier(n, 'Adorner'))) {
54+
const domFragment = DomFragment.getOrCreate(node, sourceCode);
55+
domFragment.tagName = 'devtools-adorner';
56+
}
57+
},
58+
};
59+
}
60+
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
/**
5+
* @fileoverview Library to identify and templatize manually calls to DevTools DOM API extensions.
6+
*/
7+
'use strict';
8+
9+
const {isIdentifier} = require('./ast.js');
10+
const {DomFragment} = require('./dom-fragment.js');
11+
12+
/** @typedef {import('eslint').Rule.Node} Node */
13+
14+
module.exports = {
15+
create : function(context) {
16+
const sourceCode = context.getSourceCode();
17+
18+
return {
19+
/**
20+
* @param {Node} property
21+
* @param {Node} firstArg
22+
* @param {Node} secondArg
23+
* @param {DomFragment} domFragment
24+
* @param {Node} call
25+
*/
26+
methodCall(property, firstArg, secondArg, domFragment, call) {
27+
if (isIdentifier(property, 'createChild')) {
28+
if (firstArg?.type === 'Literal') {
29+
const childFragment = DomFragment.getOrCreate(call, sourceCode);
30+
childFragment.tagName = String(firstArg.value);
31+
childFragment.parent = domFragment;
32+
domFragment.children.push(childFragment);
33+
if (secondArg) {
34+
childFragment.classList.push(secondArg);
35+
}
36+
}
37+
}
38+
}
39+
};
40+
}
41+
};

0 commit comments

Comments
 (0)