@tianjos/eslint-plugin-elegant 0.6.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 +89 -1
- package/dist/index.d.ts +16 -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/package.json +1 -1
package/README.md
CHANGED
|
@@ -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
|
|
|
@@ -554,6 +568,80 @@ Unlike its neighbours, this rule does not have a mechanical fix: it asks you to
|
|
|
554
568
|
introduce a named type and decide where it lives. That is a design change, so
|
|
555
569
|
expect adoption to cost more than a find-and-replace.
|
|
556
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
|
+
|
|
557
645
|
## Configuration
|
|
558
646
|
|
|
559
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> & {
|
|
@@ -82,6 +84,19 @@ declare const rules: {
|
|
|
82
84
|
}], unknown, TSESLint.RuleListener> & {
|
|
83
85
|
name: string;
|
|
84
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
|
+
};
|
|
85
100
|
};
|
|
86
101
|
type Plugin = {
|
|
87
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
|
+
});
|
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",
|