chrome-devtools-frontend 1.0.999791 → 1.0.1000057

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,7 @@
2
2
  // Use of this source code is governed by a BSD-style license that can be
3
3
  // found in the LICENSE file.
4
4
 
5
+ // clang-format off
5
6
  const path = require('path');
6
7
  const rulesDirPlugin = require('eslint-plugin-rulesdir');
7
8
  rulesDirPlugin.RULES_DIR = path.join(__dirname, '..', 'scripts', 'eslint_rules', 'lib');
@@ -28,6 +29,7 @@ module.exports = {
28
29
  'rules': {
29
30
  '@typescript-eslint/explicit-function-return-type': 2,
30
31
  'rulesdir/no_importing_images_from_src': 2,
32
+ 'rulesdir/enforce_bound_render_for_schedule_render': 2,
31
33
  'rulesdir/enforce_custom_event_names': 2,
32
34
  'rulesdir/set_data_type_reference': 2,
33
35
  'rulesdir/no_bound_component_methods': 2,
@@ -120,10 +122,7 @@ module.exports = {
120
122
  },
121
123
  {
122
124
  // Ignore type properties that require quotes
123
- 'selector': [
124
- 'typeProperty',
125
- 'enumMember'
126
- ],
125
+ 'selector': ['typeProperty', 'enumMember'],
127
126
  'format': null,
128
127
  'modifiers': ['requiresQuotes']
129
128
  }
@@ -150,3 +149,4 @@ module.exports = {
150
149
  }
151
150
  ]
152
151
  };
152
+ // clang-format on
@@ -153,10 +153,10 @@ export class ParsedURL {
153
153
  return null;
154
154
  }
155
155
 
