Skip to content

Commit e419264

Browse files
danilsomsikovDevtools-frontend LUCI CQ
authored andcommitted
Support toolbar buttons in the preferTemplateLiterals
Tighten up types along the way. Bug: 400353541 Change-Id: I23b644e7ebc2c19194486817ac98c39498ae1679 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6387112 Reviewed-by: Philip Pfaffe <[email protected]> Commit-Queue: Danil Somsikov <[email protected]>
1 parent d4fcd5f commit e419264

File tree

2 files changed

+112
-19
lines changed

2 files changed

+112
-19
lines changed

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

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,26 @@
99
*/
1010
'use strict';
1111

12+
/**
13+
* @param {Node} node
14+
* @param {string|Array<string>} name
15+
* @return {boolean}
16+
*/
1217
function isIdentifier(node, name) {
1318
return node.type === 'Identifier' && (Array.isArray(name) ? name.includes(node.name) : node.name === name);
1419
}
1520

21+
/**
22+
* @param {Node} node
23+
* @param {function(Node): boolean} objectPredicate
24+
* @param {function(Node): boolean} propertyPredicate
25+
*/
1626
function isMemberExpression(node, objectPredicate, propertyPredicate) {
17-
return node.type === 'MemberExpression' && objectPredicate(node.object) && propertyPredicate(node.property);
27+
return node.type === 'MemberExpression' && objectPredicate(/** @type {Node} */ (node.object)) &&
28+
propertyPredicate(/** @type {Node} */ (node.property));
1829
}
1930

