Skip to content

Commit 282ff27

Browse files
danilsomsikovDevtools-frontend LUCI CQ
authored andcommitted
Only remove processed references to DOM fragments
Bug: 400353541 Change-Id: I9b93a605c7ae90705539a9293c4c9b1a9c998582 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6405493 Reviewed-by: Philip Pfaffe <[email protected]> Commit-Queue: Danil Somsikov <[email protected]>
1 parent 0fa4f1a commit 282ff27

File tree

8 files changed

+127
-37
lines changed

8 files changed

+127
-37
lines changed

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

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ module.exports = {
6666
}
6767

6868
/**
69-
* @param {EsLintNode} reference
70-
* @param {DomFragment} domFragment
69+
* @param {EsLintNode} reference
70+
* @param {DomFragment} domFragment
71+
* @return {boolean}
7172
*/
7273
function processReference(reference, domFragment) {
7374
const parent = reference.parent;
@@ -93,23 +94,32 @@ module.exports = {
9394
const subpropertyValue = isSubpropertyAssignment ? grandGrandParent.right : null;
9495
for (const rule of subrules) {
9596
if (isPropertyAssignment) {
96-
rule.propertyAssignment?.(property, propertyValue, domFragment);
97+
if (rule.propertyAssignment?.(property, propertyValue, domFragment)) {
98+
return true;
99+
}
97100
} else if (isMethodCall) {
98101
if (isIdentifier(property, 'addEventListener')) {
99102
const event = getEvent(firstArg);
100103
const value = secondArg;
101104
if (event && value.type !== 'SpreadElement') {
102105
domFragment.eventListeners.push({key: event, value});
103106
}
104-
return;
107+
return true;
108+
}
109+
if (rule.methodCall?.(property, firstArg, secondArg, domFragment, grandParent)) {
110+
return true;
105111
}
106-
rule.methodCall?.(property, firstArg, secondArg, domFragment, grandParent);
107112
} else if (isPropertyMethodCall) {
108-
rule.propertyMethodCall?.(property, grandParent.property, propertyMethodArgument, domFragment);
113+
if (rule.propertyMethodCall?.(property, grandParent.property, propertyMethodArgument, domFragment)) {
114+
return true;
115+
}
109116
} else if (isSubpropertyAssignment) {
110-
rule.subpropertyAssignment?.(property, subproperty, subpropertyValue, domFragment);
117+
if (rule.subpropertyAssignment?.(property, subproperty, subpropertyValue, domFragment)) {
118+
return true;
119+
}
111120
}
112121
}
122+
return false;
113123
}
114124

115125
/**
@@ -119,23 +129,32 @@ module.exports = {
119129
/** @type {[number, number][]} */
120130
const ranges = [];
121131
for (const reference of domFragment.references) {
122-
const expression = getEnclosingExpression(reference);
132+
if (!reference.processed) {
133+
continue;
134+
}
135+
const expression = getEnclosingExpression(reference.node);
123136
if (!expression) {
124137
continue;
125138
}
126139
const range = expression.range;
127-
while ([' ', '\n'].includes(sourceCode.text[range[0] - 1])) {
128-
range[0]--;
129-
}
130140
ranges.push(range);
131141
for (const child of domFragment.children) {
132142
ranges.push(...getRangesToRemove(child));
133143
}
134144
}
145+
146+
if (domFragment.initializer && domFragment.references.every(r => r.processed)) {
147+
ranges.push(getEnclosingExpression(domFragment.initializer).range);
148+
}
149+
for (const range of ranges) {
150+
while ([' ', '\n'].includes(sourceCode.text[range[0] - 1])) {
151+
range[0]--;
152+
}
153+
}
135154
ranges.sort((a, b) => a[0] - b[0]);
136155
for (let i = 1; i < ranges.length; i++) {
137156
if (ranges[i][0] < ranges[i - 1][1]) {
138-
ranges[i] = [ranges[i - 1][1], ranges[i][1]];
157+
ranges[i] = [ranges[i - 1][1], Math.max(ranges[i][1], ranges[i - 1][1])];
139158
}
140159
}
141160

@@ -146,7 +165,8 @@ module.exports = {
146165
* @param {DomFragment} domFragment
147166
*/
148167
function maybeReportDomFragment(domFragment) {
149-
if (!domFragment.replacementLocation || domFragment.parent || !domFragment.tagName) {
168+
if (!domFragment.replacementLocation || domFragment.parent || !domFragment.tagName ||
169+
domFragment.references.every(r => !r.processed)) {
150170
return;
151171
}
152172
context.report({
@@ -194,7 +214,9 @@ export const DEFAULT_VIEW = (input, _output, target) => {
194214
'Program:exit'() {
195215
for (const domFragment of DomFragment.values()) {
196216
for (const reference of domFragment.references) {
197-
processReference(reference, domFragment);
217+
if (processReference(reference.node, domFragment)) {
218+
reference.processed = true;
219+
}
198220
}
199221
}
200222

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,10 @@ module.exports = {
3838
{key: 'jslog', value: '${VisualLogging.adorner(' + sourceCode.getText(property.value) + ')}'});
3939
}
4040
if (isIdentifier(property.key, 'content')) {
41-
const childFragment = DomFragment.getOrCreate(property.value, sourceCode);
42-
childFragment.parent = domFragment;
43-
domFragment.children.push(childFragment);
41+
domFragment.appendChild(property.value, sourceCode);
4442
}
4543
}
44+
return true;
4645
}
4746
},
4847
NewExpression(node) {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ class ClassMember {
2121
/** @type {Node} */
2222
classDeclaration;
2323

24+
/** @type {Node|undefined} */
25+
initializer;
26+
2427
/**
2528
* @param {Node} node
2629
* @param {SourceCode} sourceCode
@@ -44,6 +47,9 @@ class ClassMember {
4447
classMembers.set(memberName, classMember);
4548
}
4649
classMember.references.add(node);
50+
if (node.type === 'AssignmentExpression') {
51+
classMember.initializer = node;
52+
}
4753
return classMember;
4854
}
4955
}

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
'use strict';
88

99
const {isIdentifier} = require('./ast.js');
10-
const {DomFragment} = require('./dom-fragment.js');
1110

1211
/** @typedef {import('eslint').Rule.Node} Node */
12+
/** @typedef {import('./dom-fragment.js').DomFragment} DomFragment */
1313

1414
module.exports = {
1515
create : function(context) {
@@ -26,13 +26,12 @@ module.exports = {
2626
methodCall(property, firstArg, secondArg, domFragment, call) {
2727
if (isIdentifier(property, 'createChild')) {
2828
if (firstArg?.type === 'Literal') {
29-
const childFragment = DomFragment.getOrCreate(call, sourceCode);
29+
const childFragment = domFragment.appendChild(call, sourceCode);
3030
childFragment.tagName = String(firstArg.value);
31-
childFragment.parent = domFragment;
32-
domFragment.children.push(childFragment);
3331
if (secondArg) {
3432
childFragment.classList.push(secondArg);
3533
}
34+
return true;
3635
}
3736
}
3837
}

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ module.exports = {
2323
propertyAssignment(property, propertyValue, domFragment) {
2424
if (isIdentifier(property, 'className')) {
2525
domFragment.classList.push(propertyValue);
26-
} else if (isIdentifier(property, ['textContent', 'innerHTML'])) {
26+
return true;
27+
}
28+
if (isIdentifier(property, ['textContent', 'innerHTML'])) {
2729
domFragment.textContent = propertyValue;
30+
return true;
2831
}
32+
return false;
2933
},
3034
/**
3135
* @param {Node} property
@@ -36,7 +40,9 @@ module.exports = {
3640
propertyMethodCall(property, method, firstArg, domFragment) {
3741
if (isIdentifier(property, 'classList') && isIdentifier(method, 'add')) {
3842
domFragment.classList.push(firstArg);
43+
return true;
3944
}
45+
return false;
4046
},
4147
/**
4248
* @param {Node} property
@@ -52,8 +58,10 @@ module.exports = {
5258
key: property,
5359
value: subpropertyValue,
5460
});
61+
return true;
5562
}
5663
}
64+
return false;
5765
},
5866
/**
5967
* @param {Node} property
@@ -68,12 +76,14 @@ module.exports = {
6876
const value = secondArg;
6977
if (attribute.type === 'Literal' && value.type !== 'SpreadElement') {
7078
domFragment.attributes.push({key: attribute.value.toString(), value});
79+
return true;
7180
}
72-
} else if (isIdentifier(property, 'appendChild')) {
73-
const childFragment = DomFragment.getOrCreate(firstArg, sourceCode);
74-
childFragment.parent = domFragment;
75-
domFragment.children.push(childFragment);
7681
}
82+
if (isIdentifier(property, 'appendChild')) {
83+
domFragment.appendChild(firstArg, sourceCode);
84+
return true;
85+
}
86+
return false;
7787
},
7888
MemberExpression(node) {
7989
if (isIdentifier(node.object, 'document') && isIdentifier(node.property, 'createElement')

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

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ class DomFragment {
2929
/** @type {DomFragment|undefined} */ parent;
3030
/** @type {string|undefined} */ expression;
3131
/** @type {EsLintNode|undefined} */ replacementLocation;
32-
/** @type {EsLintNode[]} */ references = [];
32+
/** @type {EsLintNode|undefined} */ initializer;
33+
/** @type {{node: EsLintNode, processed?: boolean}[]} */ references = [];
3334

3435
/**
3536
* @param {Node} estreeNode
@@ -46,17 +47,19 @@ class DomFragment {
4647
domFragments.set(key, result);
4748
if ('parent' in key) {
4849
result.expression = sourceCode.getText(node);
49-
result.references.push(node);
50+
result.references.push({node});
5051
} else if (key instanceof ClassMember) {
5152
result.replacementLocation = /** @type {EsLintNode} */ (key.classDeclaration);
5253
result.expression = sourceCode.getText(node);
5354
} else {
54-
result.references = key.references.map(r => (/** @type {EsLintNode} */ (r.identifier)));
55-
result.references.push(/** @type {EsLintNode} */ (key.identifiers[0]));
55+
result.references = key.references.filter(r => !key.identifiers.includes(r.identifier))
56+
.map(r => ({node: /** @type {EsLintNode} */ (r.identifier)}));
57+
result.initializer = /** @type {EsLintNode} */ (key.identifiers[0]);
5658
}
5759
}
5860
if (key instanceof ClassMember) {
59-
result.references = /** @type {EsLintNode[]} */ ([...key.references]);
61+
result.references = [...key.references].map(r => ({node: /** @type {EsLintNode} */ (r)}));
62+
result.initializer = /** @type {EsLintNode} */ (key.initializer);
6063
}
6164
return result;
6265
}
@@ -145,6 +148,22 @@ class DomFragment {
145148
components.push('</', this.tagName, '>');
146149
return components;
147150
}
151+
152+
/**
153+
* @param {Node} node
154+
* @param {SourceCode} sourceCode
155+
*/
156+
appendChild(node, sourceCode) {
157+
const child = DomFragment.getOrCreate(node, sourceCode);
158+
this.children.push(child);
159+
child.parent = this;
160+
for (const reference of child.references) {
161+
if (reference.node === node) {
162+
reference.processed = true;
163+
}
164+
}
165+
return child;
166+
}
148167
}
149168

150169
/**

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ module.exports = {
3939
*/
4040
methodCall(property, firstArg, secondArg, domFragment, call) {
4141
if (isIdentifier(property, 'appendToolbarItem')) {
42-
const childFragment = DomFragment.getOrCreate(firstArg, sourceCode);
43-
childFragment.parent = domFragment;
44-
domFragment.children.push(childFragment);
42+
domFragment.appendChild(firstArg, sourceCode);
43+
return true;
4544
}
45+
return false;
4646
},
4747
NewExpression(node) {
4848
if (isMemberExpression(
@@ -99,15 +99,13 @@ module.exports = {
9999
key: 'list',
100100
value: 'completions',
101101
});
102-
const dataList = DomFragment.getOrCreate(completions, sourceCode);
102+
const dataList = domFragment.appendChild(completions, sourceCode);
103103
dataList.tagName = 'datalist';
104104
dataList.attributes.push({
105105
key: 'id',
106106
value: 'completions',
107107
});
108108
dataList.textContent = completions;
109-
domFragment.children.push(dataList);
110-
dataList.parent = domFragment;
111109
}
112110
args.shift(); // dynamicCompletions is not supported
113111
const jslogContext = args.shift();

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,5 +363,42 @@ class Widget2 extends UI.Widget.Widget {
363363
}`,
364364
errors: [{messageId: 'preferTemplateLiterals'}, {messageId: 'preferTemplateLiterals'}],
365365
},
366+
{
367+
filename: 'front_end/ui/components/component/file.ts',
368+
code: `
369+
class SomeWidget extends UI.Widget.Widget {
370+
constructor() {
371+
super();
372+
const toolbar = this.contentElement.createChild('devtools-toolbar');
373+
const filterInput = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.editName), 'edit', undefined, 'edit-name');
374+
toolbar.appendToolbarItem(filterInput);
375+
const anotherElement = document.createElement('div');
376+
this.process(filterInput, anotherElement);
377+
}
378+
}`,
379+
output: `
380+
381+
export const DEFAULT_VIEW = (input, _output, target) => {
382+
render(html\`
383+
<div>
384+
<devtools-toolbar>
385+
<devtools-button title=\${i18nString(UIStrings.editName)}
386+
.variant=\${Buttons.Button.Variant.TOOLBAR} .iconName=\${'edit'}
387+
.jslogContext=\${'edit-name'}></devtools-button>
388+
</devtools-toolbar>
389+
</div>\`,
390+
target, {host: input});
391+
};
392+
393+
class SomeWidget extends UI.Widget.Widget {
394+
constructor() {
395+
super();
396+
const filterInput = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.editName), 'edit', undefined, 'edit-name');
397+
const anotherElement = document.createElement('div');
398+
this.process(filterInput, anotherElement);
399+
}
400+
}`,
401+
errors: [{messageId: 'preferTemplateLiterals'}],
402+
},
366403
],
367404
});

0 commit comments

Comments
 (0)