@tianjos/eslint-plugin-elegant 0.5.0 → 0.7.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 +130 -4
- package/dist/index.d.ts +18 -1
- package/dist/index.js +9 -0
- package/dist/rules/max-method-lines.d.ts +7 -0
- package/dist/rules/max-method-lines.js +61 -0
- package/dist/rules/no-generic-error.d.ts +4 -0
- package/dist/rules/no-generic-error.js +49 -0
- package/dist/rules/no-public-mutable-props.d.ts +4 -1
- package/dist/rules/no-public-mutable-props.js +20 -9
- package/dist/rules/no-self-mutation.d.ts +7 -0
- package/dist/rules/no-self-mutation.js +100 -0
- package/dist/rules/no-static-members.d.ts +5 -2
- package/dist/rules/no-static-members.js +81 -5
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ The `recommended` config enables every custom rule plus two native ones,
|
|
|
84
84
|
| `elegant/no-logic-in-constructor` | custom | Any constructor code beyond `this.field = value` stores and a `super(...)` call | `error` |
|
|
85
85
|
| `elegant/no-getters-setters` | custom | `get`/`set` accessors (and `getX`/`setX` methods with `{ methods: true }`) | `error` |
|
|
86
86
|
| `elegant/no-instanceof` | custom | Use of the `instanceof` operator | `error` |
|
|
87
|
-
| `elegant/no-static-members` | custom | Static methods, properties, accessors, and blocks (
|
|
87
|
+
| `elegant/no-static-members` | custom | Static methods, properties, accessors, and blocks (secondary constructors and Nest module factories excepted) | `error` |
|
|
88
88
|
| `elegant/no-null` | custom | The `null` literal as a value (type annotations and direct `return null` excepted) | `error` |
|
|
89
89
|
| `elegant/no-comments-in-function-body` | custom | Comments inside function bodies (directives and empty blocks excepted) | `error` |
|
|
90
90
|
| `elegant/no-else-after-throw` | custom | An `else` branch when the `then` branch always throws | `error` |
|
|
@@ -93,6 +93,9 @@ The `recommended` config enables every custom rule plus two native ones,
|
|
|
93
93
|
| `elegant/no-property-alias` | custom | Locals that only rename a property of an object already in hand | `error` |
|
|
94
94
|
| `elegant/no-property-destructuring` | custom | Destructuring an object already in hand into locals | `error` |
|
|
95
95
|
| `elegant/no-anonymous-param-type` | custom | Parameters typed as an anonymous shape of `minMembers` or more properties | `error` (minMembers 2) |
|
|
96
|
+
| `elegant/no-self-mutation` | custom | Writing to your own fields outside the constructor | `error` |
|
|
97
|
+
| `elegant/no-generic-error` | custom | `throw new Error(...)` and the other built-in error types | `error` |
|
|
98
|
+
| `elegant/max-method-lines` | custom | Named functions and methods longer than `max` lines | `warn` (max 50) |
|
|
96
99
|
| `max-params` | native | Functions declaring more than `max` parameters | `warn` (max 3) |
|
|
97
100
|
| `no-else-return` | native | An `else` branch when the `then` branch always returns (`allowElseIf: false`) | `error` |
|
|
98
101
|
|
|
@@ -183,7 +186,18 @@ throw.
|
|
|
183
186
|
#### `no-public-mutable-props`
|
|
184
187
|
|
|
185
188
|
Public state should be `readonly` so callers cannot break an aggregate's
|
|
186
|
-
invariants. `private`/`protected`
|
|
189
|
+
invariants. A declared `private`/`protected` field is allowed, and so is any
|
|
190
|
+
`readonly` member.
|
|
191
|
+
|
|
192
|
+
Constructor parameter properties are covered at **every** visibility by
|
|
193
|
+
default, which declared fields are not: a `private repo: Repository<Proposal>`
|
|
194
|
+
that nobody marked `readonly` can still be swapped from inside, and unlike a
|
|
195
|
+
declared field it is a collaborator the container handed you. Set
|
|
196
|
+
`{ parameterProperties: 'public' }` to keep the rule to what its name says.
|
|
197
|
+
|
|
198
|
+
This rule asks whether a field is *declared* changeable. Its behavioural
|
|
199
|
+
counterpart is `no-self-mutation`, which asks whether anything actually
|
|
200
|
+
changes it.
|
|
187
201
|
|
|
188
202
|
#### `no-logic-in-constructor`
|
|
189
203
|
|
|
@@ -212,8 +226,46 @@ the codebase.
|
|
|
212
226
|
Static state and behavior cannot be injected, substituted, or mocked. Prefer
|
|
213
227
|
instances (with dependency injection) and a module-level `const` for shared
|
|
214
228
|
values. The `{ allowReadonly: true }` option permits `static readonly`
|
|
215
|
-
constants.
|
|
216
|
-
|
|
229
|
+
constants.
|
|
230
|
+
|
|
231
|
+
Two kinds of static are allowed by default, because neither is behaviour that
|
|
232
|
+
anyone would want to substitute.
|
|
233
|
+
|
|
234
|
+
**A secondary constructor** — a static whose declared return type is the class
|
|
235
|
+
itself. TypeScript cannot overload a constructor, so `static of(...): DueDate`
|
|
236
|
+
inside `DueDate` is the only way to write one, and calling it is
|
|
237
|
+
indistinguishable from calling `new`. That is what separates a named
|
|
238
|
+
constructor from a procedure that moved into a class:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
class DueDate {
|
|
242
|
+
static of(props: DueDateProps): DueDate {} // allowed
|
|
243
|
+
static parse(raw: unknown): DueDate {} // allowed
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
class DocumentFormatter {
|
|
247
|
+
static formatCNPJ(document: string): string {} // reported — a module function
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`this`, `Promise<Self>` and `Self | undefined` all count: a polymorphic, an
|
|
252
|
+
asynchronous and a failing constructor are still constructors. `Self | null`
|
|
253
|
+
does not, because `no-null-return` already owns that shape. The return type has
|
|
254
|
+
to be **written down** — the rule carries no type information, so an
|
|
255
|
+
unannotated `static create() { … }` stays reported. On a factory the annotation
|
|
256
|
+
is one word, and it is what makes the intent legible. Off via
|
|
257
|
+
`{ allowSelfReturning: false }`.
|
|
258
|
+
|
|
259
|
+
**A Nest module factory** — a static returning `DynamicModule` from a class
|
|
260
|
+
decorated with `@Module`. `forRoot`, `forRootAsync`, `register` and
|
|
261
|
+
`registerAsync` are mandated by the framework, not chosen by the design. Both
|
|
262
|
+
halves are required, so naming `DynamicModule` in a return type is not a way
|
|
263
|
+
out of the rule, and a module class gets no blanket exemption for its other
|
|
264
|
+
statics. Off via `{ allowModuleFactories: false }`.
|
|
265
|
+
|
|
266
|
+
Everything else still reports: static accessors (reading one is reaching for
|
|
267
|
+
static state, whatever it returns), `private static` helpers, and static
|
|
268
|
+
classes used as a namespace for functions.
|
|
217
269
|
|
|
218
270
|
#### `no-null`
|
|
219
271
|
|
|
@@ -516,6 +568,80 @@ Unlike its neighbours, this rule does not have a mechanical fix: it asks you to
|
|
|
516
568
|
introduce a named type and decide where it lives. That is a design change, so
|
|
517
569
|
expect adoption to cost more than a find-and-replace.
|
|
518
570
|
|
|
571
|
+
#### `no-self-mutation`
|
|
572
|
+
|
|
573
|
+
`no-public-mutable-props` asks whether a field is *declared* changeable. This
|
|
574
|
+
asks whether anything actually changes it. A write to `this.something` after
|
|
575
|
+
the constructor has returned means the object is not a value anyone can hold
|
|
576
|
+
with confidence: whoever received it a moment ago is now holding something
|
|
577
|
+
else.
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
// reported — each one is a lifecycle, not a value
|
|
581
|
+
this.accessToken = access_token;
|
|
582
|
+
this.isPolling = false;
|
|
583
|
+
this.filterOptionsCache = options;
|
|
584
|
+
this.snsClient = new SNSClient({});
|
|
585
|
+
|
|
586
|
+
// passes — this is where an object is built
|
|
587
|
+
constructor(private readonly token: string) {
|
|
588
|
+
this.expiresAt = expiry(token);
|
|
589
|
+
}
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Compound assignment (`this.count += 1`) and increment (`this.count++`) are
|
|
593
|
+
writes too. A computed write (`this[key] = value`) names no field and is left
|
|
594
|
+
alone. Writing to *another* object (`box.value = v`) is that object's business.
|
|
595
|
+
|
|
596
|
+
A callback the constructor schedules is **not** construction — it runs after
|
|
597
|
+
the constructor returned, so `setTimeout(() => { this.token = load(); })`
|
|
598
|
+
inside a constructor is reported.
|
|
599
|
+
|
|
600
|
+
Nest calls `onModuleInit`, `onApplicationBootstrap`, `onModuleDestroy`,
|
|
601
|
+
`beforeApplicationShutdown` and `onApplicationShutdown` after the container has
|
|
602
|
+
built the instance, finishing a construction the constructor could not — a
|
|
603
|
+
timer needs a running event loop. Those five are allowed by default, and the
|
|
604
|
+
list is the `{ allowedMethods: string[] }` option; pass `[]` to hold them to
|
|
605
|
+
the same standard, or add your own.
|
|
606
|
+
|
|
607
|
+
#### `no-generic-error`
|
|
608
|
+
|
|
609
|
+
`throw new Error('RETRY_BATCH_QUEUE_URL not configured')` describes the failure
|
|
610
|
+
only in a string the thrower is free to reword. A caller that wants to handle
|
|
611
|
+
that case specifically has nothing to catch but `Error`, which every other
|
|
612
|
+
failure also is, so it ends up matching on the message.
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
// reported
|
|
616
|
+
throw new Error('origin codes are required');
|
|
617
|
+
throw new TypeError('not a number');
|
|
618
|
+
|
|
619
|
+
// passes
|
|
620
|
+
throw new MissingOriginCodes();
|
|
621
|
+
throw new ProposalNotFound(id);
|
|
622
|
+
throw error; // rethrow keeps whatever it was
|
|
623
|
+
throw invalidRow(raw); // a factory decides which exception to build
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
Covers the eight built-in error types (`Error`, `TypeError`, `RangeError`,
|
|
627
|
+
`ReferenceError`, `SyntaxError`, `EvalError`, `URIError`, `AggregateError`).
|
|
628
|
+
A subclass is a named exception and passes — that is the whole point. Only
|
|
629
|
+
`throw` is examined: building an `Error` to hand to `Promise.reject` or a
|
|
630
|
+
callback is a different question and belongs to a different rule.
|
|
631
|
+
|
|
632
|
+
#### `max-method-lines`
|
|
633
|
+
|
|
634
|
+
A port of Checkstyle's `MethodLength`, which qulice runs. Measured from the
|
|
635
|
+
signature to the closing brace, so the declaration and the blank lines that
|
|
636
|
+
separate the body's paragraphs count — they are part of what a reader has to
|
|
637
|
+
hold.
|
|
638
|
+
|
|
639
|
+
Named units are measured: methods, function declarations, and a function or
|
|
640
|
+
arrow bound to a `const`. An inline callback is **not** measured on its own,
|
|
641
|
+
because a long callback already makes its host long and the host is what gets
|
|
642
|
+
reported. Configurable via `{ max: number }` (default `50`), and a `warn`
|
|
643
|
+
rather than an `error`, like the other thresholds.
|
|
644
|
+
|
|
519
645
|
## Configuration
|
|
520
646
|
|
|
521
647
|
### Overriding thresholds
|
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,9 @@ declare const rules: {
|
|
|
26
26
|
'no-null-return': TSESLint.RuleModule<"noNullReturn", [], unknown, TSESLint.RuleListener> & {
|
|
27
27
|
name: string;
|
|
28
28
|
};
|
|
29
|
-
'no-public-mutable-props': TSESLint.RuleModule<"mutableProp", [
|
|
29
|
+
'no-public-mutable-props': TSESLint.RuleModule<"mutableProp", [{
|
|
30
|
+
parameterProperties: "public" | "all";
|
|
31
|
+
}], unknown, TSESLint.RuleListener> & {
|
|
30
32
|
name: string;
|
|
31
33
|
};
|
|
32
34
|
'no-logic-in-constructor': TSESLint.RuleModule<"statement" | "computation", [], unknown, TSESLint.RuleListener> & {
|
|
@@ -42,6 +44,8 @@ declare const rules: {
|
|
|
42
44
|
};
|
|
43
45
|
'no-static-members': TSESLint.RuleModule<"staticMember", [{
|
|
44
46
|
allowReadonly: boolean;
|
|
47
|
+
allowSelfReturning: boolean;
|
|
48
|
+
allowModuleFactories: boolean;
|
|
45
49
|
}], unknown, TSESLint.RuleListener> & {
|
|
46
50
|
name: string;
|
|
47
51
|
};
|
|
@@ -80,6 +84,19 @@ declare const rules: {
|
|
|
80
84
|
}], unknown, TSESLint.RuleListener> & {
|
|
81
85
|
name: string;
|
|
82
86
|
};
|
|
87
|
+
'no-self-mutation': TSESLint.RuleModule<"mutatesSelf", [{
|
|
88
|
+
allowedMethods: string[];
|
|
89
|
+
}], unknown, TSESLint.RuleListener> & {
|
|
90
|
+
name: string;
|
|
91
|
+
};
|
|
92
|
+
'no-generic-error': TSESLint.RuleModule<"genericError", [], unknown, TSESLint.RuleListener> & {
|
|
93
|
+
name: string;
|
|
94
|
+
};
|
|
95
|
+
'max-method-lines': TSESLint.RuleModule<"tooManyLines", [{
|
|
96
|
+
max: number;
|
|
97
|
+
}], unknown, TSESLint.RuleListener> & {
|
|
98
|
+
name: string;
|
|
99
|
+
};
|
|
83
100
|
};
|
|
84
101
|
type Plugin = {
|
|
85
102
|
meta: {
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
const no_anonymous_param_type_1 = __importDefault(require("./rules/no-anonymous-param-type"));
|
|
6
6
|
const max_class_dependencies_1 = __importDefault(require("./rules/max-class-dependencies"));
|
|
7
|
+
const max_method_lines_1 = __importDefault(require("./rules/max-method-lines"));
|
|
7
8
|
const max_class_fields_1 = __importDefault(require("./rules/max-class-fields"));
|
|
8
9
|
const max_returns_1 = __importDefault(require("./rules/max-returns"));
|
|
9
10
|
const max_class_methods_1 = __importDefault(require("./rules/max-class-methods"));
|
|
@@ -15,7 +16,9 @@ const no_instanceof_1 = __importDefault(require("./rules/no-instanceof"));
|
|
|
15
16
|
const no_interpolated_log_message_1 = __importDefault(require("./rules/no-interpolated-log-message"));
|
|
16
17
|
const no_logic_in_constructor_1 = __importDefault(require("./rules/no-logic-in-constructor"));
|
|
17
18
|
const no_null_1 = __importDefault(require("./rules/no-null"));
|
|
19
|
+
const no_generic_error_1 = __importDefault(require("./rules/no-generic-error"));
|
|
18
20
|
const no_null_return_1 = __importDefault(require("./rules/no-null-return"));
|
|
21
|
+
const no_self_mutation_1 = __importDefault(require("./rules/no-self-mutation"));
|
|
19
22
|
const no_property_alias_1 = __importDefault(require("./rules/no-property-alias"));
|
|
20
23
|
const no_property_destructuring_1 = __importDefault(require("./rules/no-property-destructuring"));
|
|
21
24
|
const no_public_mutable_props_1 = __importDefault(require("./rules/no-public-mutable-props"));
|
|
@@ -46,6 +49,9 @@ const rules = {
|
|
|
46
49
|
'no-property-alias': no_property_alias_1.default,
|
|
47
50
|
'no-property-destructuring': no_property_destructuring_1.default,
|
|
48
51
|
'no-anonymous-param-type': no_anonymous_param_type_1.default,
|
|
52
|
+
'no-self-mutation': no_self_mutation_1.default,
|
|
53
|
+
'no-generic-error': no_generic_error_1.default,
|
|
54
|
+
'max-method-lines': max_method_lines_1.default,
|
|
49
55
|
};
|
|
50
56
|
const plugin = {
|
|
51
57
|
meta: { name, version },
|
|
@@ -75,6 +81,9 @@ plugin.configs.recommended = {
|
|
|
75
81
|
'elegant/no-property-alias': 'error',
|
|
76
82
|
'elegant/no-property-destructuring': 'error',
|
|
77
83
|
'elegant/no-anonymous-param-type': ['error', { minMembers: 2 }],
|
|
84
|
+
'elegant/no-self-mutation': 'error',
|
|
85
|
+
'elegant/no-generic-error': 'error',
|
|
86
|
+
'elegant/max-method-lines': ['warn', { max: 50 }],
|
|
78
87
|
'max-params': ['warn', { max: 3 }],
|
|
79
88
|
'no-else-return': ['error', { allowElseIf: false }],
|
|
80
89
|
},
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
4
|
+
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
const DEFAULT_MAX = 50;
|
|
6
|
+
exports.default = (0, createRule_1.createRule)({
|
|
7
|
+
name: 'max-method-lines',
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Enforce a maximum length, in lines, for a named function or method. An inline callback is measured through the function hosting it.',
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
tooManyLines: "'{{name}}' spans {{count}} lines (max {{max}}). A body that long is holding several ideas; give each one a name.",
|
|
15
|
+
},
|
|
16
|
+
schema: [
|
|
17
|
+
{
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: { max: { type: 'integer', minimum: 1 } },
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
defaultOptions: [{ max: DEFAULT_MAX }],
|
|
25
|
+
create(context, [{ max }]) {
|
|
26
|
+
const measure = (node, name, key) => {
|
|
27
|
+
const count = node.loc.end.line - node.loc.start.line + 1;
|
|
28
|
+
if (count <= max) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
context.report({
|
|
32
|
+
node: key,
|
|
33
|
+
messageId: 'tooManyLines',
|
|
34
|
+
data: { name, count, max },
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
MethodDefinition(node) {
|
|
39
|
+
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
measure(node, node.key.name, node.key);
|
|
43
|
+
},
|
|
44
|
+
FunctionDeclaration(node) {
|
|
45
|
+
if (node.id === null) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
measure(node, node.id.name, node.id);
|
|
49
|
+
},
|
|
50
|
+
VariableDeclarator(node) {
|
|
51
|
+
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
52
|
+
node.init === null ||
|
|
53
|
+
(node.init.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
54
|
+
node.init.type !== utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
measure(node.init, node.id.name, node.id);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
4
|
+
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
/**
|
|
6
|
+
* The error types the language ships. Each says only "something went wrong",
|
|
7
|
+
* so every caller that wants to distinguish failures ends up matching on the
|
|
8
|
+
* message — a string the thrower is free to reword.
|
|
9
|
+
*/
|
|
10
|
+
const BUILT_IN_ERRORS = new Set([
|
|
11
|
+
'Error',
|
|
12
|
+
'EvalError',
|
|
13
|
+
'RangeError',
|
|
14
|
+
'ReferenceError',
|
|
15
|
+
'SyntaxError',
|
|
16
|
+
'TypeError',
|
|
17
|
+
'URIError',
|
|
18
|
+
'AggregateError',
|
|
19
|
+
]);
|
|
20
|
+
exports.default = (0, createRule_1.createRule)({
|
|
21
|
+
name: 'no-generic-error',
|
|
22
|
+
meta: {
|
|
23
|
+
type: 'suggestion',
|
|
24
|
+
docs: {
|
|
25
|
+
description: 'Disallow throwing the built-in error types. A named exception carries the failure in its type, where a caller can catch exactly it.',
|
|
26
|
+
},
|
|
27
|
+
messages: {
|
|
28
|
+
genericError: "Throwing '{{name}}' leaves the failure describable only by its message. Throw a named exception the caller can catch by type.",
|
|
29
|
+
},
|
|
30
|
+
schema: [],
|
|
31
|
+
},
|
|
32
|
+
defaultOptions: [],
|
|
33
|
+
create(context) {
|
|
34
|
+
return {
|
|
35
|
+
ThrowStatement(node) {
|
|
36
|
+
if (node.argument.type !== utils_1.AST_NODE_TYPES.NewExpression ||
|
|
37
|
+
node.argument.callee.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
38
|
+
!BUILT_IN_ERRORS.has(node.argument.callee.name)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
context.report({
|
|
42
|
+
node,
|
|
43
|
+
messageId: 'genericError',
|
|
44
|
+
data: { name: node.argument.callee.name },
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
type Options = [{
|
|
2
|
+
parameterProperties: 'public' | 'all';
|
|
3
|
+
}];
|
|
4
|
+
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"mutableProp", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
2
5
|
name: string;
|
|
3
6
|
};
|
|
4
7
|
export default _default;
|
|
@@ -13,11 +13,14 @@ const keyName = (key) => {
|
|
|
13
13
|
return 'property';
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
16
|
-
* A constructor parameter property
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* A constructor parameter property that can be written to. At `'all'` the
|
|
17
|
+
* visibility stops mattering: a `private` collaborator nobody marked readonly
|
|
18
|
+
* can still be swapped from inside, which is the same defect one wall further
|
|
19
|
+
* in. A parameter property with no modifier at all declares no field.
|
|
19
20
|
*/
|
|
20
|
-
const
|
|
21
|
+
const isMutable = (node, scope) => !node.readonly &&
|
|
22
|
+
node.accessibility !== undefined &&
|
|
23
|
+
(scope === 'all' || node.accessibility === 'public');
|
|
21
24
|
exports.default = (0, createRule_1.createRule)({
|
|
22
25
|
name: 'no-public-mutable-props',
|
|
23
26
|
meta: {
|
|
@@ -26,12 +29,20 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
26
29
|
description: 'Disallow public mutable class properties. Public state should be readonly to protect invariants and preserve encapsulation.',
|
|
27
30
|
},
|
|
28
31
|
messages: {
|
|
29
|
-
mutableProp: "
|
|
32
|
+
mutableProp: "Property '{{name}}' is mutable. Make it readonly or expose it through a method that protects the invariant.",
|
|
30
33
|
},
|
|
31
|
-
schema: [
|
|
34
|
+
schema: [
|
|
35
|
+
{
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
parameterProperties: { type: 'string', enum: ['public', 'all'] },
|
|
39
|
+
},
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
32
43
|
},
|
|
33
|
-
defaultOptions: [],
|
|
34
|
-
create(context) {
|
|
44
|
+
defaultOptions: [{ parameterProperties: 'all' }],
|
|
45
|
+
create(context, [{ parameterProperties }]) {
|
|
35
46
|
return {
|
|
36
47
|
PropertyDefinition(node) {
|
|
37
48
|
if (node.readonly || isHidden(node.accessibility)) {
|
|
@@ -44,7 +55,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
44
55
|
});
|
|
45
56
|
},
|
|
46
57
|
TSParameterProperty(node) {
|
|
47
|
-
if (!
|
|
58
|
+
if (!isMutable(node, parameterProperties)) {
|
|
48
59
|
return;
|
|
49
60
|
}
|
|
50
61
|
const target = node.parameter.type === utils_1.AST_NODE_TYPES.AssignmentPattern
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type Options = [{
|
|
2
|
+
allowedMethods: string[];
|
|
3
|
+
}];
|
|
4
|
+
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"mutatesSelf", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
4
|
+
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
/**
|
|
6
|
+
* Nest calls these after the container has built the instance, so they finish
|
|
7
|
+
* a construction the constructor could not: a timer needs a running event
|
|
8
|
+
* loop, a pool needs a connection. Teardown is the same seam in reverse.
|
|
9
|
+
*/
|
|
10
|
+
const LIFECYCLE_HOOKS = [
|
|
11
|
+
'onModuleInit',
|
|
12
|
+
'onApplicationBootstrap',
|
|
13
|
+
'onModuleDestroy',
|
|
14
|
+
'beforeApplicationShutdown',
|
|
15
|
+
'onApplicationShutdown',
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* The field a write targets, when the target is a field of `this`. A computed
|
|
19
|
+
* write (`this[key] = value`) names no field and is left to other rules.
|
|
20
|
+
*/
|
|
21
|
+
const fieldOf = (target) => target.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
22
|
+
!target.computed &&
|
|
23
|
+
target.object.type === utils_1.AST_NODE_TYPES.ThisExpression &&
|
|
24
|
+
target.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
25
|
+
? target.property.name
|
|
26
|
+
: undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Whether the write happens while the object is still being built. Only the
|
|
29
|
+
* constructor's own body counts: a callback the constructor schedules runs
|
|
30
|
+
* after construction has returned, so it mutates a finished object.
|
|
31
|
+
*/
|
|
32
|
+
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;
|
|
44
|
+
};
|
|
45
|
+
/** The method a write sits in, if it sits in one directly. */
|
|
46
|
+
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;
|
|
57
|
+
};
|
|
58
|
+
exports.default = (0, createRule_1.createRule)({
|
|
59
|
+
name: 'no-self-mutation',
|
|
60
|
+
meta: {
|
|
61
|
+
type: 'suggestion',
|
|
62
|
+
docs: {
|
|
63
|
+
description: 'Disallow writing to your own fields outside the constructor. An object that changes after construction is holding a lifecycle, not modelling a value.',
|
|
64
|
+
},
|
|
65
|
+
messages: {
|
|
66
|
+
mutatesSelf: "'this.{{name}}' is written after construction. Build a new object instead of letting this one change underneath its holders.",
|
|
67
|
+
},
|
|
68
|
+
schema: [
|
|
69
|
+
{
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
allowedMethods: { type: 'array', items: { type: 'string' } },
|
|
73
|
+
},
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
defaultOptions: [{ allowedMethods: LIFECYCLE_HOOKS }],
|
|
79
|
+
create(context, [{ allowedMethods }]) {
|
|
80
|
+
const check = (node, target) => {
|
|
81
|
+
const name = fieldOf(target);
|
|
82
|
+
if (name === undefined || isDuringConstruction(node)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const method = enclosingMethod(node);
|
|
86
|
+
if (method !== undefined && allowedMethods.includes(method)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
context.report({ node, messageId: 'mutatesSelf', data: { name } });
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
AssignmentExpression(node) {
|
|
93
|
+
check(node, node.left);
|
|
94
|
+
},
|
|
95
|
+
UpdateExpression(node) {
|
|
96
|
+
check(node, node.argument);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
type
|
|
1
|
+
type Allowances = {
|
|
2
2
|
allowReadonly: boolean;
|
|
3
|
-
|
|
3
|
+
allowSelfReturning: boolean;
|
|
4
|
+
allowModuleFactories: boolean;
|
|
5
|
+
};
|
|
6
|
+
type Options = [Allowances];
|
|
4
7
|
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"staticMember", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
5
8
|
name: string;
|
|
6
9
|
};
|
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
3
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
/**
|
|
6
|
+
* Whether the annotation denotes the class itself. TypeScript has no secondary
|
|
7
|
+
* constructors, so `static of(...): DueDate` inside `DueDate` is the only way
|
|
8
|
+
* to write one — and calling it is indistinguishable from calling `new`, which
|
|
9
|
+
* is what separates a named constructor from a procedure that moved into a
|
|
10
|
+
* class.
|
|
11
|
+
*
|
|
12
|
+
* `this`, `Promise<Self>` and `Self | undefined` all count: a polymorphic, an
|
|
13
|
+
* asynchronous and a failing constructor are still constructors. `Self | null`
|
|
14
|
+
* deliberately does not, because `no-null-return` already owns that shape.
|
|
15
|
+
*/
|
|
16
|
+
const isNamed = (node, name) => node.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
17
|
+
node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
18
|
+
node.typeName.name === name;
|
|
19
|
+
/** What a `Promise<T>` resolves to, or the node itself. */
|
|
20
|
+
const awaited = (node) => isNamed(node, 'Promise') && node.type === utils_1.AST_NODE_TYPES.TSTypeReference
|
|
21
|
+
? (node.typeArguments?.params[0] ?? node)
|
|
22
|
+
: node;
|
|
23
|
+
const isSelf = (node, className) => {
|
|
24
|
+
const resolved = awaited(node);
|
|
25
|
+
if (resolved.type === utils_1.AST_NODE_TYPES.TSThisType) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (resolved.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
29
|
+
return (resolved.types.some((member) => isSelf(member, className)) &&
|
|
30
|
+
resolved.types.every((member) => isSelf(member, className) ||
|
|
31
|
+
member.type === utils_1.AST_NODE_TYPES.TSUndefinedKeyword));
|
|
32
|
+
}
|
|
33
|
+
return isNamed(resolved, className);
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A Nest module factory: `DynamicModule` returned from a class Nest recognises
|
|
37
|
+
* as a module. Both halves are required, so `DynamicModule` cannot become a
|
|
38
|
+
* general escape from the rule by being named in a return type.
|
|
39
|
+
*/
|
|
40
|
+
const isModuleFactory = (method) => {
|
|
41
|
+
const annotation = method.value.returnType?.typeAnnotation;
|
|
42
|
+
if (annotation === undefined || !isNamed(awaited(annotation), 'DynamicModule')) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return method.parent.parent.decorators.some((decorator) => decorator.expression.type === utils_1.AST_NODE_TYPES.CallExpression
|
|
46
|
+
? isModuleIdentifier(decorator.expression.callee)
|
|
47
|
+
: isModuleIdentifier(decorator.expression));
|
|
48
|
+
};
|
|
49
|
+
const isModuleIdentifier = (node) => node.type === utils_1.AST_NODE_TYPES.Identifier && node.name === 'Module';
|
|
50
|
+
/**
|
|
51
|
+
* A secondary constructor's return, read from the annotation alone. The rule
|
|
52
|
+
* carries no type information, so an unannotated static stays reported: on a
|
|
53
|
+
* factory the annotation is one word, and it is what makes the intent legible.
|
|
54
|
+
*/
|
|
55
|
+
const returnsSelf = (method, className) => {
|
|
56
|
+
const annotation = method.value.returnType?.typeAnnotation;
|
|
57
|
+
return (className !== undefined &&
|
|
58
|
+
annotation !== undefined &&
|
|
59
|
+
isSelf(annotation, className));
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* A static the rule lets through: a secondary constructor, or a module factory
|
|
63
|
+
* the framework demands. An accessor is neither, whatever it returns — reading
|
|
64
|
+
* one is reaching for static state.
|
|
65
|
+
*/
|
|
66
|
+
const isPermitted = (node, allow) => node.kind === 'method' &&
|
|
67
|
+
((allow.allowSelfReturning &&
|
|
68
|
+
returnsSelf(node, node.parent.parent.id?.name)) ||
|
|
69
|
+
(allow.allowModuleFactories && isModuleFactory(node)));
|
|
4
70
|
exports.default = (0, createRule_1.createRule)({
|
|
5
71
|
name: 'no-static-members',
|
|
6
72
|
meta: {
|
|
@@ -14,24 +80,34 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
14
80
|
schema: [
|
|
15
81
|
{
|
|
16
82
|
type: 'object',
|
|
17
|
-
properties: {
|
|
83
|
+
properties: {
|
|
84
|
+
allowReadonly: { type: 'boolean' },
|
|
85
|
+
allowSelfReturning: { type: 'boolean' },
|
|
86
|
+
allowModuleFactories: { type: 'boolean' },
|
|
87
|
+
},
|
|
18
88
|
additionalProperties: false,
|
|
19
89
|
},
|
|
20
90
|
],
|
|
21
91
|
},
|
|
22
|
-
defaultOptions: [
|
|
23
|
-
|
|
92
|
+
defaultOptions: [
|
|
93
|
+
{
|
|
94
|
+
allowReadonly: false,
|
|
95
|
+
allowSelfReturning: true,
|
|
96
|
+
allowModuleFactories: true,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
create(context, [allow]) {
|
|
24
100
|
const reportKey = (key) => {
|
|
25
101
|
context.report({ node: key, messageId: 'staticMember' });
|
|
26
102
|
};
|
|
27
103
|
return {
|
|
28
104
|
MethodDefinition(node) {
|
|
29
|
-
if (node.static) {
|
|
105
|
+
if (node.static && !isPermitted(node, allow)) {
|
|
30
106
|
reportKey(node.key);
|
|
31
107
|
}
|
|
32
108
|
},
|
|
33
109
|
PropertyDefinition(node) {
|
|
34
|
-
if (node.static && !(allowReadonly && node.readonly)) {
|
|
110
|
+
if (node.static && !(allow.allowReadonly && node.readonly)) {
|
|
35
111
|
reportKey(node.key);
|
|
36
112
|
}
|
|
37
113
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tianjos/eslint-plugin-elegant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
"lint": "npm run build && eslint .",
|
|
45
45
|
"typecheck": "tsc -p tsconfig.test.json",
|
|
46
46
|
"test": "jest",
|
|
47
|
-
"release": "standard-version",
|
|
47
|
+
"release": "standard-version --release-as minor",
|
|
48
|
+
"release:patch": "standard-version --release-as patch",
|
|
48
49
|
"prepublishOnly": "npm run build && npm test"
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|