31+
/** @param {Node} node */
2032
function getEnclosingExpression(node) {
2133
while (node.parent) {
2234
if (node.parent.type === 'BlockStatement') {
@@ -27,6 +39,7 @@ function getEnclosingExpression(node) {
2739
return null;
2840
}
2941

42+
/** @param {Node} node */
3043
function getEnclosingClassDeclaration(node) {
3144
let parent = node.parent;
3245
while (parent && parent.type !== 'ClassDeclaration') {
@@ -35,6 +48,7 @@ function getEnclosingClassDeclaration(node) {
3548
return parent;
3649
}
3750

51+
/** @param {string} outputString */
3852
function attributeValue(outputString) {
3953
if (outputString.startsWith('${') && outputString.endsWith('}')) {
4054
return outputString;
@@ -50,9 +64,10 @@ function attributeValue(outputString) {
5064
class DomFragment {
5165
/** @type {string|undefined} */ tagName;
5266
/** @type {Node[]} */ classList = [];
53-
/** @type {{key: string, value: Node}[]} */ attributes = [];
67+
/** @type {{key: string, value: Node|string}[]} */ attributes = [];
5468
/** @type {{key: string, value: Node}[]} */ style = [];
5569
/** @type {{key: string, value: Node}[]} */ eventListeners = [];
70+
/** @type {{key: string, value: Node|string}[]} */ bindings = [];
5671
/** @type {Node} */ textContent;
5772
/** @type {DomFragment[]} */ children = [];
5873
/** @type {DomFragment|undefined} */ parent;
@@ -65,9 +80,18 @@ class DomFragment {
6580
if (this.expression && !this.tagName) {
6681
return [`\n${' '.repeat(indent)}`, '${', this.expression, '}'];
6782
}
68-
function toOutputString(node) {
69-
if (node.type === 'Literal') {
70-
return node.value;
83+
84+
/**
85+
* @param {Node|string} node
86+
* @param {boolean} quoteLiterals
87+
* @return {string}
88+
*/
89+
function toOutputString(node, quoteLiterals = false) {
90+
if (typeof node === 'string') {
91+
return node;
92+
}
93+
if (node.type === 'Literal' && !quoteLiterals) {
94+
return node.value.toString();
7195
}
7296
const text = sourceCode.getText(node);
7397
if (node.type === 'TemplateLiteral') {
@@ -97,14 +121,17 @@ class DomFragment {
97121
lineLength += this.tagName.length + 1;
98122
}
99123
if (this.classList.length) {
100-
appendExpression(`class="${this.classList.map(toOutputString).join(' ')}"`);
124+
appendExpression(`class="${this.classList.map(c => toOutputString(c)).join(' ')}"`);
101125
}
102126
for (const attribute of this.attributes || []) {
103127
appendExpression(`${attribute.key}=${attributeValue(toOutputString(attribute.value))}`);
104128
}
105129
for (const eventListener of this.eventListeners || []) {
106130
appendExpression(`@${eventListener.key}=${attributeValue(toOutputString(eventListener.value))}`);
107131
}
132+
for (const binding of this.bindings || []) {
133+
appendExpression(`.${binding.key}=${toOutputString(binding.value, /* quoteLiterals=*/ true)}`);
134+
}
108135
if (this.style.length) {
109136
const style = this.style.map(s => `${s.key}:${toOutputString(s.value)}`).join('; ');
110137
appendExpression(`style="${style}"`);
@@ -228,7 +255,7 @@ module.exports = {
228255
function processReference(reference, domFragment) {
229256
const parent = reference.parent;
230257
const isAccessed = parent.type === 'MemberExpression' && parent.object === reference;
231-
const property = isAccessed ? parent.property : null;
258+
const property = isAccessed ? /** @type {Node} */ (parent.property) : null;
232259
const grandParent = parent.parent;
233260
const isPropertyAssignment =
234261
isAccessed && grandParent.type === 'AssignmentExpression' && grandParent.left === parent;
@@ -258,20 +285,18 @@ module.exports = {
258285
if (property.type !== 'Property') {
259286
continue;
260287
}
261-
if (isIdentifier(property.key, 'name')) {
288+
const key = /** @type {Node} */ (property.key);
289+
if (isIdentifier(key, 'name')) {
262290
domFragment.attributes.push({
263291
key: 'aria-label',
264292
value: /** @type {Node} */ (property.value),
265293
});
266294
}
267-
if (isIdentifier(property.key, 'jslogContext')) {
268-
domFragment.attributes.push({
269-
key: 'jslog',
270-
value: /** @type {Node} */ (
271-
{type: 'Literal', value: '${VisualLogging.adorner(' + sourceCode.getText(property.value) + ')}'})
272-
});
295+
if (isIdentifier(key, 'jslogContext')) {
296+
domFragment.attributes.push(
297+
{key: 'jslog', value: '${VisualLogging.adorner(' + sourceCode.getText(property.value) + ')}'});
273298
}
274-
if (isIdentifier(property.key, 'content')) {
299+
if (isIdentifier(key, 'content')) {
275300
const childFragment = getOrCreateDomFragment(/** @type {Node} */ (property.value));
276301
childFragment.parent = domFragment;
277302
domFragment.children.push(childFragment);
@@ -294,7 +319,8 @@ module.exports = {
294319
childFragment.parent = domFragment;
295320
domFragment.children.push(childFragment);
296321
} else if (
297-
isPropertyMethodCall && isIdentifier(property, 'classList') && isIdentifier(grandParent.property, 'add')) {
322+
isPropertyMethodCall && isIdentifier(property, 'classList') &&
323+
isIdentifier(/** @type {Node} */ (grandParent.property), 'add')) {
298324
domFragment.classList.push(propertyMethodArgument);
299325
} else if (isSubpropertyAssignment && isIdentifier(property, 'style')) {
300326
const property = subproperty.name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
@@ -405,7 +431,7 @@ export const DEFAULT_VIEW = (input, _output, target) => {
405431
const type = isIdentifier(node.callee.property, 'ToolbarFilter') ? 'filter' : 'text';
406432
domFragment.attributes.push({
407433
key: 'type',
408-
value: /** @type {Node} */ ({type: 'Literal', value: type}),
434+
value: type,
409435
});
410436
const args = [...node.arguments];
411437
const placeholder = args.shift();
@@ -449,13 +475,13 @@ export const DEFAULT_VIEW = (input, _output, target) => {
449475
if (completions && !isIdentifier(completions, 'undefined')) {
450476
domFragment.attributes.push({
451477
key: 'list',
452-
value: /** @type {Node} */ ({type: 'Literal', value: 'completions'}),
478+
value: 'completions',
453479
});
454480
const dataList = getOrCreateDomFragment(completions);
455481
dataList.tagName = 'datalist';
456482
dataList.attributes.push({
457483
key: 'id',
458-
value: /** @type {Node} */ ({type: 'Literal', value: 'completions'}),
484+
value: 'completions',
459485
});
460486
dataList.textContent = completions;
461487
domFragment.children.push(dataList);
@@ -477,6 +503,41 @@ export const DEFAULT_VIEW = (input, _output, target) => {
477503
const domFragment = getOrCreateDomFragment(node);
478504
domFragment.tagName = 'devtools-adorner';
479505
}
506+
if (isMemberExpression(
507+
node.callee, n => isMemberExpression(n, n => isIdentifier(n, 'UI'), n => isIdentifier(n, 'Toolbar')),
508+
n => isIdentifier(n, 'ToolbarButton'))) {
509+
const domFragment = getOrCreateDomFragment(node);
510+
domFragment.tagName = 'devtools-button';
511+
const title = node.arguments[0];
512+
domFragment.bindings.push({
513+
key: 'variant',
514+
value: '${Buttons.Button.Variant.TOOLBAR}',
515+
});
516+
if (title && !isIdentifier(title, 'undefined')) {
517+
domFragment.attributes.push({
518+
key: 'title',
519+
value: title,
520+
});
521+
}
522+
const glyph = node.arguments[1];
523+
if (glyph && !isIdentifier(glyph, 'undefined')) {
524+
domFragment.bindings.push({
525+
key: 'iconName',
526+
value: glyph,
527+
});
528+
}
529+
const text = node.arguments[2];
530+
if (text && !isIdentifier(text, 'undefined')) {
531+
domFragment.textContent = text;
532+
}
533+
const jslogContext = node.arguments[3];
534+
if (jslogContext && !isIdentifier(jslogContext, 'undefined')) {
535+
domFragment.bindings.push({
536+
key: 'jslogContext',
537+
value: jslogContext,
538+
});
539+
}
540+
}
480541
},
481542
'Program:exit'() {
482543
while (queue.length) {

scripts/eslint_rules/tests/no-imperative-dom-api.test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,38 @@ export const DEFAULT_VIEW = (input, _output, target) => {
273273
target, {host: input});
274274
};
275275
276+
class SomeWidget extends UI.Widget.Widget {
277+
constructor() {
278+
super();
279+
}
280+
}`,
281+
errors: [{messageId: 'preferTemplateLiterals'}],
282+
},
283+
{
284+
filename: 'front_end/ui/components/component/file.ts',
285+
code: `
286+
class SomeWidget extends UI.Widget.Widget {
287+
constructor() {
288+
super();
289+
const toolbar = this.contentElement.createChild('devtools-toolbar');
290+
const filterInput = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.editName), 'edit', undefined, 'edit-name');
291+
toolbar.appendToolbarItem(filterInput);
292+
}
293+
}`,
294+
output: `
295+
296+
export const DEFAULT_VIEW = (input, _output, target) => {
297+
render(html\`
298+
<div>
299+
<devtools-toolbar>
300+
<devtools-button title=\${i18nString(UIStrings.editName)}
301+
.variant=\${Buttons.Button.Variant.TOOLBAR} .iconName=\${'edit'}
302+
.jslogContext=\${'edit-name'}></devtools-button>
303+
</devtools-toolbar>
304+
</div>\`,
305+
target, {host: input});
306+
};
307+
276308
class SomeWidget extends UI.Widget.Widget {
277309
constructor() {
278310
super();

0 commit comments

Comments
 (0)