@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dmitrii Kustov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# eslint-plugin-simbiat
|
|
2
|
+
|
|
3
|
+
Custom ESLint rules that are used in the [simbiat.eu](https://github.com/Simbiat/simbiat.ru) project. Created with the use of Claude AI (I am realistically not that proficient) but manually reviewed, adjusted, and tested on the existing codebase.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install --save-dev @simbiat/eslint-plugin-simbiat
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires ESLint
|
|
12
|
+
`>=9.38.0`, [flat config](https://eslint.org/docs/latest/use/configure/configuration-files), and [ESM](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#how-can-i-make-my-typescript-project-output-esm).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Usage (flat config)
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
// eslint.config.js
|
|
20
|
+
import simbiat from '@simbiat/eslint-plugin-simbiat';
|
|
21
|
+
|
|
22
|
+
export default [
|
|
23
|
+
{
|
|
24
|
+
plugins: { simbiat },
|
|
25
|
+
rules: {
|
|
26
|
+
'simbiat/no-forbidden-in-constructor': 'error',
|
|
27
|
+
'simbiat/no-external-listeners-in-constructor': 'warn',
|
|
28
|
+
'simbiat/prefer-field-initializer': 'warn',
|
|
29
|
+
'simbiat/require-type-parameter': 'warn',
|
|
30
|
+
'simbiat/require-super-first-in-constructor': 'error',
|
|
31
|
+
'simbiat/no-keypress-event': 'warn',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Rules
|
|
40
|
+
|
|
41
|
+
### `simbiat/no-forbidden-in-constructor` - *problem*
|
|
42
|
+
|
|
43
|
+
Flags things the [Custom Elements spec](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance)
|
|
44
|
+
forbids in a constructor and in class field definitions:
|
|
45
|
+
|
|
46
|
+
| Category | Members |
|
|
47
|
+
|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
48
|
+
| Attribute manipulation | `this.setAttribute()`, `this.toggleAttribute()`, creation/update attributes like `this.id`, `this.className`, `this.tabIndex`, etc., that are not class fields. |
|
|
49
|
+
| Child-element access | `this.children`, `this.childNodes`, `this.firstChild`, `this.lastChild`, `this.firstElementChild`, `this.lastElementChild`, `this.childElementCount` |
|
|
50
|
+
| Child-element queries | `this.querySelector()`, `this.querySelectorAll()`, `this.getElementsByTagName()`, `this.getElementsByClassName()`, `this.getElementsByName()` |
|
|
51
|
+
| Content mutation | `this.innerHTML`, `this.outerHTML`, `this.textContent`, `this.innerText`, `this.appendChild()`, `this.insertBefore`, `this.replaceChild`, `this.removeChild()`, `this.append()`, `this.prepend()`, `this.replaceChildren()`, `this.insertAdjacentHTML()`, `this.insertAdjacentElement()`, `this.insertAdjacentText()`, `this.after()`, `this.before()`, `this.replaceWith()`, `this.remove()` |
|
|
52
|
+
| Global calls | `document.write()`, `document.open()` |
|
|
53
|
+
| Illegal return | Any `return <expr>;` that is not a bare `return;` or `return this;` |
|
|
54
|
+
|
|
55
|
+
Not flagged: `this.shadowRoot.*`, `this.attachShadow()`, `this.getAttribute()`,
|
|
56
|
+
`this.removeAttribute()`, `appendChild` on shadow-root elements, event
|
|
57
|
+
listeners on shadow-scoped elements.
|
|
58
|
+
|
|
59
|
+
**Options**
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
'simbiat/no-forbidden-in-constructor': ['error', {
|
|
63
|
+
baseClasses: ['HTMLElement', 'LitElement', 'BaseComponent'],
|
|
64
|
+
}]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`baseClasses` defaults to `['HTMLElement']`.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### `simbiat/no-external-listeners-in-constructor` - *suggestion*
|
|
72
|
+
|
|
73
|
+
Flags `addEventListener` calls on `document`, `window`, `document.body`,
|
|
74
|
+
`document.documentElement`, or `document.head` that appear **directly** in
|
|
75
|
+
the constructor body (not inside a nested callback or arrow function).
|
|
76
|
+
|
|
77
|
+
These listeners belong in `connectedCallback`, paired with removal in
|
|
78
|
+
`disconnectedCallback`; otherwise they leak when the element is moved or
|
|
79
|
+
re-inserted into the DOM.
|
|
80
|
+
|
|
81
|
+
**Options** - same `baseClasses` schema as above.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
### `simbiat/require-super-first-in-constructor` - *problem*
|
|
86
|
+
|
|
87
|
+
Flags constructors, that do not start with empty
|
|
88
|
+
`super();`. This is normally flagged by TypeScript but may not be flagged in pure JavaScript.
|
|
89
|
+
|
|
90
|
+
**Options** - same `baseClasses` schema as above.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### `simbiat/require-listener-cleanup` - *suggestion*
|
|
95
|
+
|
|
96
|
+
Verifies that every `addEventListener` call on an external target inside
|
|
97
|
+
`connectedCallback` of an HTMLElement subclass has a matching `removeEventListener` call in `disconnectedCallback`. If the handler in `addEventListener` is a proper class field already, it can be auto-fixed (respective `removeEventListener` will be added).
|
|
98
|
+
|
|
99
|
+
**Options** - same `baseClasses` schema as above.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### `simbiat/prefer-field-initializer` - *suggestion*
|
|
104
|
+
|
|
105
|
+
Flags `this.x = expr` assignments in a constructor when all the following hold:
|
|
106
|
+
|
|
107
|
+
1. `x` already has a class field declaration (`PropertyDefinition`).
|
|
108
|
+
2. The RHS does **not** reference a constructor parameter by name.
|
|
109
|
+
3. The RHS does **not** contain `this.anything` (field-initializer vs.
|
|
110
|
+
constructor-assignment ordering can differ subtly).
|
|
111
|
+
|
|
112
|
+
Only top-level assignments in the constructor body are checked. Assignments
|
|
113
|
+
inside `if`/`for`/`while` blocks, ternaries, or nested functions are
|
|
114
|
+
intentionally ignored.
|
|
115
|
+
|
|
116
|
+
> **Known limitation:** references to local variables defined earlier in the
|
|
117
|
+
> constructor (not parameters) are not detected and may produce false
|
|
118
|
+
> positives. Suppress with `// eslint-disable-next-line` where needed.
|
|
119
|
+
|
|
120
|
+
No auto-fix: the change requires removing the assignment *and* updating the
|
|
121
|
+
field declaration simultaneously.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### `simbiat/require-type-parameter` - *suggestion*
|
|
126
|
+
|
|
127
|
+
Flags `querySelector`, `querySelectorAll` and `closest` calls in TypeScript (`.ts` /
|
|
128
|
+
`.tsx`) files that lack a type parameter.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// ✗ flagged
|
|
132
|
+
const element = document.querySelector('.link');
|
|
133
|
+
|
|
134
|
+
// ✓ OK
|
|
135
|
+
const element = document.querySelector<HTMLAnchorElement>('.link');
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
JS files are left alone. No auto-fix: the correct type depends on the
|
|
139
|
+
selector and must be supplied by the developer.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### `simbiat/no-keypress-event` - *suggestion*
|
|
144
|
+
|
|
145
|
+
Flags use of `keypress` event, since it is deprecated, and `keydown` is recommended as replacement (or `beforeinput` in some cases). Provides autofix except for `removeEventListener`, because these require a complete match, and if the listener being removed is third party, it will not work as expected. Thus, manual intervention is recommended here.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Index file for the plugin.
|
|
3
|
+
*/
|
|
4
|
+
declare const Plugin: {
|
|
5
|
+
meta: {
|
|
6
|
+
name: string;
|
|
7
|
+
url: string;
|
|
8
|
+
};
|
|
9
|
+
rules: {
|
|
10
|
+
'no-forbidden-in-constructor': import("eslint").Rule.RuleModule;
|
|
11
|
+
'no-external-listeners-in-constructor': import("eslint").Rule.RuleModule;
|
|
12
|
+
'prefer-field-initializer': import("eslint").Rule.RuleModule;
|
|
13
|
+
'require-type-parameter': import("eslint").Rule.RuleModule;
|
|
14
|
+
'require-super-first-in-constructor': import("eslint").Rule.RuleModule;
|
|
15
|
+
'require-listener-cleanup': import("eslint").Rule.RuleModule;
|
|
16
|
+
'no-keypress-event': import("eslint").Rule.RuleModule;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export default Plugin;
|
package/dist/Plugin.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Index file for the plugin.
|
|
3
|
+
*/
|
|
4
|
+
// @ts-check
|
|
5
|
+
import noForbiddenInConstructor from './rules/NoForbiddenInConstructor.mjs';
|
|
6
|
+
import noExternalListenersInConstructor from './rules/NoExternalListenersInConstructor.mjs';
|
|
7
|
+
import preferFieldInitializer from './rules/PreferFieldInitializer.mjs';
|
|
8
|
+
import requireListenerCleanup from './rules/RequireListenerCleanup.mjs';
|
|
9
|
+
import requireTypeParameter from './rules/RequireTypeParameter.mjs';
|
|
10
|
+
import requireSuperFirstInConstructor from './rules/RequireSuperFirstInConstructor.mjs';
|
|
11
|
+
import noKeypressEvent from './rules/NoKeypressEvent.mjs';
|
|
12
|
+
const Plugin = {
|
|
13
|
+
meta: {
|
|
14
|
+
name: 'eslint-plugin-simbiat',
|
|
15
|
+
url: 'https://github.com/simbiat/eslint-plugin-simbiat',
|
|
16
|
+
},
|
|
17
|
+
rules: {
|
|
18
|
+
'no-forbidden-in-constructor': noForbiddenInConstructor,
|
|
19
|
+
'no-external-listeners-in-constructor': noExternalListenersInConstructor,
|
|
20
|
+
'prefer-field-initializer': preferFieldInitializer,
|
|
21
|
+
'require-type-parameter': requireTypeParameter,
|
|
22
|
+
'require-super-first-in-constructor': requireSuperFirstInConstructor,
|
|
23
|
+
'require-listener-cleanup': requireListenerCleanup,
|
|
24
|
+
'no-keypress-event': noKeypressEvent,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
// Used by ESLint Config
|
|
28
|
+
// noinspection JSUnusedGlobalSymbols
|
|
29
|
+
export default Plugin;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Rule: simbiat/no-external-listeners-in-constructor.
|
|
3
|
+
*
|
|
4
|
+
* Flags `addEventListener` calls on `document`, `window`, `document.body`,
|
|
5
|
+
* `document.documentElement`, or `document.head` that appear *directly* in
|
|
6
|
+
* the constructor body of a Custom Element (not inside a nested
|
|
7
|
+
* callback/arrow function).
|
|
8
|
+
*
|
|
9
|
+
* These listeners belong in `connectedCallback`, paired with removal in
|
|
10
|
+
* `disconnectedCallback`; otherwise they leak if the element is moved or
|
|
11
|
+
* re-inserted into the DOM.
|
|
12
|
+
*
|
|
13
|
+
* Options:
|
|
14
|
+
* baseClasses: string[] – additional class names to treat as HTMLElement. Defaults to ['HTMLElement'].
|
|
15
|
+
*/
|
|
16
|
+
import type { Rule } from 'eslint';
|
|
17
|
+
declare const noExternalListenersInConstructor: Rule.RuleModule;
|
|
18
|
+
export default noExternalListenersInConstructor;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Rule: simbiat/no-external-listeners-in-constructor.
|
|
3
|
+
*
|
|
4
|
+
* Flags `addEventListener` calls on `document`, `window`, `document.body`,
|
|
5
|
+
* `document.documentElement`, or `document.head` that appear *directly* in
|
|
6
|
+
* the constructor body of a Custom Element (not inside a nested
|
|
7
|
+
* callback/arrow function).
|
|
8
|
+
*
|
|
9
|
+
* These listeners belong in `connectedCallback`, paired with removal in
|
|
10
|
+
* `disconnectedCallback`; otherwise they leak if the element is moved or
|
|
11
|
+
* re-inserted into the DOM.
|
|
12
|
+
*
|
|
13
|
+
* Options:
|
|
14
|
+
* baseClasses: string[] – additional class names to treat as HTMLElement. Defaults to ['HTMLElement'].
|
|
15
|
+
*/
|
|
16
|
+
import { adaptNodeHandler } from '../utils/Adapters.mjs';
|
|
17
|
+
import { isExternalTarget, targetName } from '../utils/ASTHelpers.mjs';
|
|
18
|
+
import { isActiveScope, buildScopeVisitors, baseClassesSchema, } from '../utils/CustomElementsScope.mjs';
|
|
19
|
+
// Visitor handler
|
|
20
|
+
/**
|
|
21
|
+
* Reports `addEventListener` calls on external targets found directly in the constructor.
|
|
22
|
+
* @param state - Rule state including ESLint context and scope stack.
|
|
23
|
+
* @param node - CallExpression node to inspect (as unknown from ESLint).
|
|
24
|
+
*/
|
|
25
|
+
function onCallExpression(state, node) {
|
|
26
|
+
if (!isActiveScope(state)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const call = node;
|
|
30
|
+
const { callee } = call;
|
|
31
|
+
if (callee.type !== 'MemberExpression') {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (callee.property.type !== 'Identifier') {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (callee.property.name !== 'addEventListener') {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!isExternalTarget(callee.object)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
state.context.report({
|
|
44
|
+
node: node,
|
|
45
|
+
messageId: 'externalListener',
|
|
46
|
+
data: { target: targetName(callee.object) },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// Rule definition
|
|
50
|
+
const noExternalListenersInConstructor = {
|
|
51
|
+
meta: {
|
|
52
|
+
type: 'suggestion',
|
|
53
|
+
docs: {
|
|
54
|
+
description: 'Discourage addEventListener on document / window in Custom Element constructors.',
|
|
55
|
+
},
|
|
56
|
+
messages: {
|
|
57
|
+
externalListener: 'Avoid attaching listeners to {{target}} in the constructor. '
|
|
58
|
+
+ 'Add them in connectedCallback and remove them in disconnectedCallback; '
|
|
59
|
+
+ 'otherwise listeners will be lost if the element is moved or re-inserted.',
|
|
60
|
+
},
|
|
61
|
+
schema: baseClassesSchema,
|
|
62
|
+
hasSuggestions: false,
|
|
63
|
+
},
|
|
64
|
+
/**
|
|
65
|
+
* Create rule.
|
|
66
|
+
* @param context - Contect to process.
|
|
67
|
+
*/
|
|
68
|
+
create(context) {
|
|
69
|
+
const options = context.options[0];
|
|
70
|
+
const state = {
|
|
71
|
+
context,
|
|
72
|
+
stack: [],
|
|
73
|
+
base_classes: options?.baseClasses ?? ['HTMLElement'],
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
...buildScopeVisitors(state),
|
|
77
|
+
CallExpression: adaptNodeHandler(state, onCallExpression),
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
export default noExternalListenersInConstructor;
|
|
@@ -0,0 +1,28 @@
|
|
|
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 type { Rule } from 'eslint';
|
|
27
|
+
declare const noForbiddenInConstructor: Rule.RuleModule;
|
|
28
|
+
export default noForbiddenInConstructor;
|