156
- private static preEncodeSpecialCharactersInPath(path: string): string {
156
+ static preEncodeSpecialCharactersInPath(path: string): string {
157
157
  // Based on net::FilePathToFileURL. Ideally we would handle
158
158
  // '\\' as well on non-Windows file systems.
159
- for (const specialChar of ['%', ';', '#', '?']) {
159
+ for (const specialChar of ['%', ';', '#', '?', ' ']) {
160
160
  (path as string) = path.replaceAll(specialChar, encodeURIComponent(specialChar));
161
161
  }
162
162
  return path;
@@ -185,7 +185,7 @@ export class ParsedURL {
185
185
  */
186
186
  static urlFromParentUrlAndName(parentUrl: Platform.DevToolsPath.UrlString, name: string):
187
187
  Platform.DevToolsPath.UrlString {
188
- return ParsedURL.concatenate(parentUrl, '/', encodeURIComponent(name));
188
+ return ParsedURL.concatenate(parentUrl, '/', ParsedURL.preEncodeSpecialCharactersInPath(name));
189
189
  }
190
190
 
191
191
  static encodedPathToRawPathString(encPath: Platform.DevToolsPath.EncodedPathString):
@@ -5822,6 +5822,9 @@
5822
5822
  "panels/lighthouse/LighthouseController.ts | isThisPageUsableByPeopleWith": {
5823
5823
  "message": "Is this page usable by people with disabilities or impairments"
5824
5824
  },
5825
+ "panels/lighthouse/LighthouseController.ts | javaScriptDisabled": {
5826
+ "message": "JavaScript is disabled. You need to enable JavaScript to audit this page. Open the Command Menu and run the Enable JavaScript command to enable JavaScript."
5827
+ },
5825
5828
  "panels/lighthouse/LighthouseController.ts | legacyNavigation": {
5826
5829
  "message": "Legacy navigation"
5827
5830
  },
@@ -5822,6 +5822,9 @@
5822
5822
  "panels/lighthouse/LighthouseController.ts | isThisPageUsableByPeopleWith": {
5823
5823
  "message": "Îś t̂h́îś p̂áĝé ûśâb́l̂é b̂ý p̂éôṕl̂é ŵít̂h́ d̂íŝáb̂íl̂ít̂íêś ôŕ îḿp̂áîŕm̂én̂t́ŝ"
5824
5824
  },
5825
+ "panels/lighthouse/LighthouseController.ts | javaScriptDisabled": {
5826
+ "message": "Ĵáv̂áŜćr̂íp̂t́ îś d̂íŝáb̂ĺêd́. Ŷóû ńêéd̂ t́ô én̂áb̂ĺê J́âv́âŚĉŕîṕt̂ t́ô áûd́ît́ t̂h́îś p̂áĝé. Ôṕêń t̂h́ê Ćôḿm̂án̂d́ M̂én̂ú âńd̂ ŕûń t̂h́ê Én̂áb̂ĺê J́âv́âŚĉŕîṕt̂ ćôḿm̂án̂d́ t̂ó êńâb́l̂é Ĵáv̂áŜćr̂íp̂t́."
5827
+ },
5825
5828
  "panels/lighthouse/LighthouseController.ts | legacyNavigation": {
5826
5829
  "message": "L̂éĝáĉý n̂áv̂íĝát̂íôń"
5827
5830
  },
@@ -183,7 +183,7 @@ export abstract class ProjectStore implements Project {
183
183
  const oldPath = uiSourceCode.url();
184
184
  const newPath = uiSourceCode.parentURL() ?
185
185
  Common.ParsedURL.ParsedURL.urlFromParentUrlAndName(uiSourceCode.parentURL(), newName) :
186
- encodeURIComponent(newName) as Platform.DevToolsPath.UrlString;
186
+ Common.ParsedURL.ParsedURL.preEncodeSpecialCharactersInPath(newName) as Platform.DevToolsPath.UrlString;
187
187
  const value = this.uiSourceCodesMap.get(oldPath) as {
188
188
  uiSourceCode: UISourceCode,
189
189
  index: number,
@@ -171,6 +171,11 @@ const UIStrings = {
171
171
  */
172
172
  resetStorageLocalstorage:
173
173
  'Reset storage (`cache`, `service workers`, etc) before auditing. (Good for performance & `PWA` testing)',
174
+ /**
175
+ *@description Explanation for user that Ligthhouse can only audit when JavaScript is enabled
176
+ */
177
+ javaScriptDisabled:
178
+ 'JavaScript is disabled. You need to enable JavaScript to audit this page. Open the Command Menu and run the Enable JavaScript command to enable JavaScript.',
174
179
  };
175
180
  const str_ = i18n.i18n.registerUIStrings('panels/lighthouse/LighthouseController.ts', UIStrings);
176
181
  const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
@@ -196,6 +201,9 @@ export class LighthouseController extends Common.ObjectWrapper.ObjectWrapper<Eve
196
201
  runtimeSetting.setting.addChangeListener(this.recomputePageAuditability.bind(this));
197
202
  }
198
203
 
204
+ const javaScriptDisabledSetting = Common.Settings.Settings.instance().moduleSetting('javaScriptDisabled');
205
+ javaScriptDisabledSetting.addChangeListener(this.recomputePageAuditability.bind(this));
206
+
199
207
  SDK.TargetManager.TargetManager.instance().observeModels(SDK.ServiceWorkerManager.ServiceWorkerManager, this);
200
208
  SDK.TargetManager.TargetManager.instance().addEventListener(
201
209
  SDK.TargetManager.Events.InspectedURLChanged, this.recomputePageAuditability, this);
@@ -273,6 +281,10 @@ export class LighthouseController extends Common.ObjectWrapper.ObjectWrapper<Eve
273
281
  return null;
274
282
  }
275
283
 
284
+ private javaScriptDisabled(): boolean {
285
+ return Common.Settings.Settings.instance().moduleSetting('javaScriptDisabled').get();
286
+ }
287
+
276
288
  private async hasImportantResourcesNotCleared(): Promise<string> {
277
289
  const clearStorageSetting =
278
290
  RuntimeSettings.find(runtimeSetting => runtimeSetting.setting.name === 'lighthouse.clear_storage');
@@ -363,6 +375,7 @@ export class LighthouseController extends Common.ObjectWrapper.ObjectWrapper<Eve
363
375
  const hasActiveServiceWorker = this.hasActiveServiceWorker();
364
376
  const hasAtLeastOneCategory = this.hasAtLeastOneCategory();
365
377
  const unauditablePageMessage = this.unauditablePageMessage();
378
+ const javaScriptDisabled = this.javaScriptDisabled();
366
379
 
367
380
  let helpText = '';
368
381
  if (hasActiveServiceWorker) {
@@ -371,6 +384,8 @@ export class LighthouseController extends Common.ObjectWrapper.ObjectWrapper<Eve
371
384
  helpText = i18nString(UIStrings.atLeastOneCategoryMustBeSelected);
372
385
  } else if (unauditablePageMessage) {
373
386
  helpText = unauditablePageMessage;
387
+ } else if (javaScriptDisabled) {
388
+ helpText = i18nString(UIStrings.javaScriptDisabled);
374
389
  }
375
390
 
376
391
  this.dispatchEventToListeners(Events.PageAuditabilityChanged, {helpText});
@@ -3,6 +3,7 @@
3
3
  // found in the LICENSE file.
4
4
 
5
5
  import * as i18n from '../../core/i18n/i18n.js';
6
+ import type * as Platform from '../../core/platform/platform.js';
6
7
  import * as UI from '../../ui/legacy/legacy.js';
7
8
 
8
9
  import dialogStyles from './dialog.css.js';
@@ -22,8 +23,8 @@ const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
22
23
  export class AddSourceMapURLDialog extends UI.Widget.HBox {
23
24
  private readonly input: HTMLInputElement;
24
25
  private readonly dialog: UI.Dialog.Dialog;
25
- private readonly callback: (arg0: string) => void;
26
- constructor(callback: (arg0: string) => void) {
26
+ private readonly callback: (arg0: Platform.DevToolsPath.UrlString) => void;
27
+ constructor(callback: (arg0: Platform.DevToolsPath.UrlString) => void) {
27
28
  super(/* isWebComponent */ true);
28
29
 
29
30
  this.contentElement.createChild('label').textContent = i18nString(UIStrings.sourceMapUrl);
@@ -51,13 +52,13 @@ export class AddSourceMapURLDialog extends UI.Widget.HBox {
51
52
  this.dialog.show();
52
53
  }
53
54
 
54
- private done(value: string): void {
55
+ private done(value: Platform.DevToolsPath.UrlString): void {
55
56
  this.dialog.hide();
56
57
  this.callback(value);
57
58
  }
58
59
 
59
60
  private apply(): void {
60
- this.done(this.input.value);
61
+ this.done(this.input.value as Platform.DevToolsPath.UrlString);
61
62
  }
62
63
 
63
64
  private onKeyDown(event: KeyboardEvent): void {
@@ -4,6 +4,7 @@
4
4
 
5
5
  import * as Common from '../../core/common/common.js';
6
6
  import * as i18n from '../../core/i18n/i18n.js';
7
+ import type * as Platform from '../../core/platform/platform.js';
7
8
  import * as SDK from '../../core/sdk/sdk.js';
8
9
 
9
10
  import debuggerPausedMessageStyles from './debuggerPausedMessage.css.js';
@@ -276,6 +277,6 @@ export const BreakpointTypeNouns = new Map([
276
277
  ]);
277
278
  interface PausedDetailsAuxData {
278
279
  description?: string;
279
- url?: string;
280
+ url?: Platform.DevToolsPath.UrlString;
280
281
  value?: string;
281
282
  }
@@ -472,11 +472,11 @@ export class DebuggerPlugin extends Plugin {
472
472
  }
473
473
 
474
474
  function addSourceMapURLDialogCallback(
475
- scriptFile: Bindings.ResourceScriptMapping.ResourceScriptFile, url: string): void {
475
+ scriptFile: Bindings.ResourceScriptMapping.ResourceScriptFile, url: Platform.DevToolsPath.UrlString): void {
476
476
  if (!url) {
477
477
  return;
478
478
  }
479
- scriptFile.addSourceMapURL(url as Platform.DevToolsPath.UrlString);
479
+ scriptFile.addSourceMapURL(url);
480
480
  }
481
481
 
482
482
  if (this.uiSourceCode.project().type() === Workspace.Workspace.projectTypes.Network &&
@@ -93,9 +93,7 @@ export class EditingLocationHistoryManager {
93
93
  }
94
94
 
95
95
  private reveal(entry: EditingLocationHistoryEntry): void {
96
- // TODO(crbug.com/1253323): Cast to UrlString will be removed when migration to branded types is complete.
97
- const uiSourceCode = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCode(
98
- entry.projectId, entry.url as Platform.DevToolsPath.UrlString);
96
+ const uiSourceCode = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCode(entry.projectId, entry.url);
99
97
  if (uiSourceCode) {
100
98
  this.revealing = true;
101
99
  this.sourcesView.showSourceLocation(uiSourceCode, entry.position, false, true);
@@ -131,7 +129,7 @@ export class EditingLocationHistoryManager {
131
129
 
132
130
  class EditingLocationHistoryEntry {
133
131
  readonly projectId: string;
134
- readonly url: string;
132
+ readonly url: Platform.DevToolsPath.UrlString;
135
133
  position: number;
136
134
 
137
135
  constructor(uiSourceCode: Workspace.UISourceCode.UISourceCode, position: number) {
@@ -290,7 +290,7 @@ export class NavigatorView extends UI.Widget.VBox implements SDK.TargetManager.O
290
290
  // Update folder titles.
291
291
  const pathTokens =
292
292
  Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding.relativePath(binding.fileSystem);
293
- let folderPath = '' as Platform.DevToolsPath.EncodedPathString;
293
+ let folderPath = Platform.DevToolsPath.EmptyEncodedPathString;
294
294
  for (let i = 0; i < pathTokens.length - 1; ++i) {
295
295
  folderPath = Common.ParsedURL.ParsedURL.concatenate(folderPath, pathTokens[i]);
296
296
  const folderId =
@@ -744,7 +744,7 @@ export class NavigatorView extends UI.Widget.VBox implements SDK.TargetManager.O
744
744
  const folderId = this.folderNodeId(
745
745
  project, target, frame, uiSourceCode.origin(),
746
746
  currentNode instanceof NavigatorFolderTreeNode && currentNode.folderPath ||
747
- '' as Platform.DevToolsPath.EncodedPathString);
747
+ Platform.DevToolsPath.EmptyEncodedPathString);
748
748
  this.subfolderNodes.delete(folderId);
749
749
  parentNode.removeChild(currentNode);
750
750
  currentNode = (parentNode as NavigatorUISourceCodeTreeNode | null);
@@ -822,8 +822,7 @@ export class NavigatorView extends UI.Widget.VBox implements SDK.TargetManager.O
822
822
  contextMenu.editSection().appendItem(i18nString(UIStrings.rename), this.handleContextMenuRename.bind(this, node));
823
823
  contextMenu.editSection().appendItem(
824
824
  i18nString(UIStrings.makeACopy),
825
- this.handleContextMenuCreate.bind(
826
- this, project, '' as Platform.DevToolsPath.EncodedPathString, uiSourceCode));
825
+ this.handleContextMenuCreate.bind(this, project, Platform.DevToolsPath.EmptyEncodedPathString, uiSourceCode));
827
826
  contextMenu.editSection().appendItem(
828
827
  i18nString(UIStrings.delete), this.handleContextMenuDelete.bind(this, uiSourceCode));
829
828
  }
@@ -1472,7 +1471,7 @@ export class NavigatorFolderTreeNode extends NavigatorTreeNode {
1472
1471
  folderPath: Platform.DevToolsPath.EncodedPathString, title: string) {
1473
1472
  super(navigatorView, id, type);
1474
1473
  this.project = project;
1475
- this.folderPath = folderPath as Platform.DevToolsPath.EncodedPathString;
1474
+ this.folderPath = folderPath;
1476
1475
  this.title = title;
1477
1476
  }
1478
1477
 
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type * as Common from '../../core/common/common.js';
6
6
  import * as i18n from '../../core/i18n/i18n.js';
7
+ import type * as Platform from '../../core/platform/platform.js';
7
8
  import * as FormatterModule from '../../models/formatter/formatter.js';
8
9
  import * as Persistence from '../../models/persistence/persistence.js';
9
10
  import * as Workspace from '../../models/workspace/workspace.js';
@@ -30,7 +31,7 @@ const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
30
31
  let scriptFormatterEditorActionInstance: ScriptFormatterEditorAction;
31
32
 
32
33
  export class ScriptFormatterEditorAction implements EditorAction {
33
- private readonly pathsToFormatOnLoad: Set<string>;
34
+ private readonly pathsToFormatOnLoad: Set<Platform.DevToolsPath.UrlString>;
34
35
  private sourcesView!: SourcesView;
35
36
  private button!: UI.Toolbar.ToolbarButton;
36
37
  private constructor() {
@@ -436,10 +436,10 @@ export class TabbedEditorContainer extends Common.ObjectWrapper.ObjectWrapper<Ev
436
436
  private updateHistory(): void {
437
437
  const tabIds = this.tabbedPane.lastOpenedTabIds(maximalPreviouslyViewedFilesCount);
438
438
 
439
- function tabIdToURI(this: TabbedEditorContainer, tabId: string): string {
439
+ function tabIdToURI(this: TabbedEditorContainer, tabId: string): Platform.DevToolsPath.UrlString {
440
440
  const tab = this.files.get(tabId);
441
441
  if (!tab) {
442
- return '';
442
+ return Platform.DevToolsPath.EmptyUrlString;
443
443
  }
444
444
  return tab.url();
445
445
  }
@@ -639,18 +639,19 @@ export let tabId = 0;
639
639
  export const maximalPreviouslyViewedFilesCount = 30;
640
640
 
641
641
  interface SerializedHistoryItem {
642
- url: string;
642
+ url: Platform.DevToolsPath.UrlString;
643
643
  selectionRange?: TextUtils.TextRange.SerializedTextRange;
644
644
  scrollLineNumber?: number;
645
645
  }
646
646
 
647
647
  export class HistoryItem {
648
- url: string;
648
+ url: Platform.DevToolsPath.UrlString;
649
649
  private isSerializable: boolean;
650
650
  selectionRange: TextUtils.TextRange.TextRange|undefined;
651
651
  scrollLineNumber: number|undefined;
652
652
 
653
- constructor(url: string, selectionRange?: TextUtils.TextRange.TextRange, scrollLineNumber?: number) {
653
+ constructor(
654
+ url: Platform.DevToolsPath.UrlString, selectionRange?: TextUtils.TextRange.TextRange, scrollLineNumber?: number) {
654
655
  this.url = url;
655
656
  this.isSerializable = url.length < HistoryItem.serializableUrlLengthLimit;
656
657
  this.selectionRange = selectionRange;
@@ -700,7 +701,7 @@ export class History {
700
701
  return new History(items);
701
702
  }
702
703
 
703
- index(url: string): number {
704
+ index(url: Platform.DevToolsPath.UrlString): number {
704
705
  const index = this.itemsIndex.get(url);
705
706
  if (index !== undefined) {
706
707
  return index;
@@ -716,12 +717,12 @@ export class History {
716
717
  }
717
718
  }
718
719
 
719
- selectionRange(url: string): TextUtils.TextRange.TextRange|undefined {
720
+ selectionRange(url: Platform.DevToolsPath.UrlString): TextUtils.TextRange.TextRange|undefined {
720
721
  const index = this.index(url);
721
722
  return index !== -1 ? this.items[index].selectionRange : undefined;
722
723
  }
723
724
 
724
- updateSelectionRange(url: string, selectionRange?: TextUtils.TextRange.TextRange): void {
725
+ updateSelectionRange(url: Platform.DevToolsPath.UrlString, selectionRange?: TextUtils.TextRange.TextRange): void {
725
726
  if (!selectionRange) {
726
727
  return;
727
728
  }
@@ -732,12 +733,12 @@ export class History {
732
733
  this.items[index].selectionRange = selectionRange;
733
734
  }
734
735
 
735
- scrollLineNumber(url: string): number|undefined {
736
+ scrollLineNumber(url: Platform.DevToolsPath.UrlString): number|undefined {
736
737
  const index = this.index(url);
737
738
  return index !== -1 ? this.items[index].scrollLineNumber : undefined;
738
739
  }
739
740
 
740
- updateScrollLineNumber(url: string, scrollLineNumber: number): void {
741
+ updateScrollLineNumber(url: Platform.DevToolsPath.UrlString, scrollLineNumber: number): void {
741
742
  const index = this.index(url);
742
743
  if (index === -1) {
743
744
  return;
@@ -745,7 +746,7 @@ export class History {
745
746
  this.items[index].scrollLineNumber = scrollLineNumber;
746
747
  }
747
748
 
748
- update(urls: string[]): void {
749
+ update(urls: Platform.DevToolsPath.UrlString[]): void {
749
750
  for (let i = urls.length - 1; i >= 0; --i) {
750
751
  const index = this.index(urls[i]);
751
752
  let item;
@@ -760,7 +761,7 @@ export class History {
760
761
  }
761
762
  }
762
763
 
763
- remove(url: string): void {
764
+ remove(url: Platform.DevToolsPath.UrlString): void {
764
765
  const index = this.index(url);
765
766
  if (index !== -1) {
766
767
  this.items.splice(index, 1);
@@ -786,7 +787,7 @@ export class History {
786
787
  return serializedHistory;
787
788
  }
788
789
 
789
- urls(): string[] {
790
+ urls(): Platform.DevToolsPath.UrlString[] {
790
791
  const result = [];
791
792
  for (let i = 0; i < this.items.length; ++i) {
792
793
  result.push(this.items[i].url);
package/package.json CHANGED
@@ -55,5 +55,5 @@
55
55
  "unittest": "scripts/test/run_unittests.py --no-text-coverage",
56
56
  "watch": "vpython third_party/node/node.py --output scripts/watch_build.js"
57
57
  },
58
- "version": "1.0.999791"
58
+ "version": "1.0.1000057"
59
59
  }
@@ -0,0 +1,110 @@
1
+ // Copyright 2020 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
+ 'use strict';
5
+
6
+ function goToClassDeclaration(node) {
7
+ if (!node) {
8
+ return null;
9
+ }
10
+
11
+ if (node.type === 'ClassDeclaration') {
12
+ return node;
13
+ }
14
+
15
+ return goToClassDeclaration(node.parent);
16
+ }
17
+
18
+ function isMemberExpressionOnThis(memberExpression) {
19
+ if (!memberExpression) {
20
+ return false;
21
+ }
22
+
23
+ if (memberExpression.object.type === 'ThisExpression') {
24
+ // Take into `a.this.bind()` case into account
25
+ // `this` must be the last object in the `MemberExpression` chain
26
+ return !memberExpression.object.object;
27
+ }
28
+
29
+ return isMemberExpressionOnThis(memberExpression.object);
30
+ }
31
+
32
+ // Whether the right hand side of property definition is `this.xxx.yyy.bind(this);`
33
+ function isPropertyDefinitionViaBindCallToThis(propertyDefinition) {
34
+ if (propertyDefinition.value.type !== 'CallExpression' ||
35
+ propertyDefinition.value.callee.type !== 'MemberExpression') {
36
+ return false;
37
+ }
38
+
39
+ const isCalleeObjectThis = isMemberExpressionOnThis(propertyDefinition.value.callee);
40
+ // Whether the CallExpression is on a property of `this` (this.xxx.yyy.bind)
41
+ if (!isCalleeObjectThis) {
42
+ return false;
43
+ }
44
+
45
+ const isItBindCall = propertyDefinition.value.callee.property.name === 'bind';
46
+ // Whether the CallExpression is a `bind` call on a property of `this`
47
+ if (!isItBindCall) {
48
+ return false;
49
+ }
50
+
51
+ const callArgument = propertyDefinition.value.arguments[0];
52
+ // Call argument to `bind` is not `this`
53
+ if (!callArgument || callArgument.type !== 'ThisExpression') {
54
+ return false;
55
+ }
56
+
57
+ return true;
58
+ }
59
+
60
+ // Whether the property definition is arrow function like `#render = () => {}`
61
+ function isPropertyDefinitionViaArrowFunction(propertyDefinition) {
62
+ return propertyDefinition.value.type === 'ArrowFunctionExpression';
63
+ }
64
+
65
+ module.exports = {
66
+ meta: {
67
+ type: 'problem',
68
+ docs: {
69
+ description: 'Enforce render method to be bound while calling scheduleRender',
70
+ category: 'Possible Errors',
71
+ },
72
+ fixable: 'code',
73
+ schema: [] // no options
74
+ },
75
+ create: function(context) {
76
+ return {
77
+ CallExpression(node) {
78
+ // Calls in the form of `ScheduledRender.scheduleRender`
79
+ const isScheduleRenderCall = node.callee.type === 'MemberExpression' &&
80
+ node.callee.object?.property?.name === 'ScheduledRender' && node.callee.property?.name === 'scheduleRender';
81
+ if (!isScheduleRenderCall) {
82
+ return;
83
+ }
84
+
85
+ const callbackArgument = node.arguments[1];
86
+ // Whether the second argument points to a property of `this`
87
+ // like `ScheduledRender.scheduleRender(<any>, this.<any>)
88
+ if (callbackArgument.type !== 'MemberExpression' || callbackArgument.object.type !== 'ThisExpression') {
89
+ return;
90
+ }
91
+
92
+ const containingClassForTheCall = goToClassDeclaration(node);
93
+ // Only care about the calls in custom components
94
+ if (!containingClassForTheCall.superClass || containingClassForTheCall.superClass.name !== 'HTMLElement') {
95
+ return;
96
+ }
97
+
98
+ const calledMethod = callbackArgument.property;
99
+ // Check whether the called method is bound (it should be 'PropertyDefinition')
100
+ const propertyDefinition = containingClassForTheCall.body.body.find(
101
+ bodyNode => bodyNode.type === 'PropertyDefinition' && bodyNode.key.name === calledMethod.name);
102
+ if (!propertyDefinition ||
103
+ (!isPropertyDefinitionViaArrowFunction(propertyDefinition) &&
104
+ !isPropertyDefinitionViaBindCallToThis(propertyDefinition))) {
105
+ context.report({node, message: 'Bind `render` method of `scheduleRender` to `this` in components'});
106
+ }
107
+ }
108
+ };
109
+ }
110
+ };
@@ -0,0 +1,74 @@
1
+ // Copyright 2020 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
+ 'use strict';
5
+
6
+ const rule = require('../lib/enforce_bound_render_for_schedule_render.js');
7
+ const ruleTester = new (require('eslint').RuleTester)({
8
+ parserOptions: {ecmaVersion: 9, sourceType: 'module'},
9
+ parser: require.resolve('@typescript-eslint/parser'),
10
+ });
11
+
12
+ ruleTester.run('enforce_bound_render_for_schedule_render', rule, {
13
+ valid: [
14
+ {
15
+ code: `
16
+ class Component extends HTMLElement {
17
+ #boundRender = this.#render.bind(this);
18
+ get data(data) {
19
+ this.data = data;
20
+ void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender);
21
+ }
22
+
23
+ #render() {}
24
+ }
25
+ `,
26
+ filename: 'front_end/ui/components/foo/Foo.ts',
27
+ },
28
+ {
29
+ code: `
30
+ class Component extends HTMLElement {
31
+ get data(data) {
32
+ this.data = data;
33
+ void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
34
+ }
35
+
36
+ #render = () => {};
37
+ }
38
+ `,
39
+ filename: 'front_end/ui/components/foo/Foo.ts',
40
+ },
41
+ {
42
+ code: `
43
+ class Renderer {
44
+ render() {
45
+ }
46
+ }
47
+
48
+ class Component extends HTMLElement {
49
+ #renderer = new Renderer();
50
+ #boundRender = this.#renderer.render.bind(this);
51
+ get data(data) {
52
+ this.data = data;
53
+ void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender);
54
+ }
55
+ }
56
+ `,
57
+ filename: 'front_end/ui/components/foo/Foo.ts',
58
+ },
59
+ ],
60
+ invalid: [
61
+ {
62
+ code: `class Component extends HTMLElement {
63
+ get data(data) {
64
+ this.data = data;
65
+ void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
66
+ }
67
+
68
+ #render() {}
69
+ }`,
70
+ filename: 'front_end/components/test.ts',
71
+ errors: [{message: 'Bind `render` method of `scheduleRender` to `this` in components'}],
72
+ },
73
+ ]
74
+ });