@tianjos/eslint-plugin-elegant 0.7.0 → 0.7.2
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
|
@@ -189,12 +189,36 @@ Public state should be `readonly` so callers cannot break an aggregate's
|
|
|
189
189
|
invariants. A declared `private`/`protected` field is allowed, and so is any
|
|
190
190
|
`readonly` member.
|
|
191
191
|
|
|
192
|
+
A **decorated** property is allowed by default. `@Column`, `@IsString` and
|
|
193
|
+
friends assign the field from outside the class, so `readonly` would be a lie
|
|
194
|
+
and the property is framework shape rather than state the class chose to carry
|
|
195
|
+
— a NestJS DTO or a TypeORM entity is a wall of them. Pass
|
|
196
|
+
`{ ignoreDecorated: false }` to hold them to the same standard.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
// passes — the framework populates these
|
|
200
|
+
class BankDto {
|
|
201
|
+
@IsString() code: string;
|
|
202
|
+
@IsOptional() ispb_code?: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// reported — state the class chose to expose
|
|
206
|
+
class Money {
|
|
207
|
+
amount = 0;
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
192
211
|
Constructor parameter properties are covered at **every** visibility by
|
|
193
212
|
default, which declared fields are not: a `private repo: Repository<Proposal>`
|
|
194
213
|
that nobody marked `readonly` can still be swapped from inside, and unlike a
|
|
195
214
|
declared field it is a collaborator the container handed you. Set
|
|
196
215
|
`{ parameterProperties: 'public' }` to keep the rule to what its name says.
|
|
197
216
|
|
|
217
|
+
`ignoreDecorated` deliberately does not reach parameter properties either. A
|
|
218
|
+
decorator on a parameter is injection (`@Inject(TOKEN)`), which supplies a
|
|
219
|
+
collaborator rather than populating a field, so it still has no business being
|
|
220
|
+
reassignable. `max-class-fields` draws the same line for the same reason.
|
|
221
|
+
|
|
198
222
|
This rule asks whether a field is *declared* changeable. Its behavioural
|
|
199
223
|
counterpart is `no-self-mutation`, which asks whether anything actually
|
|
200
224
|
changes it.
|
|
@@ -659,6 +683,74 @@ rules: {
|
|
|
659
683
|
}
|
|
660
684
|
```
|
|
661
685
|
|
|
686
|
+
### Adopting on an existing codebase
|
|
687
|
+
|
|
688
|
+
The `recommended` config is written for the code you wish you had. Turning it
|
|
689
|
+
on over code that already exists is a different exercise, and worth planning
|
|
690
|
+
with numbers rather than discovering at the first `eslint .`.
|
|
691
|
+
|
|
692
|
+
Measured over **1,261 production TypeScript files** across three NestJS
|
|
693
|
+
services — a DTO-heavy, string-logging, TypeORM-backed shape this preset was
|
|
694
|
+
built for:
|
|
695
|
+
|
|
696
|
+
| Rule | Severity | Reports | Per file | In test files |
|
|
697
|
+
| --- | --- | ---: | ---: | ---: |
|
|
698
|
+
| `no-comments-in-function-body` | `error` | 5,559 | 4.41 | 2,589 |
|
|
699
|
+
| `no-interpolated-log-message` | `error` | 1,941 | 1.54 | 0 |
|
|
700
|
+
| `no-null` | `error` | 1,367 | 1.08 | 896 |
|
|
701
|
+
| `no-type-assertion` | `error` | 524 | 0.42 | 935 |
|
|
702
|
+
| `max-method-lines` | `warn` | 467 | 0.37 | 1 |
|
|
703
|
+
| `max-params` | `warn` | 195 | 0.15 | 6 |
|
|
704
|
+
| `no-null-return` | `error` | 194 | 0.15 | 2 |
|
|
705
|
+
| `no-instanceof` | `error` | 179 | 0.14 | 0 |
|
|
706
|
+
| `no-generic-error` | `error` | 163 | 0.13 | 17 |
|
|
707
|
+
| `max-returns` | `warn` | 125 | 0.10 | 0 |
|
|
708
|
+
| `no-public-mutable-props` | `error` | 116 | 0.09 | 0 |
|
|
709
|
+
| `no-property-alias` | `error` | 111 | 0.09 | 1 |
|
|
710
|
+
| `no-logic-in-constructor` | `error` | 98 | 0.08 | 0 |
|
|
711
|
+
| `no-static-members` | `error` | 96 | 0.08 | 0 |
|
|
712
|
+
| `no-anonymous-param-type` | `error` | 91 | 0.07 | 27 |
|
|
713
|
+
| `no-self-mutation` | `error` | 66 | 0.05 | 0 |
|
|
714
|
+
| `max-class-dependencies` | `warn` | 64 | 0.05 | 0 |
|
|
715
|
+
| `max-class-fields` | `warn` | 57 | 0.05 | 0 |
|
|
716
|
+
| `no-property-destructuring` | `error` | 53 | 0.04 | 0 |
|
|
717
|
+
| `no-boolean-param` | `error` | 47 | 0.04 | 1 |
|
|
718
|
+
| `max-class-methods` | `warn` | 40 | 0.03 | 0 |
|
|
719
|
+
| `no-getters-setters` | `error` | 29 | 0.02 | 0 |
|
|
720
|
+
| `no-else-return` | `error` | 13 | 0.01 | 0 |
|
|
721
|
+
| `no-else-after-throw` | `error` | 8 | 0.01 | 0 |
|
|
722
|
+
| **Total** | | **11,603** | **9.20** | |
|
|
723
|
+
|
|
724
|
+
Four rules account for 81% of it, and they are the four whose principle has a
|
|
725
|
+
boundary this plugin cannot see. `no-comments-in-function-body` asks you to
|
|
726
|
+
rewrite a function, not edit a line. `no-interpolated-log-message` fires on
|
|
727
|
+
whatever logging convention the project already chose, so on a codebase that
|
|
728
|
+
logs with template strings it fires everywhere. `no-null` cannot tell a domain
|
|
729
|
+
value from the wire format of a database column. `no-type-assertion` counts
|
|
730
|
+
`x as unknown as T` twice, once per assertion, which is arguably correct.
|
|
731
|
+
|
|
732
|
+
None of that makes them wrong — it makes them rules you adopt on purpose
|
|
733
|
+
rather than inherit. **Enable the preset, then take the top of that table back
|
|
734
|
+
to `warn` or `off` and work down it.** The rules below `no-type-assertion` sum
|
|
735
|
+
to 1.75 per file, which is a starting point you can actually clear:
|
|
736
|
+
|
|
737
|
+
```js
|
|
738
|
+
rules: {
|
|
739
|
+
...elegant.configs.recommended.rules,
|
|
740
|
+
|
|
741
|
+
// The four that need a plan of their own. Re-enable one at a time.
|
|
742
|
+
'elegant/no-comments-in-function-body': 'off',
|
|
743
|
+
'elegant/no-interpolated-log-message': 'warn',
|
|
744
|
+
'elegant/no-null': 'warn',
|
|
745
|
+
'elegant/no-type-assertion': 'warn',
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
Numbers from one corpus are indicative, not universal. Run
|
|
750
|
+
`npx eslint . --format json` on your own and sort by rule before deciding
|
|
751
|
+
anything — the shape of your code decides which of these rules is a signal and
|
|
752
|
+
which is a migration.
|
|
753
|
+
|
|
662
754
|
### Relaxing rules in test files
|
|
663
755
|
|
|
664
756
|
Tests routinely use flag arguments and larger fixtures. Add a second config
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
type Options = [
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
type Options = [
|
|
2
|
+
{
|
|
3
|
+
parameterProperties: 'public' | 'all';
|
|
4
|
+
ignoreDecorated: boolean;
|
|
5
|
+
}
|
|
6
|
+
];
|
|
4
7
|
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"mutableProp", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
5
8
|
name: string;
|
|
6
9
|
};
|
|
@@ -21,6 +21,17 @@ const keyName = (key) => {
|
|
|
21
21
|
const isMutable = (node, scope) => !node.readonly &&
|
|
22
22
|
node.accessibility !== undefined &&
|
|
23
23
|
(scope === 'all' || node.accessibility === 'public');
|
|
24
|
+
/**
|
|
25
|
+
* A property a decorator maps to a table or a payload. `@Column`, `@IsString`
|
|
26
|
+
* and friends assign it from outside the class, so `readonly` would be a lie
|
|
27
|
+
* and the field is framework shape rather than state the class chose to carry.
|
|
28
|
+
*
|
|
29
|
+
* Deliberately does not reach parameter properties: a decorator on a parameter
|
|
30
|
+
* is injection (`@Inject(TOKEN)`), so the field is a genuine collaborator and
|
|
31
|
+
* still has no business being reassignable. `max-class-fields` draws the same
|
|
32
|
+
* line for the same reason.
|
|
33
|
+
*/
|
|
34
|
+
const isMapped = (node) => node.decorators.length > 0;
|
|
24
35
|
exports.default = (0, createRule_1.createRule)({
|
|
25
36
|
name: 'no-public-mutable-props',
|
|
26
37
|
meta: {
|
|
@@ -36,18 +47,22 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
36
47
|
type: 'object',
|
|
37
48
|
properties: {
|
|
38
49
|
parameterProperties: { type: 'string', enum: ['public', 'all'] },
|
|
50
|
+
ignoreDecorated: { type: 'boolean' },
|
|
39
51
|
},
|
|
40
52
|
additionalProperties: false,
|
|
41
53
|
},
|
|
42
54
|
],
|
|
43
55
|
},
|
|
44
|
-
defaultOptions: [{ parameterProperties: 'all' }],
|
|
45
|
-
create(context, [{ parameterProperties }]) {
|
|
56
|
+
defaultOptions: [{ parameterProperties: 'all', ignoreDecorated: true }],
|
|
57
|
+
create(context, [{ parameterProperties, ignoreDecorated }]) {
|
|
46
58
|
return {
|
|
47
59
|
PropertyDefinition(node) {
|
|
48
60
|
if (node.readonly || isHidden(node.accessibility)) {
|
|
49
61
|
return;
|
|
50
62
|
}
|
|
63
|
+
if (ignoreDecorated && isMapped(node)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
51
66
|
context.report({
|
|
52
67
|
node: node.key,
|
|
53
68
|
messageId: 'mutableProp',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tianjos/eslint-plugin-elegant",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
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",
|