@simbiat/eslint-plugin-simbiat 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/dist/Plugin.d.mts +19 -0
- package/dist/Plugin.mjs +29 -0
- package/dist/rules/NoExternalListenersInConstructor.d.mts +18 -0
- package/dist/rules/NoExternalListenersInConstructor.mjs +81 -0
- package/dist/rules/NoForbiddenInConstructor.d.mts +28 -0
- package/dist/rules/NoForbiddenInConstructor.mjs +348 -0
- package/dist/rules/NoKeypressEvent.d.mts +13 -0
- package/dist/rules/NoKeypressEvent.mjs +136 -0
- package/dist/rules/PreferFieldInitializer.d.mts +21 -0
- package/dist/rules/PreferFieldInitializer.mjs +289 -0
- package/dist/rules/RequireListenerCleanup.d.mts +28 -0
- package/dist/rules/RequireListenerCleanup.mjs +361 -0
- package/dist/rules/RequireSuperFirstInConstructor.d.mts +18 -0
- package/dist/rules/RequireSuperFirstInConstructor.mjs +117 -0
- package/dist/rules/RequireTypeParameter.d.mts +13 -0
- package/dist/rules/RequireTypeParameter.mjs +71 -0
- package/dist/utils/ASTHelpers.d.mts +70 -0
- package/dist/utils/ASTHelpers.mjs +275 -0
- package/dist/utils/Adapters.d.mts +21 -0
- package/dist/utils/Adapters.mjs +29 -0
- package/dist/utils/CustomElementsScope.d.mts +80 -0
- package/dist/utils/CustomElementsScope.mjs +270 -0
- package/package.json +50 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Rule: simbiat/no-forbidden-in-constructor.
|
|
3
|
+
*
|
|
4
|
+
* Flags everything the Custom Elements spec says you cannot or should not do
|
|
5
|
+
* during the construction phase — both in the constructor body and in instance
|
|
6
|
+
* field initializers (which run as part of construction, before the element
|
|
7
|
+
* is connected).
|
|
8
|
+
*
|
|
9
|
+
* Attribute / property writes: any `this.x = value` assignment where `x` is not declared as a class field (PropertyDefinition) in the current class body is flagged.
|
|
10
|
+
*
|
|
11
|
+
* DOMTokenList mutation: this.classList.add / remove / toggle / replace (…), this.part.add / remove / toggle / replace (…)
|
|
12
|
+
*
|
|
13
|
+
* Chained attribute / style writes: this.dataset.<key> = value / this.style.<prop> = value
|
|
14
|
+
*
|
|
15
|
+
* Method-based attribute manipulation: this.setAttribute(…) / this.toggleAttribute(…)
|
|
16
|
+
*
|
|
17
|
+
* Child / content access (reads and mutations via children-related properties
|
|
18
|
+
* and methods such as querySelector, appendChild, innerHTML, etc.)
|
|
19
|
+
*
|
|
20
|
+
* Forbidden global calls: document.write(…) / document.open(…)
|
|
21
|
+
*
|
|
22
|
+
* Illegal return (constructor only): any `return <expr>` that is not a bare `return` or `return this`.
|
|
23
|
+
*
|
|
24
|
+
* Options: baseClasses: string[] – additional class names to treat as HTMLElement. Defaults to ['HTMLElement'].
|
|
25
|
+
*/
|
|
26
|
+
import { adaptNodeHandler } from '../utils/Adapters.mjs';
|
|
27
|
+
import { isActiveScope, getActiveScopeLocation, getClassFieldNames, buildScopeVisitors, baseClassesSchema, } from '../utils/CustomElementsScope.mjs';
|
|
28
|
+
// Static member sets
|
|
29
|
+
/**
|
|
30
|
+
* Reading any of these own-child properties is forbidden during construction
|
|
31
|
+
* (child elements are absent until the element is connected).
|
|
32
|
+
*
|
|
33
|
+
* These are READ checks via onMemberExpression. Assignments to them would
|
|
34
|
+
* also be caught by the "not a class field" write check, but we skip them
|
|
35
|
+
* there to avoid double reports.
|
|
36
|
+
*/
|
|
37
|
+
const FORBIDDEN_CHILD_PROPS = new Set([
|
|
38
|
+
'children',
|
|
39
|
+
'childNodes',
|
|
40
|
+
'firstChild',
|
|
41
|
+
'lastChild',
|
|
42
|
+
'firstElementChild',
|
|
43
|
+
'lastElementChild',
|
|
44
|
+
'childElementCount',
|
|
45
|
+
]);
|
|
46
|
+
/**
|
|
47
|
+
* Calling any of these on `this` is forbidden during construction.
|
|
48
|
+
* Covers: child queries, all DOM-mutation methods that gain / rearrange /
|
|
49
|
+
* remove children, and ChildNode self-manipulation methods.
|
|
50
|
+
*/
|
|
51
|
+
const FORBIDDEN_CHILD_METHODS = new Set([
|
|
52
|
+
// child queries
|
|
53
|
+
'querySelector',
|
|
54
|
+
'querySelectorAll',
|
|
55
|
+
'getElementsByTagName',
|
|
56
|
+
'getElementsByClassName',
|
|
57
|
+
'getElementsByName',
|
|
58
|
+
// classic DOM mutation
|
|
59
|
+
'appendChild',
|
|
60
|
+
'insertBefore',
|
|
61
|
+
'replaceChild',
|
|
62
|
+
'removeChild',
|
|
63
|
+
// ParentNode / Element convenience
|
|
64
|
+
'append',
|
|
65
|
+
'prepend',
|
|
66
|
+
'replaceChildren',
|
|
67
|
+
// adjacent insertion
|
|
68
|
+
'insertAdjacentHTML',
|
|
69
|
+
'insertAdjacentElement',
|
|
70
|
+
'insertAdjacentText',
|
|
71
|
+
// ChildNode self-manipulation (no parent exists during construction)
|
|
72
|
+
'after',
|
|
73
|
+
'before',
|
|
74
|
+
'replaceWith',
|
|
75
|
+
'remove',
|
|
76
|
+
]);
|
|
77
|
+
/** Calling these attribute-manipulation methods on `this` is forbidden. */
|
|
78
|
+
const FORBIDDEN_ATTR_METHODS = new Set(['setAttribute', 'toggleAttribute']);
|
|
79
|
+
/**
|
|
80
|
+
* Writing to any of these properties replaces or deeply modifies element
|
|
81
|
+
* content. Reported with a specific message rather than the generic one.
|
|
82
|
+
*/
|
|
83
|
+
const FORBIDDEN_CONTENT_PROP_WRITES = new Set([
|
|
84
|
+
'innerHTML',
|
|
85
|
+
'outerHTML',
|
|
86
|
+
'textContent',
|
|
87
|
+
'innerText',
|
|
88
|
+
]);
|
|
89
|
+
/** DOMTokenList-typed properties whose mutating methods set reflected attrs. */
|
|
90
|
+
const TOKEN_LIST_PROPS = new Set(['classList', 'part']);
|
|
91
|
+
/** Mutating methods on DOMTokenList instances. */
|
|
92
|
+
const TOKEN_LIST_MUTATING_METHODS = new Set(['add', 'remove', 'toggle', 'replace']);
|
|
93
|
+
/**
|
|
94
|
+
* Properties whose subproperties correspond to HTML attributes or CSS: dataset → data-* attributes / style → inline CSS properties.
|
|
95
|
+
*/
|
|
96
|
+
const CHAINED_WRITE_PROPS = new Set(['dataset', 'style']);
|
|
97
|
+
/** Calling document.x() where x is in this set is forbidden. */
|
|
98
|
+
const FORBIDDEN_DOCUMENT_METHODS = new Set(['write', 'open']);
|
|
99
|
+
// Visitor handlers
|
|
100
|
+
/**
|
|
101
|
+
* Reports forbidden method calls on `this`, DOMTokenList members, or document.
|
|
102
|
+
* @param state - Rule state including ESLint context and scope stack.
|
|
103
|
+
* @param node - CallExpression node to inspect (as unknown from ESLint).
|
|
104
|
+
*/
|
|
105
|
+
function onCallExpression(state, node) {
|
|
106
|
+
if (!isActiveScope(state)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const call = node;
|
|
110
|
+
const { callee } = call;
|
|
111
|
+
if (callee.type !== 'MemberExpression') {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (callee.property.type !== 'Identifier') {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// this.method(…)
|
|
118
|
+
if (callee.object.type === 'ThisExpression') {
|
|
119
|
+
const method = callee.property.name;
|
|
120
|
+
if (FORBIDDEN_ATTR_METHODS.has(method)) {
|
|
121
|
+
state.context.report({
|
|
122
|
+
node: node,
|
|
123
|
+
messageId: 'attrMethod',
|
|
124
|
+
data: {
|
|
125
|
+
method,
|
|
126
|
+
location: getActiveScopeLocation(state),
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else if (FORBIDDEN_CHILD_METHODS.has(method)) {
|
|
131
|
+
state.context.report({
|
|
132
|
+
node: node,
|
|
133
|
+
messageId: 'childMethod',
|
|
134
|
+
data: {
|
|
135
|
+
method,
|
|
136
|
+
location: getActiveScopeLocation(state),
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// this.classList.add / this.part.remove / …
|
|
143
|
+
if (callee.object.type === 'MemberExpression'
|
|
144
|
+
&& !callee.object.computed
|
|
145
|
+
&& callee.object.object.type === 'ThisExpression'
|
|
146
|
+
&& callee.object.property.type === 'Identifier'
|
|
147
|
+
&& TOKEN_LIST_PROPS.has(callee.object.property.name)
|
|
148
|
+
&& TOKEN_LIST_MUTATING_METHODS.has(callee.property.name)) {
|
|
149
|
+
state.context.report({
|
|
150
|
+
node: node,
|
|
151
|
+
messageId: 'tokenListMutation',
|
|
152
|
+
data: {
|
|
153
|
+
prop: callee.object.property.name,
|
|
154
|
+
method: callee.property.name,
|
|
155
|
+
location: getActiveScopeLocation(state),
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// document.write(…) / document.open(…)
|
|
161
|
+
if (callee.object.type === 'Identifier'
|
|
162
|
+
&& callee.object.name === 'document'
|
|
163
|
+
&& FORBIDDEN_DOCUMENT_METHODS.has(callee.property.name)) {
|
|
164
|
+
state.context.report({
|
|
165
|
+
node: node,
|
|
166
|
+
messageId: 'documentMethod',
|
|
167
|
+
data: {
|
|
168
|
+
method: callee.property.name,
|
|
169
|
+
location: getActiveScopeLocation(state),
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Flags reads of forbidden own-child properties on `this`.
|
|
176
|
+
* @param state - Rule state including ESLint context and scope stack.
|
|
177
|
+
* @param node - MemberExpression node to inspect (as unknown from ESLint).
|
|
178
|
+
*/
|
|
179
|
+
function onMemberExpression(state, node) {
|
|
180
|
+
if (!isActiveScope(state)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const mem = node;
|
|
184
|
+
// Only flag `this.prop` - not `this.shadowRoot.prop` etc.
|
|
185
|
+
if (mem.object.type !== 'ThisExpression') {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (mem.property.type !== 'Identifier') {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (mem.computed) {
|
|
192
|
+
return; // skip this['children'] - unusual enough to ignore
|
|
193
|
+
}
|
|
194
|
+
const prop = mem.property.name;
|
|
195
|
+
if (FORBIDDEN_CHILD_PROPS.has(prop)) {
|
|
196
|
+
state.context.report({
|
|
197
|
+
node: node,
|
|
198
|
+
messageId: 'childProp',
|
|
199
|
+
data: {
|
|
200
|
+
prop,
|
|
201
|
+
location: getActiveScopeLocation(state),
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Flags three categories of write during construction:
|
|
208
|
+
* 1. This.<contentProp> = … e.g., this.innerHTML = '<div>'
|
|
209
|
+
* 2. `this.<anything>` = … where <anything> is not a declared class field
|
|
210
|
+
* 3. This.dataset.<key> = … / this.style.<prop> = ….
|
|
211
|
+
* Only simple `=` assignments; compound operators (+=, |=, …) are left alone.
|
|
212
|
+
* @param state - Rule state including ESLint context and scope stack.
|
|
213
|
+
* @param node - AssignmentExpression node to inspect (as unknown from ESLint).
|
|
214
|
+
*/
|
|
215
|
+
function onAssignmentExpression(state, node) {
|
|
216
|
+
if (!isActiveScope(state)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const assign = node;
|
|
220
|
+
if (assign.operator !== '=') {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const { left } = assign;
|
|
224
|
+
if (left.type !== 'MemberExpression') {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Branch 1: this.prop = value
|
|
228
|
+
if (!left.computed
|
|
229
|
+
&& left.object.type === 'ThisExpression'
|
|
230
|
+
&& left.property.type === 'Identifier') {
|
|
231
|
+
const prop = left.property.name;
|
|
232
|
+
const location = getActiveScopeLocation(state);
|
|
233
|
+
if (FORBIDDEN_CONTENT_PROP_WRITES.has(prop)) {
|
|
234
|
+
state.context.report({
|
|
235
|
+
node: node,
|
|
236
|
+
messageId: 'contentPropWrite',
|
|
237
|
+
data: {
|
|
238
|
+
prop,
|
|
239
|
+
location,
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
// Avoid double-reporting what onMemberExpression already flags as childProp.
|
|
245
|
+
if (FORBIDDEN_CHILD_PROPS.has(prop)) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// Everything else that is not an explicit class field is potentially a
|
|
249
|
+
// reflected HTML/ARIA attribute (or state that should be declared).
|
|
250
|
+
if (!getClassFieldNames(state)
|
|
251
|
+
.has(prop)) {
|
|
252
|
+
state.context.report({
|
|
253
|
+
node: node,
|
|
254
|
+
messageId: 'undeclaredPropWrite',
|
|
255
|
+
data: {
|
|
256
|
+
prop,
|
|
257
|
+
location,
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
// Branch 2: this.dataset.<key> = value / this.style.<prop> = value
|
|
264
|
+
if (left.object.type === 'MemberExpression'
|
|
265
|
+
&& !left.object.computed
|
|
266
|
+
&& left.object.object.type === 'ThisExpression'
|
|
267
|
+
&& left.object.property.type === 'Identifier'
|
|
268
|
+
&& CHAINED_WRITE_PROPS.has(left.object.property.name)) {
|
|
269
|
+
state.context.report({
|
|
270
|
+
node: node,
|
|
271
|
+
messageId: 'chainedPropWrite',
|
|
272
|
+
data: {
|
|
273
|
+
prop: left.object.property.name,
|
|
274
|
+
location: getActiveScopeLocation(state),
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Flags `return <expr>` inside the constructor unless the expression is
|
|
281
|
+
* absent (`return;`) or is exactly `this` (`return this;`).
|
|
282
|
+
*
|
|
283
|
+
* Per spec: "A return statement must not appear anywhere inside the
|
|
284
|
+
* constructor body, unless it is a simple early-return (return or return this)".
|
|
285
|
+
* @param state - Rule state including ESLint context and scope stack.
|
|
286
|
+
* @param node - ReturnStatement node to inspect (as unknown from ESLint).
|
|
287
|
+
*/
|
|
288
|
+
function onReturnStatement(state, node) {
|
|
289
|
+
if (!isActiveScope(state)) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const ret = node;
|
|
293
|
+
if (ret.argument === null) {
|
|
294
|
+
return; // bare `return;` is fine
|
|
295
|
+
}
|
|
296
|
+
if (ret.argument?.type === 'ThisExpression') {
|
|
297
|
+
return; // `return this;` is fine
|
|
298
|
+
}
|
|
299
|
+
state.context.report({
|
|
300
|
+
node: node,
|
|
301
|
+
messageId: 'illegalReturn',
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
// Rule definition
|
|
305
|
+
const noForbiddenInConstructor = {
|
|
306
|
+
meta: {
|
|
307
|
+
type: 'problem',
|
|
308
|
+
docs: {
|
|
309
|
+
description: 'Disallow spec-forbidden operations in Custom Element constructors and field initializers.',
|
|
310
|
+
url: 'https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance',
|
|
311
|
+
},
|
|
312
|
+
messages: {
|
|
313
|
+
attrMethod: 'Do not call this.{{method}}() in {{location}} - attributes cannot be reliably set before the element is upgraded. Move this to connectedCallback.',
|
|
314
|
+
childProp: 'Do not read this.{{prop}} in {{location}} - child elements are not present until the element is connected. Move this to connectedCallback.',
|
|
315
|
+
childMethod: 'Do not call this.{{method}}() in {{location}} - child elements cannot be accessed or modified before the element is connected. Move this to connectedCallback.',
|
|
316
|
+
contentPropWrite: 'Do not assign to this.{{prop}} in {{location}} - this modifies element content before it is connected. Move this to connectedCallback.',
|
|
317
|
+
undeclaredPropWrite: 'this.{{prop}} is not declared as a class field (found in {{location}}). '
|
|
318
|
+
+ 'If \'{{prop}}\' is a reflected HTML or ARIA attribute, move this assignment to connectedCallback. '
|
|
319
|
+
+ 'If it is custom element state, declare it as a class field instead.',
|
|
320
|
+
tokenListMutation: 'Do not call this.{{prop}}.{{method}}() in {{location}} - this modifies a reflected attribute. Move this to connectedCallback.',
|
|
321
|
+
chainedPropWrite: 'Do not write to this.{{prop}} properties in {{location}} - this modifies element attributes or styles. Move this to connectedCallback.',
|
|
322
|
+
documentMethod: 'Do not call document.{{method}}() in {{location}} - this is explicitly forbidden by the Custom Elements spec.',
|
|
323
|
+
illegalReturn: 'The constructor must not return a value other than undefined or this.',
|
|
324
|
+
},
|
|
325
|
+
schema: baseClassesSchema,
|
|
326
|
+
hasSuggestions: false,
|
|
327
|
+
},
|
|
328
|
+
/**
|
|
329
|
+
* Creates rule.
|
|
330
|
+
* @param context - Context to process.
|
|
331
|
+
*/
|
|
332
|
+
create(context) {
|
|
333
|
+
const options = context.options[0];
|
|
334
|
+
const state = {
|
|
335
|
+
context,
|
|
336
|
+
stack: [],
|
|
337
|
+
base_classes: options?.baseClasses ?? ['HTMLElement'],
|
|
338
|
+
};
|
|
339
|
+
return {
|
|
340
|
+
...buildScopeVisitors(state),
|
|
341
|
+
CallExpression: adaptNodeHandler(state, onCallExpression),
|
|
342
|
+
MemberExpression: adaptNodeHandler(state, onMemberExpression),
|
|
343
|
+
AssignmentExpression: adaptNodeHandler(state, onAssignmentExpression),
|
|
344
|
+
ReturnStatement: adaptNodeHandler(state, onReturnStatement),
|
|
345
|
+
};
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
export default noForbiddenInConstructor;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Rule: no-keypress-event.
|
|
3
|
+
*
|
|
4
|
+
* Flags the deprecated `keypress` event and suggests replacing it with `keydown`.
|
|
5
|
+
*
|
|
6
|
+
* Covers two patterns:
|
|
7
|
+
* - element.addEventListener('keypress', handler) → suggestion offered
|
|
8
|
+
* - element.removeEventListener('keypress', handler) → flagged, no suggestion
|
|
9
|
+
* - element.onkeypress = handler → suggestion offered.
|
|
10
|
+
*/
|
|
11
|
+
import type { Rule } from 'eslint';
|
|
12
|
+
declare const noKeypressEvent: Rule.RuleModule;
|
|
13
|
+
export default noKeypressEvent;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Rule: no-keypress-event.
|
|
3
|
+
*
|
|
4
|
+
* Flags the deprecated `keypress` event and suggests replacing it with `keydown`.
|
|
5
|
+
*
|
|
6
|
+
* Covers two patterns:
|
|
7
|
+
* - element.addEventListener('keypress', handler) → suggestion offered
|
|
8
|
+
* - element.removeEventListener('keypress', handler) → flagged, no suggestion
|
|
9
|
+
* - element.onkeypress = handler → suggestion offered.
|
|
10
|
+
*/
|
|
11
|
+
import { adaptNodeHandler } from '../utils/Adapters.mjs';
|
|
12
|
+
const DEPRECATED = 'keypress';
|
|
13
|
+
const REPLACEMENT = 'keydown';
|
|
14
|
+
/**
|
|
15
|
+
* Checks addEventListener / removeEventListener calls whose first argument is
|
|
16
|
+
* the literal string 'keypress'. A fix suggestion is only offered for
|
|
17
|
+
* addEventListener — renaming inside removeEventListener requires the developer
|
|
18
|
+
* to also update the paired registration, so a manual edit is safer.
|
|
19
|
+
* @param context - ESLint rule context.
|
|
20
|
+
* @param node - CallExpression node to inspect.
|
|
21
|
+
*/
|
|
22
|
+
function checkListenerCall(context, node) {
|
|
23
|
+
const call = node;
|
|
24
|
+
if (call.callee.type !== 'MemberExpression'
|
|
25
|
+
|| call.callee.property.type !== 'Identifier') {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const method = call.callee.property.name;
|
|
29
|
+
const is_add = method === 'addEventListener';
|
|
30
|
+
const is_remove = method === 'removeEventListener';
|
|
31
|
+
if (!is_add && !is_remove) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const event_arg = call.arguments[0];
|
|
35
|
+
if (event_arg?.type !== 'Literal'
|
|
36
|
+
|| event_arg.value !== DEPRECATED) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
context.report({
|
|
40
|
+
node: event_arg,
|
|
41
|
+
messageId: 'avoidKeypress',
|
|
42
|
+
suggest: is_add
|
|
43
|
+
? [
|
|
44
|
+
{
|
|
45
|
+
messageId: 'replaceWithKeydown',
|
|
46
|
+
/**
|
|
47
|
+
* Replaces the 'keypress' literal with 'keydown' in the source.
|
|
48
|
+
* @param fixer - ESLint rule fixer.
|
|
49
|
+
* @returns The text replacement fix.
|
|
50
|
+
*/
|
|
51
|
+
fix(fixer) {
|
|
52
|
+
const quote = event_arg.raw[0];
|
|
53
|
+
return fixer.replaceText(event_arg, `${quote}${REPLACEMENT}${quote}`);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
: [],
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Flags `element.onkeypress = handler` assignments and suggests renaming the
|
|
62
|
+
* property to `onkeydown`.
|
|
63
|
+
* @param context - ESLint rule context.
|
|
64
|
+
* @param node - AssignmentExpression node to inspect.
|
|
65
|
+
*/
|
|
66
|
+
function checkOnkeypressAssignment(context, node) {
|
|
67
|
+
const assign = node;
|
|
68
|
+
if (assign.left.type !== 'MemberExpression'
|
|
69
|
+
|| assign.left.computed
|
|
70
|
+
|| assign.left.property.type !== 'Identifier'
|
|
71
|
+
|| assign.left.property.name !== 'onkeypress') {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
context.report({
|
|
75
|
+
node: assign.left.property,
|
|
76
|
+
messageId: 'avoidOnkeypress',
|
|
77
|
+
suggest: [
|
|
78
|
+
{
|
|
79
|
+
messageId: 'replaceWithOnkeydown',
|
|
80
|
+
/**
|
|
81
|
+
* Replaces the 'onkeypress' property with 'onkeydown' in the source.
|
|
82
|
+
* @param fixer - ESLint rule fixer.
|
|
83
|
+
* @returns The text replacement fix.
|
|
84
|
+
*/
|
|
85
|
+
fix(fixer) {
|
|
86
|
+
return fixer.replaceText(assign.left.property, 'onkeydown');
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Top-level ESLint visitor for CallExpression nodes.
|
|
94
|
+
* @param context - ESLint rule context.
|
|
95
|
+
* @param node - CallExpression node.
|
|
96
|
+
*/
|
|
97
|
+
function onCallExpression(context, node) {
|
|
98
|
+
checkListenerCall(context, node);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Top-level ESLint visitor for AssignmentExpression nodes.
|
|
102
|
+
* @param context - ESLint rule context.
|
|
103
|
+
* @param node - AssignmentExpression node.
|
|
104
|
+
*/
|
|
105
|
+
function onAssignmentExpression(context, node) {
|
|
106
|
+
checkOnkeypressAssignment(context, node);
|
|
107
|
+
}
|
|
108
|
+
const noKeypressEvent = {
|
|
109
|
+
meta: {
|
|
110
|
+
type: 'suggestion',
|
|
111
|
+
hasSuggestions: true,
|
|
112
|
+
docs: {
|
|
113
|
+
description: 'Disallow the deprecated `keypress` event in favour of `keydown`.',
|
|
114
|
+
url: 'https://github.com/simbiat/eslint-plugin-simbiat',
|
|
115
|
+
},
|
|
116
|
+
messages: {
|
|
117
|
+
avoidKeypress: '`keypress` is deprecated, use `keydown` instead.',
|
|
118
|
+
replaceWithKeydown: '`keypress` is deprecated, use `keydown` instead.',
|
|
119
|
+
avoidOnkeypress: '`onkeypress` is deprecated, use `onkeydown` instead.',
|
|
120
|
+
replaceWithOnkeydown: '`onkeypress` is deprecated, use `onkeydown` instead.',
|
|
121
|
+
},
|
|
122
|
+
schema: [],
|
|
123
|
+
},
|
|
124
|
+
/**
|
|
125
|
+
* Creates the rule listeners.
|
|
126
|
+
* @param context - ESLint rule context.
|
|
127
|
+
* @returns Rule listener object.
|
|
128
|
+
*/
|
|
129
|
+
create(context) {
|
|
130
|
+
return {
|
|
131
|
+
CallExpression: adaptNodeHandler(context, onCallExpression),
|
|
132
|
+
AssignmentExpression: adaptNodeHandler(context, onAssignmentExpression),
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
export default noKeypressEvent;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Rule: simbiat/prefer-field-initializer.
|
|
3
|
+
* Flags `this.x = expr` assignments in a class `constructor` where all the following hold:
|
|
4
|
+
* 1. `x` already has a class field declaration (PropertyDefinition).
|
|
5
|
+
* 2. The RHS does NOT reference a constructor parameter by name because those values are unavailable at a declaration site.
|
|
6
|
+
* 3. The RHS does NOT reference a local variable declared inside the constructor body because those values are also unavailable at the declaration site.
|
|
7
|
+
* 4. The RHS does NOT contain `this.anything` – field-initializer ordering versus constructor-assignment ordering can differ subtly, so those are left for the developer to evaluate.
|
|
8
|
+
* Only top-level assignment statements in the constructor body are checked.
|
|
9
|
+
* Assignments inside if / else / for / for-of / for-in / while / do-while / switch / try-catch blocks, or inside nested functions / arrow functions, are intentionally ignored – they are conditional or deferred and cannot be safely lifted to a field initializer.
|
|
10
|
+
* No auto-fix is provided: the change involves removing the assignment AND updating the field declaration simultaneously; doing that incorrectly could silently break the program.
|
|
11
|
+
*/
|
|
12
|
+
import type { Rule } from 'eslint';
|
|
13
|
+
/**
|
|
14
|
+
* Stack entry shapes:
|
|
15
|
+
* { kind: 'class', fieldNames: Set<string>, node: ClassNode }
|
|
16
|
+
* { kind: 'method', methodName: string, paramNames: Set<string>, localNames: Set<string> }
|
|
17
|
+
* { kind: 'block' } ← inside a control-flow statement (if/for/while/switch/try)
|
|
18
|
+
* { kind: 'fn' } ← inside a nested function or arrow function.
|
|
19
|
+
*/
|
|
20
|
+
declare const preferFieldInitializer: Rule.RuleModule;
|
|
21
|
+
export default preferFieldInitializer;
|