-
Notifications
You must be signed in to change notification settings - Fork 498
/
Copy pathno-bound-component-methods.ts
161 lines (140 loc) · 5.7 KB
/
no-bound-component-methods.ts
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
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type {TSESTree} from '@typescript-eslint/utils';
import {createRule} from './utils/ruleCreator.ts';
// Define types based on TSESTree
type Node = TSESTree.Node;
type ClassDeclaration = TSESTree.ClassDeclaration;
type PropertyDefinition = TSESTree.PropertyDefinition;
type MessageIds = 'nonRenderBindFound';
export default createRule<[], MessageIds>({
name: 'no-bound-component-methods',
meta: {
type: 'problem',
docs: {
description: 'Enforce that no methods that are used as Lit events are bound.',
category: 'Possible Errors',
},
schema: [],
messages: {
nonRenderBindFound:
'Found bound method name {{ methodName }} on {{ componentName }} that was not `render`. Lit-Html binds all event handlers for you automatically so this is not required.',
},
},
defaultOptions: [],
create: function(context) {
function nodeIsHTMLElementClassDeclaration(node: Node): node is ClassDeclaration {
return node.type === 'ClassDeclaration' && node.superClass?.type === 'Identifier' &&
node.superClass.name === 'HTMLElement';
}
const classesToCheck = new Set<ClassDeclaration>();
// Store any method names that were passed to addEventListener.
// With the following code:
// window.addEventListener('click', this.boundOnClick)
// we would add `boundOnClick` to this set.
const addEventListenerCallPropertyNames = new Set<string>();
// Type parameters for the helper function
function checkPropertyDeclarationForBinding(className: string, node: PropertyDefinition): void {
if (!node.value || node.value.type !== 'CallExpression') {
return;
}
if (node.value.callee.type !== 'MemberExpression') {
return;
}
// Ensure property is an Identifier before accessing name
if (node.value.callee.property.type !== 'Identifier' || node.value.callee.property.name !== 'bind') {
return;
}
// At this point we know it's a property of the form:
// someBoundThing = this.thing.bind(X)
// and now we want to check that the argument passed to bind is `this`.
// If the argument to bind is not `this`, we leave it be and move on.
if (node.value.arguments[0]?.type !== 'ThisExpression') {
return;
}
// At this point it's definitely of the form:
// someBoundThing = this.thing.bind(this)
// But we know that `render` may be bound for the scheduler, so if it's render we can move on
if (node.value.callee.object.type === 'MemberExpression' &&
(node.value.callee.object.property.type === 'Identifier' ||
node.value.callee.object.property.type === 'PrivateIdentifier') &&
node.value.callee.object.property.name === 'render') {
return;
}
// Now it's an error UNLESS we found a call to
// addEventListener(x, this.#boundFoo),
// in which case it's allowed.
// Get the property name for the bound method
// #boundFoo = this.foo.bind(this);
// node.key.name === 'boundFoo';
if (node.key.type !== 'PrivateIdentifier' && node.key.type !== 'Identifier') {
return;
}
const boundPropertyName = node.key.name;
if (addEventListenerCallPropertyNames.has(boundPropertyName)) {
return;
}
const methodName = node.value.callee.object.type === 'MemberExpression' &&
node.value.callee.object.property.type === 'Identifier' ?
node.value.callee.object.property.name :
'unknown';
context.report({
node,
messageId: 'nonRenderBindFound',
data: {
componentName: className,
methodName,
}
});
}
function checkClassForBoundMethods(classDeclarationNode: ClassDeclaration): void {
if (!classDeclarationNode.id) {
return;
}
const className = classDeclarationNode.id.name;
const classPropertyDeclarations = classDeclarationNode.body.body.filter((node): node is PropertyDefinition => {
return node.type === 'PropertyDefinition';
});
for (const decl of classPropertyDeclarations) {
checkPropertyDeclarationForBinding(className, decl);
}
}
return {
ClassDeclaration(classDeclarationNode) {
if (!nodeIsHTMLElementClassDeclaration(classDeclarationNode)) {
return;
}
classesToCheck.add(classDeclarationNode);
},
CallExpression(node) {
if (node.callee.type !== 'MemberExpression') {
return;
}
if ((node.callee.property.type !== 'Identifier' && node.callee.property.type !== 'PrivateIdentifier') ||
node.callee.property.name !== 'addEventListener') {
return;
}
const methodArg = node.arguments?.[1];
// Confirm that the argument is this.X, otherwise skip it
if (!methodArg || methodArg.type !== 'MemberExpression') {
return;
}
// Get the property from the addEventListener call
// window.addEventListener('click', this.#boundFoo)
// This will be the node representing `#boundFoo`
// and its `.name` property will be `boundFoo`
const propertyArg = methodArg.property;
// Ensure property type before accessing name
if (propertyArg.type === 'Identifier' || propertyArg.type === 'PrivateIdentifier') {
addEventListenerCallPropertyNames.add(propertyArg.name);
}
},
'Program:exit'() {
for (const classNode of classesToCheck) {
checkClassForBoundMethods(classNode);
}
}
};
}
});