eslint-plugin-use-disposables 0.1.0 → 0.1.1
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.
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
2
|
type MessageIds = 'using' | 'awaitUsing' | 'suggestUsing' | 'suggestAwaitUsing';
|
|
3
|
+
/** A type name, matching any declaration of it, or one declared in a package's own files. */
|
|
4
|
+
type Exception = string | {
|
|
5
|
+
name: string;
|
|
6
|
+
package: string;
|
|
7
|
+
};
|
|
8
|
+
type Options = [
|
|
9
|
+
{
|
|
10
|
+
defaultExceptions?: Exception[];
|
|
11
|
+
customExceptions?: Exception[];
|
|
12
|
+
}?
|
|
13
|
+
];
|
|
3
14
|
/**
|
|
4
15
|
* A disposable created into a plain variable, which nothing will ever dispose.
|
|
5
16
|
*
|
|
@@ -7,5 +18,5 @@ type MessageIds = 'using' | 'awaitUsing' | 'suggestUsing' | 'suggestAwaitUsing';
|
|
|
7
18
|
* borrows a resource someone else owns, and `using` there would dispose it out from under them.
|
|
8
19
|
* A variable that is returned, passed, yielded, stored or exported has handed ownership on.
|
|
9
20
|
*/
|
|
10
|
-
declare const rule: TSESLint.RuleModule<MessageIds,
|
|
21
|
+
declare const rule: TSESLint.RuleModule<MessageIds, Options>;
|
|
11
22
|
export default rule;
|
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node types where "never disposed" is the normal case: the work finishes or is cancelled by itself,
|
|
3
|
+
* and `using` would kill the child, abort the stream or cancel the timer. `net.Server` and
|
|
4
|
+
* `dgram.Socket` are left out on purpose — one left listening is the leak this rule is for.
|
|
5
|
+
*/
|
|
6
|
+
const nodeExceptions = [
|
|
7
|
+
'ChildProcess',
|
|
8
|
+
'Worker',
|
|
9
|
+
'Timeout',
|
|
10
|
+
'Immediate',
|
|
11
|
+
'Readable',
|
|
12
|
+
'Writable',
|
|
13
|
+
'Utf8Stream',
|
|
14
|
+
'Interface',
|
|
15
|
+
'CPUProfileHandle',
|
|
16
|
+
'SyncCPUProfileHandle',
|
|
17
|
+
'HeapProfileHandle',
|
|
18
|
+
'GCProfiler',
|
|
19
|
+
].map(name => ({ name, package: '@types/node' }));
|
|
20
|
+
const exceptionSchema = {
|
|
21
|
+
type: 'array',
|
|
22
|
+
items: {
|
|
23
|
+
oneOf: [
|
|
24
|
+
{ type: 'string' },
|
|
25
|
+
{
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: { name: { type: 'string' }, package: { type: 'string' } },
|
|
28
|
+
required: ['name', 'package'],
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
1
34
|
/**
|
|
2
35
|
* A disposable created into a plain variable, which nothing will ever dispose.
|
|
3
36
|
*
|
|
@@ -6,17 +39,23 @@
|
|
|
6
39
|
* A variable that is returned, passed, yielded, stored or exported has handed ownership on.
|
|
7
40
|
*/
|
|
8
41
|
const rule = {
|
|
9
|
-
defaultOptions: [],
|
|
42
|
+
defaultOptions: [{ defaultExceptions: nodeExceptions, customExceptions: [] }],
|
|
10
43
|
meta: {
|
|
11
44
|
type: 'problem',
|
|
12
45
|
hasSuggestions: true,
|
|
13
46
|
docs: {
|
|
14
47
|
description: 'Require a disposable held in a variable to be declared with `using`, or handed on',
|
|
15
48
|
},
|
|
16
|
-
schema: [
|
|
49
|
+
schema: [
|
|
50
|
+
{
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: { defaultExceptions: exceptionSchema, customExceptions: exceptionSchema },
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
17
56
|
messages: {
|
|
18
|
-
using: 'Disposable
|
|
19
|
-
awaitUsing: 'Disposable
|
|
57
|
+
using: 'Disposable assigned to a `{{kind}}` variable. Use `using` instead.',
|
|
58
|
+
awaitUsing: 'Disposable assigned to a `{{kind}}` variable. Use `await using` instead.',
|
|
20
59
|
suggestUsing: 'Declare it with `using`.',
|
|
21
60
|
suggestAwaitUsing: 'Declare it with `await using`.',
|
|
22
61
|
},
|
|
@@ -28,12 +67,37 @@ const rule = {
|
|
|
28
67
|
return {};
|
|
29
68
|
const { esTreeNodeToTSNodeMap } = services;
|
|
30
69
|
const checker = services.program.getTypeChecker();
|
|
70
|
+
const options = context.options[0];
|
|
71
|
+
const exceptions = [
|
|
72
|
+
...options?.defaultExceptions ?? nodeExceptions,
|
|
73
|
+
...options?.customExceptions ?? [],
|
|
74
|
+
];
|
|
75
|
+
const declaredIn = (symbol, pkg) => (symbol.getDeclarations() ?? [])
|
|
76
|
+
.some(declaration => declaration.getSourceFile().fileName.includes(`/node_modules/${pkg}/`));
|
|
77
|
+
const matches = (symbol) => exceptions.some(exception => typeof exception === 'string'
|
|
78
|
+
? symbol.getName() === exception
|
|
79
|
+
: symbol.getName() === exception.name && declaredIn(symbol, exception.package));
|
|
80
|
+
/** Whether the type, or any class or interface it extends, is an exception. */
|
|
81
|
+
const isExcepted = (type, seen = new Set()) => {
|
|
82
|
+
const symbol = type.getSymbol();
|
|
83
|
+
if (!symbol || seen.has(symbol))
|
|
84
|
+
return false;
|
|
85
|
+
seen.add(symbol);
|
|
86
|
+
if (matches(symbol))
|
|
87
|
+
return true;
|
|
88
|
+
const declared = checker.getDeclaredTypeOfSymbol(symbol);
|
|
89
|
+
if (!declared.isClassOrInterface())
|
|
90
|
+
return false;
|
|
91
|
+
return (checker.getBaseTypes(declared) ?? []).some(base => isExcepted(base, seen));
|
|
92
|
+
};
|
|
31
93
|
// TypeScript keys `[Symbol.dispose]` as `__@dispose@<id>`.
|
|
32
94
|
const hasProperty = (type, prefix) => type.getProperties().some(property => String(property.escapedName).startsWith(prefix));
|
|
33
95
|
/** How the value can be disposed, when every branch that is not nullish can be. */
|
|
34
96
|
const disposal = (type) => {
|
|
35
97
|
const present = checker.getNonNullableType(type);
|
|
36
98
|
const parts = present.isUnion() ? present.types : [present];
|
|
99
|
+
if (parts.some(part => isExcepted(part)))
|
|
100
|
+
return undefined;
|
|
37
101
|
const sync = parts.map(part => hasProperty(part, '__@dispose@'));
|
|
38
102
|
const async = parts.map(part => hasProperty(part, '__@asyncDispose@'));
|
|
39
103
|
if (sync.every(Boolean))
|
|
@@ -151,6 +215,7 @@ const rule = {
|
|
|
151
215
|
context.report({
|
|
152
216
|
node: declarator.id,
|
|
153
217
|
messageId: kind === 'sync' ? 'using' : 'awaitUsing',
|
|
218
|
+
data: { kind: declaration.kind },
|
|
154
219
|
suggest: keyword && canSuggest(declaration, declarator, variables, kind)
|
|
155
220
|
? [{
|
|
156
221
|
messageId: kind === 'sync' ? 'suggestUsing' : 'suggestAwaitUsing',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-use-disposables",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Require a disposable held in a variable to be declared with `using`, or handed on.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"typescript": ">=5.2"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@types/node": "^
|
|
30
|
+
"@types/node": "^24.19.1",
|
|
31
31
|
"@typescript-eslint/utils": "^8.70.0",
|
|
32
32
|
"eslint": "^10.8.1",
|
|
33
33
|
"typescript-eslint": "^8.70.0"
|