@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 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", [], unknown, TSESLint.RuleListener> & {
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
- declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"noInstanceof", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
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
- let current = node;
34
- while (current !== undefined) {
35
- if (current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
36
- current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
37
- current.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
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
- let current = node;
48
- while (current !== undefined) {
49
- if (current.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
50
- return current.key.type === utils_1.AST_NODE_TYPES.Identifier
51
- ? current.key.name
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 || isDuringConstruction(node)) {
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;
@@ -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;
@@ -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.7.2",
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",