@tianjos/eslint-plugin-elegant 0.7.2 → 0.8.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/README.md +38 -0
- package/dist/index.d.ts +4 -1
- package/dist/rules/no-instanceof.d.ts +5 -1
- package/dist/rules/no-instanceof.js +40 -3
- package/dist/rules/no-self-mutation.js +23 -22
- package/dist/utils/ancestors.d.ts +10 -0
- package/dist/utils/ancestors.js +22 -0
- package/dist/utils/locals.d.ts +7 -0
- package/dist/utils/locals.js +19 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -245,6 +245,44 @@ around repositories and framework hooks, so it stays off in `recommended`.
|
|
|
245
245
|
the object. Pairs with `no-type-assertion` to keep type-based branching out of
|
|
246
246
|
the codebase.
|
|
247
247
|
|
|
248
|
+
Two uses are allowed by default, because in both of them TypeScript leaves no
|
|
249
|
+
polymorphic alternative to reach for.
|
|
250
|
+
|
|
251
|
+
**A self-guard** — `other instanceof Money` inside `class Money`. Value
|
|
252
|
+
equality has to guard its own type before comparing fields, and there is no
|
|
253
|
+
polymorphic way to write that: the method is already on the object, so the
|
|
254
|
+
rule's own advice has nowhere left to go. Guarding against a *different* type
|
|
255
|
+
(`other instanceof Currency` inside `Money`) is discrimination and is still
|
|
256
|
+
reported. Off via `{ allowSelfGuard: false }`.
|
|
257
|
+
|
|
258
|
+
**A caught value** — a name a `catch` clause introduced. TypeScript types it
|
|
259
|
+
as `unknown`, so `instanceof` is the only narrowing the language offers; no
|
|
260
|
+
method on the value can stand in, because at that point the value has no known
|
|
261
|
+
methods. Resolved through the scope chain, so the narrowing still counts one
|
|
262
|
+
closure deeper. Off via `{ allowCaughtValues: false }`.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
// allowed
|
|
266
|
+
class Money {
|
|
267
|
+
equals(other?: unknown): boolean {
|
|
268
|
+
return other instanceof Money && this.amount === other.amount;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
try { charge(); } catch (error) {
|
|
272
|
+
if (error instanceof HttpException) { log(error.getStatus()); }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// still reported
|
|
276
|
+
if (shape instanceof Circle) { draw(); }
|
|
277
|
+
function handle(error: HttpException) { return error instanceof HttpException; }
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
An error that arrives as a plain parameter rather than through `catch` — Nest's
|
|
281
|
+
`ExceptionFilter.catch(exception, host)`, an RxJS `catchError` callback — is
|
|
282
|
+
**not** covered, because a parameter's type is whatever the signature says and
|
|
283
|
+
the rule reads no type information. Narrow it once at the boundary, or disable
|
|
284
|
+
the rule for those files.
|
|
285
|
+
|
|
248
286
|
#### `no-static-members`
|
|
249
287
|
|
|
250
288
|
Static state and behavior cannot be injected, substituted, or mocked. Prefer
|
package/dist/index.d.ts
CHANGED
|
@@ -40,7 +40,10 @@ declare const rules: {
|
|
|
40
40
|
}], unknown, TSESLint.RuleListener> & {
|
|
41
41
|
name: string;
|
|
42
42
|
};
|
|
43
|
-
'no-instanceof': TSESLint.RuleModule<"noInstanceof", [
|
|
43
|
+
'no-instanceof': TSESLint.RuleModule<"noInstanceof", [{
|
|
44
|
+
allowSelfGuard: boolean;
|
|
45
|
+
allowCaughtValues: boolean;
|
|
46
|
+
}], unknown, TSESLint.RuleListener> & {
|
|
44
47
|
name: string;
|
|
45
48
|
};
|
|
46
49
|
'no-static-members': TSESLint.RuleModule<"staticMember", [{
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
type Options = [{
|
|
2
|
+
allowSelfGuard: boolean;
|
|
3
|
+
allowCaughtValues: boolean;
|
|
4
|
+
}];
|
|
5
|
+
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"noInstanceof", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
2
6
|
name: string;
|
|
3
7
|
};
|
|
4
8
|
export default _default;
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
4
|
+
const ancestors_1 = require("../utils/ancestors");
|
|
3
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const locals_1 = require("../utils/locals");
|
|
7
|
+
/** The name of the class a node sits inside, if it sits inside a named one. */
|
|
8
|
+
const enclosingClass = (node) => {
|
|
9
|
+
const found = (0, ancestors_1.closestAncestor)(node, (candidate) => candidate.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
10
|
+
candidate.type === utils_1.AST_NODE_TYPES.ClassExpression);
|
|
11
|
+
return found?.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
12
|
+
found?.type === utils_1.AST_NODE_TYPES.ClassExpression
|
|
13
|
+
? found.id?.name
|
|
14
|
+
: undefined;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* `other instanceof Money` inside `class Money`. Value equality has to guard
|
|
18
|
+
* its own type before comparing fields, and a structurally typed language
|
|
19
|
+
* offers no polymorphic way to do it: the method is already on the object, so
|
|
20
|
+
* the rule's own advice has nowhere left to go.
|
|
21
|
+
*/
|
|
22
|
+
const isSelfGuard = (node) => node.right.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
23
|
+
node.right.name === enclosingClass(node);
|
|
4
24
|
exports.default = (0, createRule_1.createRule)({
|
|
5
25
|
name: 'no-instanceof',
|
|
6
26
|
meta: {
|
|
@@ -11,12 +31,29 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
11
31
|
messages: {
|
|
12
32
|
noInstanceof: 'Avoid `instanceof`. Replace type discrimination with a polymorphic method on the object.',
|
|
13
33
|
},
|
|
14
|
-
schema: [
|
|
34
|
+
schema: [
|
|
35
|
+
{
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
allowSelfGuard: { type: 'boolean' },
|
|
39
|
+
allowCaughtValues: { type: 'boolean' },
|
|
40
|
+
},
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
15
44
|
},
|
|
16
|
-
defaultOptions: [],
|
|
17
|
-
create(context) {
|
|
45
|
+
defaultOptions: [{ allowSelfGuard: true, allowCaughtValues: true }],
|
|
46
|
+
create(context, [{ allowSelfGuard, allowCaughtValues }]) {
|
|
18
47
|
return {
|
|
19
48
|
'BinaryExpression[operator="instanceof"]'(node) {
|
|
49
|
+
if (allowSelfGuard && isSelfGuard(node)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (allowCaughtValues &&
|
|
53
|
+
node.left.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
54
|
+
(0, locals_1.isCaughtBinding)(context.sourceCode.getScope(node), node.left.name)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
20
57
|
context.report({ node, messageId: 'noInstanceof' });
|
|
21
58
|
},
|
|
22
59
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const utils_1 = require("@typescript-eslint/utils");
|
|
4
|
+
const ancestors_1 = require("../utils/ancestors");
|
|
4
5
|
const createRule_1 = require("../utils/createRule");
|
|
5
6
|
/**
|
|
6
7
|
* Nest calls these after the container has built the instance, so they finish
|
|
@@ -24,36 +25,34 @@ const fieldOf = (target) => target.type === utils_1.AST_NODE_TYPES.MemberExpress
|
|
|
24
25
|
target.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
25
26
|
? target.property.name
|
|
26
27
|
: undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Whether the write sits inside a class body at all. `this` outside one is
|
|
30
|
+
* `module.exports`, `undefined`, or an object literal's own receiver — none of
|
|
31
|
+
* which is an object with holders to surprise, so the rule has nothing to say
|
|
32
|
+
* about them.
|
|
33
|
+
*/
|
|
34
|
+
const isInsideClass = (node) => (0, ancestors_1.closestAncestor)(node, (candidate) => candidate.type === utils_1.AST_NODE_TYPES.ClassBody ||
|
|
35
|
+
candidate.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
36
|
+
candidate.type === utils_1.AST_NODE_TYPES.ClassExpression) !== undefined;
|
|
27
37
|
/**
|
|
28
38
|
* Whether the write happens while the object is still being built. Only the
|
|
29
39
|
* constructor's own body counts: a callback the constructor schedules runs
|
|
30
40
|
* after construction has returned, so it mutates a finished object.
|
|
31
41
|
*/
|
|
32
42
|
const isDuringConstruction = (node) => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return (current.parent?.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
|
|
39
|
-
current.parent.kind === 'constructor');
|
|
40
|
-
}
|
|
41
|
-
current = current.parent;
|
|
42
|
-
}
|
|
43
|
-
return false;
|
|
43
|
+
const fn = (0, ancestors_1.closestAncestor)(node, (candidate) => candidate.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
44
|
+
candidate.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
45
|
+
candidate.type === utils_1.AST_NODE_TYPES.FunctionExpression);
|
|
46
|
+
return (fn?.parent?.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
|
|
47
|
+
fn.parent.kind === 'constructor');
|
|
44
48
|
};
|
|
45
49
|
/** The method a write sits in, if it sits in one directly. */
|
|
46
50
|
const enclosingMethod = (node) => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
: undefined;
|
|
53
|
-
}
|
|
54
|
-
current = current.parent;
|
|
55
|
-
}
|
|
56
|
-
return undefined;
|
|
51
|
+
const method = (0, ancestors_1.closestAncestor)(node, (candidate) => candidate.type === utils_1.AST_NODE_TYPES.MethodDefinition);
|
|
52
|
+
return method?.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
|
|
53
|
+
method.key.type === utils_1.AST_NODE_TYPES.Identifier
|
|
54
|
+
? method.key.name
|
|
55
|
+
: undefined;
|
|
57
56
|
};
|
|
58
57
|
exports.default = (0, createRule_1.createRule)({
|
|
59
58
|
name: 'no-self-mutation',
|
|
@@ -79,7 +78,9 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
79
78
|
create(context, [{ allowedMethods }]) {
|
|
80
79
|
const check = (node, target) => {
|
|
81
80
|
const name = fieldOf(target);
|
|
82
|
-
if (name === undefined ||
|
|
81
|
+
if (name === undefined ||
|
|
82
|
+
!isInsideClass(node) ||
|
|
83
|
+
isDuringConstruction(node)) {
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
86
|
const method = enclosingMethod(node);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* The innermost ancestor a predicate accepts, or `undefined` if the walk
|
|
4
|
+
* reaches the top without one.
|
|
5
|
+
*
|
|
6
|
+
* `Program.parent` is `null` rather than `undefined`, so a walk that only
|
|
7
|
+
* guards against `undefined` runs off the top of the tree and throws — which
|
|
8
|
+
* takes the whole lint run for that file down with it.
|
|
9
|
+
*/
|
|
10
|
+
export declare const closestAncestor: (node: TSESTree.Node, matches: (candidate: TSESTree.Node) => boolean) => TSESTree.Node | undefined;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.closestAncestor = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The innermost ancestor a predicate accepts, or `undefined` if the walk
|
|
6
|
+
* reaches the top without one.
|
|
7
|
+
*
|
|
8
|
+
* `Program.parent` is `null` rather than `undefined`, so a walk that only
|
|
9
|
+
* guards against `undefined` runs off the top of the tree and throws — which
|
|
10
|
+
* takes the whole lint run for that file down with it.
|
|
11
|
+
*/
|
|
12
|
+
const closestAncestor = (node, matches) => {
|
|
13
|
+
let current = node.parent;
|
|
14
|
+
while (current !== null && current !== undefined) {
|
|
15
|
+
if (matches(current)) {
|
|
16
|
+
return current;
|
|
17
|
+
}
|
|
18
|
+
current = current.parent;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
};
|
|
22
|
+
exports.closestAncestor = closestAncestor;
|
package/dist/utils/locals.d.ts
CHANGED
|
@@ -11,3 +11,10 @@ export declare const isReassigned: (variable: TSESLint.Scope.Variable) => boolea
|
|
|
11
11
|
* so such a declaration is load-bearing: inlining it stops compiling.
|
|
12
12
|
*/
|
|
13
13
|
export declare const escapesIntoFunction: (variable: TSESLint.Scope.Variable) => boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Whether a name resolves to a binding a `catch` clause introduced.
|
|
16
|
+
* TypeScript types a caught value as `unknown`, so `instanceof` is the only
|
|
17
|
+
* narrowing the language offers there — no method on the value can stand in
|
|
18
|
+
* for it, because at that point the value has no known methods.
|
|
19
|
+
*/
|
|
20
|
+
export declare const isCaughtBinding: (scope: TSESLint.Scope.Scope, name: string) => boolean;
|
package/dist/utils/locals.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.escapesIntoFunction = exports.isReassigned = void 0;
|
|
3
|
+
exports.isCaughtBinding = exports.escapesIntoFunction = exports.isReassigned = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Whether anything writes to the variable after its declaration. Such a local
|
|
6
6
|
* holds mutable state that no member access stands in for, so it is not a copy
|
|
@@ -24,3 +24,21 @@ const escapesIntoFunction = (variable) => variable.references.some((reference) =
|
|
|
24
24
|
return false;
|
|
25
25
|
});
|
|
26
26
|
exports.escapesIntoFunction = escapesIntoFunction;
|
|
27
|
+
/**
|
|
28
|
+
* Whether a name resolves to a binding a `catch` clause introduced.
|
|
29
|
+
* TypeScript types a caught value as `unknown`, so `instanceof` is the only
|
|
30
|
+
* narrowing the language offers there — no method on the value can stand in
|
|
31
|
+
* for it, because at that point the value has no known methods.
|
|
32
|
+
*/
|
|
33
|
+
const isCaughtBinding = (scope, name) => {
|
|
34
|
+
let current = scope;
|
|
35
|
+
while (current !== null) {
|
|
36
|
+
const found = current.variables.find((variable) => variable.name === name);
|
|
37
|
+
if (found !== undefined) {
|
|
38
|
+
return found.defs.some((def) => def.type === 'CatchClause');
|
|
39
|
+
}
|
|
40
|
+
current = current.upper;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
};
|
|
44
|
+
exports.isCaughtBinding = isCaughtBinding;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tianjos/eslint-plugin-elegant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Opinionated ESLint rules for elegant, behavior-rich TypeScript: honest types, encapsulated state, small uncoupled classes, guard-clause flow, and structured logging. Built for NestJS and DDD codebases.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|