@tianjos/eslint-plugin-elegant 0.3.2 → 0.4.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 +240 -3
- package/dist/index.d.ts +31 -0
- package/dist/index.js +19 -0
- package/dist/rules/max-class-dependencies.d.ts +9 -0
- package/dist/rules/max-class-dependencies.js +112 -0
- package/dist/rules/max-class-fields.d.ts +9 -0
- package/dist/rules/max-class-fields.js +81 -0
- package/dist/rules/max-returns.d.ts +7 -0
- package/dist/rules/max-returns.js +88 -0
- package/dist/rules/no-comments-in-function-body.d.ts +7 -0
- package/dist/rules/no-comments-in-function-body.js +85 -0
- package/dist/rules/no-else-after-throw.d.ts +4 -0
- package/dist/rules/no-else-after-throw.js +48 -0
- package/dist/rules/no-interpolated-log-message.d.ts +8 -0
- package/dist/rules/no-interpolated-log-message.js +88 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -68,13 +68,16 @@ A complete, copy-pasteable example (including test-file overrides) lives in
|
|
|
68
68
|
|
|
69
69
|
## Rules
|
|
70
70
|
|
|
71
|
-
The `recommended` config enables every custom rule plus
|
|
72
|
-
[`max-params`](https://eslint.org/docs/latest/rules/max-params)
|
|
71
|
+
The `recommended` config enables every custom rule plus two native ones,
|
|
72
|
+
[`max-params`](https://eslint.org/docs/latest/rules/max-params) and
|
|
73
|
+
[`no-else-return`](https://eslint.org/docs/latest/rules/no-else-return).
|
|
73
74
|
|
|
74
75
|
| Rule | Source | What it catches | `recommended` |
|
|
75
76
|
| -------------------------------------- | ------ | ------------------------------------------------------------------------------- | ------------- |
|
|
76
77
|
| `elegant/no-boolean-param` | custom | Boolean parameters (flag arguments) on functions, methods, and constructors | `error` |
|
|
77
78
|
| `elegant/max-class-methods` | custom | Classes with more methods than the configured `max` (constructors excluded) | `warn` (max 10) |
|
|
79
|
+
| `elegant/max-class-dependencies` | custom | Classes depending on more distinct collaborators than `max` (constructor injections plus `new`) | `warn` (max 4) |
|
|
80
|
+
| `elegant/max-class-fields` | custom | Classes holding more instance fields than `max` (declared fields plus parameter properties) | `warn` (max 5) |
|
|
78
81
|
| `elegant/no-type-assertion` | custom | `value as T` and `<T>value` assertions (`as const` is allowed) | `error` |
|
|
79
82
|
| `elegant/no-null-return` | custom | `return null` statements | `error` |
|
|
80
83
|
| `elegant/no-public-mutable-props` | custom | Public, non-`readonly` class properties and public constructor parameter props | `error` |
|
|
@@ -83,7 +86,12 @@ The `recommended` config enables every custom rule plus the native
|
|
|
83
86
|
| `elegant/no-instanceof` | custom | Use of the `instanceof` operator | `error` |
|
|
84
87
|
| `elegant/no-static-members` | custom | Static methods, properties, accessors, and blocks (`allowReadonly` to permit constants) | `error` |
|
|
85
88
|
| `elegant/no-null` | custom | The `null` literal as a value (type annotations and direct `return null` excepted) | `error` |
|
|
89
|
+
| `elegant/no-comments-in-function-body` | custom | Comments inside function bodies (directives and empty blocks excepted) | `error` |
|
|
90
|
+
| `elegant/no-else-after-throw` | custom | An `else` branch when the `then` branch always throws | `error` |
|
|
91
|
+
| `elegant/no-interpolated-log-message` | custom | Log messages built by interpolation or concatenation | `error` |
|
|
92
|
+
| `elegant/max-returns` | custom | Functions returning from more places than `max` | `warn` (max 3) |
|
|
86
93
|
| `max-params` | native | Functions declaring more than `max` parameters | `warn` (max 3) |
|
|
94
|
+
| `no-else-return` | native | An `else` branch when the `then` branch always returns (`allowElseIf: false`) | `error` |
|
|
87
95
|
|
|
88
96
|
### Rule details
|
|
89
97
|
|
|
@@ -98,6 +106,66 @@ intention-revealing functions or an options object. Flags both annotated
|
|
|
98
106
|
A proxy for the Single Responsibility Principle. Constructors are not counted;
|
|
99
107
|
getters and setters are. Configurable via `{ max: number }` (default `10`).
|
|
100
108
|
|
|
109
|
+
#### `max-class-dependencies`
|
|
110
|
+
|
|
111
|
+
The coupling counterpart to `max-class-methods`: a class that needs six
|
|
112
|
+
collaborators to do its job is coordinating, not modelling. Counts the distinct
|
|
113
|
+
types annotated on constructor parameters plus every type instantiated with
|
|
114
|
+
`new` inside the class body — so a dependency hidden behind `new HttpClient()`
|
|
115
|
+
weighs the same as an injected one.
|
|
116
|
+
|
|
117
|
+
De-duplication keeps type arguments, so `Repository<Order>` and
|
|
118
|
+
`Repository<Customer>` count as two collaborators while the same `Clock`
|
|
119
|
+
injected twice counts as one. Nested classes are budgeted independently of their
|
|
120
|
+
host.
|
|
121
|
+
|
|
122
|
+
Three things never count: primitives and inline types (they are not type
|
|
123
|
+
references), a default list of ambient built-ins (`Date`, `Map`, `Set`,
|
|
124
|
+
`Promise`, `Error`, `Array`, `RegExp`, `URL`, `WeakMap`, `WeakSet`), and
|
|
125
|
+
exceptions raised with `throw new ...`. That last exclusion is what makes the
|
|
126
|
+
rule usable in NestJS, where `throw new NotFoundException()` is routine and says
|
|
127
|
+
nothing about a class's design.
|
|
128
|
+
|
|
129
|
+
Configurable via `{ max: number, ignore: string[] }` (default `max: 4`).
|
|
130
|
+
`ignore` adds to the built-in list — reach for it when an ambient concern such
|
|
131
|
+
as `Logger` or `ConfigService` is in every constructor and you would rather not
|
|
132
|
+
budget for it:
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
'elegant/max-class-dependencies': ['warn', { max: 4, ignore: ['Logger'] }],
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### `max-class-fields`
|
|
139
|
+
|
|
140
|
+
The third axis of class size, after methods and collaborators: a class carrying
|
|
141
|
+
a dozen fields is a record with a namespace, not a model. Counts instance fields
|
|
142
|
+
declared in the body — plain, `abstract`, or `accessor` — plus every constructor
|
|
143
|
+
parameter property. Methods and accessors belong to `max-class-methods`, and
|
|
144
|
+
`static` members to `no-static-members`, so neither is counted here.
|
|
145
|
+
|
|
146
|
+
Decorated properties are skipped by default. `@Column`, `@IsString` and
|
|
147
|
+
`@ApiProperty` map a field to a table or a payload, so a DTO or an ORM entity
|
|
148
|
+
declares one field per column by design and has no business inside a budget:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
class CreateOrderDto {
|
|
152
|
+
@IsString() customerId: string; // not counted
|
|
153
|
+
@IsInt() quantity: number; // not counted
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**The exemption stops at the constructor.** A decorator on a parameter is
|
|
158
|
+
injection, not mapping, so `@Inject(TOKEN) private readonly repo: Repo` stays
|
|
159
|
+
inside the budget — otherwise a service wired entirely through tokens would
|
|
160
|
+
count zero fields, which is exactly the class the rule exists to catch. Set
|
|
161
|
+
`{ ignoreDecorated: false }` to budget mapped properties too.
|
|
162
|
+
|
|
163
|
+
Configurable via `{ max: number, ignoreDecorated: boolean }` (default `max: 5`,
|
|
164
|
+
`ignoreDecorated: true`). The default leaves room for the four collaborators
|
|
165
|
+
`max-class-dependencies` allows plus one field of genuine state; past that the
|
|
166
|
+
two rules deliberately overlap, because a class over both budgets is over-sized
|
|
167
|
+
on both axes.
|
|
168
|
+
|
|
101
169
|
#### `no-type-assertion`
|
|
102
170
|
|
|
103
171
|
Assertions silence the type checker. Reach for a type guard, a generic, or a
|
|
@@ -153,16 +221,169 @@ or `undefined`. `null` in type positions (`string | null`) and a direct
|
|
|
153
221
|
flag idioms like `JSON.stringify(x, null, 2)` — relax it in the files where you
|
|
154
222
|
interoperate with null-based APIs.
|
|
155
223
|
|
|
224
|
+
#### `no-comments-in-function-body`
|
|
225
|
+
|
|
226
|
+
A comment inside a body is usually a name that never got written: it labels a
|
|
227
|
+
run of statements that wanted to be its own function. Move the explanation to a
|
|
228
|
+
docblock above the function, or extract what it describes and let the call read
|
|
229
|
+
as the sentence the comment was trying to be.
|
|
230
|
+
|
|
231
|
+
Applies to every function with a block body — methods, constructors, function
|
|
232
|
+
declarations, and arrow functions. Comments outside a body are untouched, so
|
|
233
|
+
docblocks, module-level notes, and comments between class members are fine. Each
|
|
234
|
+
comment is attributed to the innermost function containing it, so one in a
|
|
235
|
+
nested arrow is reported once, against that arrow.
|
|
236
|
+
|
|
237
|
+
Two things are never reported:
|
|
238
|
+
|
|
239
|
+
- **Directives**, which the toolchain reads rather than a human:
|
|
240
|
+
`eslint-disable*`, `@ts-expect-error`, `@ts-ignore`, `prettier-ignore`,
|
|
241
|
+
`istanbul ignore`, `c8 ignore`, `v8 ignore`, `webpackChunkName`,
|
|
242
|
+
`@vite-ignore`. Extend the list with `{ allow: string[] }` for project
|
|
243
|
+
conventions such as `@codegen`.
|
|
244
|
+
- **Comments alone in an empty block**, where there is no code to name and the
|
|
245
|
+
comment is the only thing explaining the silence. The check looks at the
|
|
246
|
+
innermost block, not the function, so an empty `catch` keeps its note even
|
|
247
|
+
inside a busy function:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
function run(): void {
|
|
251
|
+
try {
|
|
252
|
+
go();
|
|
253
|
+
} catch {
|
|
254
|
+
// the failure is expected here
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### `no-else-after-throw`
|
|
260
|
+
|
|
261
|
+
When the `then` branch throws, control never reaches what follows, so `else`
|
|
262
|
+
carries no information and only deepens nesting. Drop it and let the alternative
|
|
263
|
+
sit at the outer level, where it reads as the normal path rather than one of two
|
|
264
|
+
symmetric cases:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
// before
|
|
268
|
+
if (amount < 0) {
|
|
269
|
+
throw new NegativeAmount(amount);
|
|
270
|
+
} else {
|
|
271
|
+
process(amount);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// after
|
|
275
|
+
if (amount < 0) {
|
|
276
|
+
throw new NegativeAmount(amount);
|
|
277
|
+
}
|
|
278
|
+
process(amount);
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
A branch counts as always throwing when it is a bare `throw` or a block whose
|
|
282
|
+
**last** statement is one. The check does not recurse, which is deliberate: a
|
|
283
|
+
block ending in a nested `if` may or may not throw, and there `else` still says
|
|
284
|
+
something.
|
|
285
|
+
|
|
286
|
+
`else if` is flagged too, since the same rewrite applies. The rule has no
|
|
287
|
+
options and no autofix — dedenting a block reliably is the formatter's job, not
|
|
288
|
+
a linter's.
|
|
289
|
+
|
|
290
|
+
Its sibling for `return` is the native
|
|
291
|
+
[`no-else-return`](https://eslint.org/docs/latest/rules/no-else-return), which
|
|
292
|
+
`recommended` enables with `allowElseIf: false` to match. Prefer the native
|
|
293
|
+
rule's defaults? Override it in one line:
|
|
294
|
+
|
|
295
|
+
```js
|
|
296
|
+
'no-else-return': 'error',
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
#### `no-interpolated-log-message`
|
|
300
|
+
|
|
301
|
+
A log message should be a constant, with everything that varies passed as
|
|
302
|
+
structured data. This is not a style preference: `` `order ${id} confirmed` ``
|
|
303
|
+
produces one distinct message per order, which no aggregator can group, and it
|
|
304
|
+
buries `id` inside prose instead of leaving it as a field you can filter on.
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
// before — N messages, and the id is not queryable
|
|
308
|
+
this.logger.log(`order ${id} confirmed for ${customer}`);
|
|
309
|
+
|
|
310
|
+
// after — one message, two fields
|
|
311
|
+
this.logger.log('order confirmed', { orderId: id, customer });
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
The rule looks only at the **message argument**, which it takes to be the first
|
|
315
|
+
argument that is not an object literal. That lands on the message under either
|
|
316
|
+
convention — `info(message, data)` as in NestJS and winston, `info(data,
|
|
317
|
+
message)` as in pino — so the rule never dictates where your data goes. An
|
|
318
|
+
interpolated *later* argument is left alone, since that position is context
|
|
319
|
+
rather than the message.
|
|
320
|
+
|
|
321
|
+
Flagged: template literals with expressions, and `+` concatenation. A plain
|
|
322
|
+
identifier passes, so `logger.info(message)` is fine — chasing that would need
|
|
323
|
+
type information, which no rule in this plugin requires.
|
|
324
|
+
|
|
325
|
+
A call counts as logging when the method is a level (`log`, `info`, `warn`,
|
|
326
|
+
`error`, `debug`, `verbose`, `trace`, `fatal`) and the receiver is named `logger`
|
|
327
|
+
or `log`, whether local or a field (`this.logger.info`). Both lists are widened
|
|
328
|
+
with `{ objects: string[], methods: string[] }`:
|
|
329
|
+
|
|
330
|
+
```js
|
|
331
|
+
'elegant/no-interpolated-log-message': ['error', { objects: ['audit'] }],
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Known limitation.** When the first argument is an identifier, it is taken for
|
|
335
|
+
the message, so pino's error form slips through:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
logger.error(err, `order ${id} failed`); // not reported
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Telling that apart from `logger.info(message)` needs type information. The case
|
|
342
|
+
is pinned by a test so the behaviour is deliberate rather than accidental.
|
|
343
|
+
|
|
344
|
+
#### `max-returns`
|
|
345
|
+
|
|
346
|
+
A port of Checkstyle's `ReturnCount`, but not of its threshold. qulice sets it
|
|
347
|
+
to `1` — a single exit — which reads well in Java and badly here, because it
|
|
348
|
+
outlaws the guard clause that `no-else-after-throw` in this very preset pushes
|
|
349
|
+
you towards. Two rules in one config should not disagree.
|
|
350
|
+
|
|
351
|
+
At `max: 3` the rule stops arguing about single exit and measures sprawl
|
|
352
|
+
instead. Two or three guards followed by a final `return` pass; a function
|
|
353
|
+
leaving from six different places is the one worth splitting.
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
// passes — idiomatic guards
|
|
357
|
+
function charge(amount: number): number {
|
|
358
|
+
if (amount < 0) return 0;
|
|
359
|
+
if (amount > limit) return limit;
|
|
360
|
+
return amount;
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Every function gets its own budget, so a callback's exits are never charged to
|
|
365
|
+
the function hosting it. Bare `return;` counts — leaving early is leaving,
|
|
366
|
+
value or not. An arrow with an expression body has no `return` statement at all
|
|
367
|
+
and never trips the rule.
|
|
368
|
+
|
|
369
|
+
Functions are reported by the name that binds them — a declaration's own, a
|
|
370
|
+
method's key, or the `const` or class field holding an arrow — falling back to
|
|
371
|
+
`(anonymous)` for an inline callback. Configurable via `{ max: number }`
|
|
372
|
+
(default `3`).
|
|
373
|
+
|
|
156
374
|
## Configuration
|
|
157
375
|
|
|
158
376
|
### Overriding thresholds
|
|
159
377
|
|
|
160
|
-
`max-class-methods
|
|
378
|
+
`max-class-methods`, `max-class-dependencies`, `max-class-fields`, and the
|
|
379
|
+
native `max-params` all take a `max` option:
|
|
161
380
|
|
|
162
381
|
```js
|
|
163
382
|
rules: {
|
|
164
383
|
...elegant.configs.recommended.rules,
|
|
165
384
|
'elegant/max-class-methods': ['warn', { max: 15 }],
|
|
385
|
+
'elegant/max-class-dependencies': ['warn', { max: 6 }],
|
|
386
|
+
'elegant/max-class-fields': ['warn', { max: 8 }],
|
|
166
387
|
'max-params': ['warn', { max: 4 }],
|
|
167
388
|
}
|
|
168
389
|
```
|
|
@@ -178,6 +399,9 @@ block scoped to your spec globs:
|
|
|
178
399
|
rules: {
|
|
179
400
|
'elegant/no-boolean-param': 'off',
|
|
180
401
|
'elegant/max-class-methods': 'off',
|
|
402
|
+
'elegant/max-class-dependencies': 'off',
|
|
403
|
+
'elegant/max-class-fields': 'off',
|
|
404
|
+
'elegant/no-comments-in-function-body': 'off',
|
|
181
405
|
'max-params': 'off',
|
|
182
406
|
},
|
|
183
407
|
}
|
|
@@ -190,6 +414,19 @@ The package ships a single CommonJS build that is consumable as both
|
|
|
190
414
|
`import elegant from '@tianjos/eslint-plugin-elegant'`. The exported object
|
|
191
415
|
exposes `{ meta, rules, configs }`.
|
|
192
416
|
|
|
417
|
+
## Prior art
|
|
418
|
+
|
|
419
|
+
This plugin is a TypeScript adaptation of [Elegant Objects](https://www.elegantobjects.org/)
|
|
420
|
+
(Yegor Bugayenko) and [qulice](https://github.com/yegor256/qulice) — the Java
|
|
421
|
+
quality enforcer that codifies those principles on top of Checkstyle and PMD.
|
|
422
|
+
Rules such as `no-logic-in-constructor` (qulice's `ConstructorsCodeFreeCheck`),
|
|
423
|
+
`max-class-dependencies` (Checkstyle's `ClassDataAbstractionCoupling`),
|
|
424
|
+
`max-class-fields` (PMD's `TooManyFields`),
|
|
425
|
+
`no-comments-in-function-body` (`MethodBodyCommentsCheck`), `no-null`,
|
|
426
|
+
`no-getters-setters`, and `no-static-members` are ports of that
|
|
427
|
+
philosophy. The concepts are reimplemented from scratch against the TypeScript
|
|
428
|
+
AST; no qulice code is used.
|
|
429
|
+
|
|
193
430
|
## License
|
|
194
431
|
|
|
195
432
|
[MIT](./LICENSE) © Thiago
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,18 @@ declare const rules: {
|
|
|
8
8
|
}], unknown, TSESLint.RuleListener> & {
|
|
9
9
|
name: string;
|
|
10
10
|
};
|
|
11
|
+
'max-class-dependencies': TSESLint.RuleModule<"tooManyDependencies", [{
|
|
12
|
+
max: number;
|
|
13
|
+
ignore: string[];
|
|
14
|
+
}], unknown, TSESLint.RuleListener> & {
|
|
15
|
+
name: string;
|
|
16
|
+
};
|
|
17
|
+
'max-class-fields': TSESLint.RuleModule<"tooManyFields", [{
|
|
18
|
+
max: number;
|
|
19
|
+
ignoreDecorated: boolean;
|
|
20
|
+
}], unknown, TSESLint.RuleListener> & {
|
|
21
|
+
name: string;
|
|
22
|
+
};
|
|
11
23
|
'no-type-assertion': TSESLint.RuleModule<"noAssertion", [], unknown, TSESLint.RuleListener> & {
|
|
12
24
|
name: string;
|
|
13
25
|
};
|
|
@@ -36,6 +48,25 @@ declare const rules: {
|
|
|
36
48
|
'no-null': TSESLint.RuleModule<"noNull", [], unknown, TSESLint.RuleListener> & {
|
|
37
49
|
name: string;
|
|
38
50
|
};
|
|
51
|
+
'no-comments-in-function-body': TSESLint.RuleModule<"commentInBody", [{
|
|
52
|
+
allow: string[];
|
|
53
|
+
}], unknown, TSESLint.RuleListener> & {
|
|
54
|
+
name: string;
|
|
55
|
+
};
|
|
56
|
+
'no-else-after-throw': TSESLint.RuleModule<"elseAfterThrow", [], unknown, TSESLint.RuleListener> & {
|
|
57
|
+
name: string;
|
|
58
|
+
};
|
|
59
|
+
'no-interpolated-log-message': TSESLint.RuleModule<"interpolatedMessage", [{
|
|
60
|
+
objects: string[];
|
|
61
|
+
methods: string[];
|
|
62
|
+
}], unknown, TSESLint.RuleListener> & {
|
|
63
|
+
name: string;
|
|
64
|
+
};
|
|
65
|
+
'max-returns': TSESLint.RuleModule<"tooManyReturns", [{
|
|
66
|
+
max: number;
|
|
67
|
+
}], unknown, TSESLint.RuleListener> & {
|
|
68
|
+
name: string;
|
|
69
|
+
};
|
|
39
70
|
};
|
|
40
71
|
type Plugin = {
|
|
41
72
|
meta: {
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
|
+
const max_class_dependencies_1 = __importDefault(require("./rules/max-class-dependencies"));
|
|
6
|
+
const max_class_fields_1 = __importDefault(require("./rules/max-class-fields"));
|
|
7
|
+
const max_returns_1 = __importDefault(require("./rules/max-returns"));
|
|
5
8
|
const max_class_methods_1 = __importDefault(require("./rules/max-class-methods"));
|
|
6
9
|
const no_boolean_param_1 = __importDefault(require("./rules/no-boolean-param"));
|
|
10
|
+
const no_comments_in_function_body_1 = __importDefault(require("./rules/no-comments-in-function-body"));
|
|
11
|
+
const no_else_after_throw_1 = __importDefault(require("./rules/no-else-after-throw"));
|
|
7
12
|
const no_getters_setters_1 = __importDefault(require("./rules/no-getters-setters"));
|
|
8
13
|
const no_instanceof_1 = __importDefault(require("./rules/no-instanceof"));
|
|
14
|
+
const no_interpolated_log_message_1 = __importDefault(require("./rules/no-interpolated-log-message"));
|
|
9
15
|
const no_logic_in_constructor_1 = __importDefault(require("./rules/no-logic-in-constructor"));
|
|
10
16
|
const no_null_1 = __importDefault(require("./rules/no-null"));
|
|
11
17
|
const no_null_return_1 = __importDefault(require("./rules/no-null-return"));
|
|
@@ -16,6 +22,8 @@ const { name, version } = require('../package.json');
|
|
|
16
22
|
const rules = {
|
|
17
23
|
'no-boolean-param': no_boolean_param_1.default,
|
|
18
24
|
'max-class-methods': max_class_methods_1.default,
|
|
25
|
+
'max-class-dependencies': max_class_dependencies_1.default,
|
|
26
|
+
'max-class-fields': max_class_fields_1.default,
|
|
19
27
|
'no-type-assertion': no_type_assertion_1.default,
|
|
20
28
|
'no-null-return': no_null_return_1.default,
|
|
21
29
|
'no-public-mutable-props': no_public_mutable_props_1.default,
|
|
@@ -24,6 +32,10 @@ const rules = {
|
|
|
24
32
|
'no-instanceof': no_instanceof_1.default,
|
|
25
33
|
'no-static-members': no_static_members_1.default,
|
|
26
34
|
'no-null': no_null_1.default,
|
|
35
|
+
'no-comments-in-function-body': no_comments_in_function_body_1.default,
|
|
36
|
+
'no-else-after-throw': no_else_after_throw_1.default,
|
|
37
|
+
'no-interpolated-log-message': no_interpolated_log_message_1.default,
|
|
38
|
+
'max-returns': max_returns_1.default,
|
|
27
39
|
};
|
|
28
40
|
const plugin = {
|
|
29
41
|
meta: { name, version },
|
|
@@ -36,6 +48,9 @@ plugin.configs.recommended = {
|
|
|
36
48
|
rules: {
|
|
37
49
|
'elegant/no-boolean-param': 'error',
|
|
38
50
|
'elegant/max-class-methods': ['warn', { max: 10 }],
|
|
51
|
+
'elegant/max-class-dependencies': ['warn', { max: 4 }],
|
|
52
|
+
'elegant/max-class-fields': ['warn', { max: 5 }],
|
|
53
|
+
'elegant/max-returns': ['warn', { max: 3 }],
|
|
39
54
|
'elegant/no-type-assertion': 'error',
|
|
40
55
|
'elegant/no-null-return': 'error',
|
|
41
56
|
'elegant/no-public-mutable-props': 'error',
|
|
@@ -44,7 +59,11 @@ plugin.configs.recommended = {
|
|
|
44
59
|
'elegant/no-instanceof': 'error',
|
|
45
60
|
'elegant/no-static-members': 'error',
|
|
46
61
|
'elegant/no-null': 'error',
|
|
62
|
+
'elegant/no-comments-in-function-body': 'error',
|
|
63
|
+
'elegant/no-else-after-throw': 'error',
|
|
64
|
+
'elegant/no-interpolated-log-message': 'error',
|
|
47
65
|
'max-params': ['warn', { max: 3 }],
|
|
66
|
+
'no-else-return': ['error', { allowElseIf: false }],
|
|
48
67
|
},
|
|
49
68
|
};
|
|
50
69
|
plugin.default = plugin;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type Options = [{
|
|
3
|
+
max: number;
|
|
4
|
+
ignore: string[];
|
|
5
|
+
}];
|
|
6
|
+
declare const _default: TSESLint.RuleModule<"tooManyDependencies", Options, unknown, TSESLint.RuleListener> & {
|
|
7
|
+
name: string;
|
|
8
|
+
};
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,112 @@
|
|
|
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 = 4;
|
|
6
|
+
/**
|
|
7
|
+
* Ambient types every codebase reaches for. Instantiating a `Date` or a `Map` is
|
|
8
|
+
* not a design decision worth budgeting, so they never count as collaborators.
|
|
9
|
+
*/
|
|
10
|
+
const BUILT_INS = [
|
|
11
|
+
'Array',
|
|
12
|
+
'Date',
|
|
13
|
+
'Error',
|
|
14
|
+
'Map',
|
|
15
|
+
'Promise',
|
|
16
|
+
'RegExp',
|
|
17
|
+
'Set',
|
|
18
|
+
'URL',
|
|
19
|
+
'WeakMap',
|
|
20
|
+
'WeakSet',
|
|
21
|
+
];
|
|
22
|
+
const injected = (body, sourceCode) => {
|
|
23
|
+
const constructor = body.body.find((member) => member.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
|
|
24
|
+
member.kind === 'constructor');
|
|
25
|
+
return (constructor?.value.params ?? []).flatMap((param) => {
|
|
26
|
+
const target = param.type === utils_1.AST_NODE_TYPES.TSParameterProperty
|
|
27
|
+
? param.parameter
|
|
28
|
+
: param;
|
|
29
|
+
const annotation = target.typeAnnotation?.typeAnnotation;
|
|
30
|
+
return annotation?.type === utils_1.AST_NODE_TYPES.TSTypeReference
|
|
31
|
+
? [
|
|
32
|
+
{
|
|
33
|
+
key: sourceCode.getText(annotation),
|
|
34
|
+
root: sourceCode.getText(annotation.typeName),
|
|
35
|
+
},
|
|
36
|
+
]
|
|
37
|
+
: [];
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
const instantiated = (node, sourceCode) => {
|
|
41
|
+
const root = sourceCode.getText(node.callee);
|
|
42
|
+
const args = node.typeArguments;
|
|
43
|
+
return {
|
|
44
|
+
key: `${root}${args === undefined ? '' : sourceCode.getText(args)}`,
|
|
45
|
+
root,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
exports.default = (0, createRule_1.createRule)({
|
|
49
|
+
name: 'max-class-dependencies',
|
|
50
|
+
meta: {
|
|
51
|
+
type: 'suggestion',
|
|
52
|
+
docs: {
|
|
53
|
+
description: 'Enforce a maximum number of distinct collaborators a class depends on.',
|
|
54
|
+
},
|
|
55
|
+
messages: {
|
|
56
|
+
tooManyDependencies: "Class '{{name}}' depends on {{count}} types (max {{max}}): {{names}}. Consider extracting a collaborator.",
|
|
57
|
+
},
|
|
58
|
+
schema: [
|
|
59
|
+
{
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
max: { type: 'integer', minimum: 1 },
|
|
63
|
+
ignore: { type: 'array', items: { type: 'string' } },
|
|
64
|
+
},
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
defaultOptions: [{ max: DEFAULT_MAX, ignore: [] }],
|
|
70
|
+
create(context, [{ max, ignore }]) {
|
|
71
|
+
const sourceCode = context.sourceCode;
|
|
72
|
+
const ignored = new Set([...BUILT_INS, ...ignore]);
|
|
73
|
+
const scopes = [];
|
|
74
|
+
const record = (scope, dependency) => {
|
|
75
|
+
if (scope !== undefined && !ignored.has(dependency.root)) {
|
|
76
|
+
scope.add(dependency.key);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
ClassBody(node) {
|
|
81
|
+
const scope = new Set();
|
|
82
|
+
scopes.push(scope);
|
|
83
|
+
for (const dependency of injected(node, sourceCode)) {
|
|
84
|
+
record(scope, dependency);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
NewExpression(node) {
|
|
88
|
+
if (node.parent.type === utils_1.AST_NODE_TYPES.ThrowStatement) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
record(scopes[scopes.length - 1], instantiated(node, sourceCode));
|
|
92
|
+
},
|
|
93
|
+
'ClassBody:exit'(node) {
|
|
94
|
+
const dependencies = scopes.pop() ?? new Set();
|
|
95
|
+
if (dependencies.size <= max) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const classNode = node.parent;
|
|
99
|
+
context.report({
|
|
100
|
+
node: classNode.id ?? node,
|
|
101
|
+
messageId: 'tooManyDependencies',
|
|
102
|
+
data: {
|
|
103
|
+
name: classNode.id?.name ?? '(anonymous)',
|
|
104
|
+
count: dependencies.size,
|
|
105
|
+
max,
|
|
106
|
+
names: [...dependencies].join(', '),
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type Options = [{
|
|
3
|
+
max: number;
|
|
4
|
+
ignoreDecorated: boolean;
|
|
5
|
+
}];
|
|
6
|
+
declare const _default: TSESLint.RuleModule<"tooManyFields", Options, unknown, TSESLint.RuleListener> & {
|
|
7
|
+
name: string;
|
|
8
|
+
};
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,81 @@
|
|
|
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 = 5;
|
|
6
|
+
const isField = (member) => member.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
|
|
7
|
+
member.type === utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition ||
|
|
8
|
+
member.type === utils_1.AST_NODE_TYPES.AccessorProperty;
|
|
9
|
+
const named = (key, sourceCode) => key.type === utils_1.AST_NODE_TYPES.Identifier ? key.name : sourceCode.getText(key);
|
|
10
|
+
/**
|
|
11
|
+
* Properties declared in the class body. A decorated one is skipped by default:
|
|
12
|
+
* `@Column`, `@IsString` and friends map a field to a table or a payload, which
|
|
13
|
+
* is framework shape rather than state the class chose to carry.
|
|
14
|
+
*/
|
|
15
|
+
const declared = (member, sourceCode, ignoreDecorated) => isField(member) &&
|
|
16
|
+
!member.static &&
|
|
17
|
+
!(ignoreDecorated && member.decorators.length > 0)
|
|
18
|
+
? [named(member.key, sourceCode)]
|
|
19
|
+
: [];
|
|
20
|
+
/**
|
|
21
|
+
* Constructor parameter properties. `ignoreDecorated` deliberately does not
|
|
22
|
+
* reach them: a decorator on a parameter is injection (`@Inject(TOKEN)`), so the
|
|
23
|
+
* field is a genuine collaborator and has to stay inside the budget.
|
|
24
|
+
*/
|
|
25
|
+
const promoted = (member, sourceCode) => {
|
|
26
|
+
if (member.type !== utils_1.AST_NODE_TYPES.MethodDefinition ||
|
|
27
|
+
member.kind !== 'constructor') {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return member.value.params.flatMap((param) => param.type === utils_1.AST_NODE_TYPES.TSParameterProperty
|
|
31
|
+
? [named(param.parameter, sourceCode)]
|
|
32
|
+
: []);
|
|
33
|
+
};
|
|
34
|
+
exports.default = (0, createRule_1.createRule)({
|
|
35
|
+
name: 'max-class-fields',
|
|
36
|
+
meta: {
|
|
37
|
+
type: 'suggestion',
|
|
38
|
+
docs: {
|
|
39
|
+
description: 'Enforce a maximum number of instance fields per class to keep objects from becoming data bags.',
|
|
40
|
+
},
|
|
41
|
+
messages: {
|
|
42
|
+
tooManyFields: "Class '{{name}}' holds {{count}} fields (max {{max}}): {{names}}. Consider grouping related fields into a value object.",
|
|
43
|
+
},
|
|
44
|
+
schema: [
|
|
45
|
+
{
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
max: { type: 'integer', minimum: 1 },
|
|
49
|
+
ignoreDecorated: { type: 'boolean' },
|
|
50
|
+
},
|
|
51
|
+
additionalProperties: false,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
defaultOptions: [{ max: DEFAULT_MAX, ignoreDecorated: true }],
|
|
56
|
+
create(context, [{ max, ignoreDecorated }]) {
|
|
57
|
+
const sourceCode = context.sourceCode;
|
|
58
|
+
return {
|
|
59
|
+
ClassBody(node) {
|
|
60
|
+
const fields = node.body.flatMap((member) => [
|
|
61
|
+
...declared(member, sourceCode, ignoreDecorated),
|
|
62
|
+
...promoted(member, sourceCode),
|
|
63
|
+
]);
|
|
64
|
+
if (fields.length <= max) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const classNode = node.parent;
|
|
68
|
+
context.report({
|
|
69
|
+
node: classNode.id ?? node,
|
|
70
|
+
messageId: 'tooManyFields',
|
|
71
|
+
data: {
|
|
72
|
+
name: classNode.id?.name ?? '(anonymous)',
|
|
73
|
+
count: fields.length,
|
|
74
|
+
max,
|
|
75
|
+
names: fields.join(', '),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
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 = 3;
|
|
6
|
+
/**
|
|
7
|
+
* The name to report a function by. Declarations carry their own; the rest
|
|
8
|
+
* borrow it from whatever binds them, so a method and an arrow assigned to a
|
|
9
|
+
* const are named rather than reported as anonymous.
|
|
10
|
+
*/
|
|
11
|
+
const nameOf = (node) => {
|
|
12
|
+
if (node.id !== null) {
|
|
13
|
+
return node.id.name;
|
|
14
|
+
}
|
|
15
|
+
const parent = node.parent;
|
|
16
|
+
if ((parent.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
|
|
17
|
+
parent.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
|
|
18
|
+
parent.type === utils_1.AST_NODE_TYPES.Property) &&
|
|
19
|
+
!parent.computed &&
|
|
20
|
+
parent.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
21
|
+
return parent.key.name;
|
|
22
|
+
}
|
|
23
|
+
if (parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
24
|
+
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
25
|
+
return parent.id.name;
|
|
26
|
+
}
|
|
27
|
+
return '(anonymous)';
|
|
28
|
+
};
|
|
29
|
+
exports.default = (0, createRule_1.createRule)({
|
|
30
|
+
name: 'max-returns',
|
|
31
|
+
meta: {
|
|
32
|
+
type: 'suggestion',
|
|
33
|
+
docs: {
|
|
34
|
+
description: 'Enforce a maximum number of return statements per function.',
|
|
35
|
+
},
|
|
36
|
+
messages: {
|
|
37
|
+
tooManyReturns: "Function '{{name}}' returns from {{count}} places (max {{max}}). Consider collapsing the branches or extracting them into named functions.",
|
|
38
|
+
},
|
|
39
|
+
schema: [
|
|
40
|
+
{
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
max: { type: 'integer', minimum: 1 },
|
|
44
|
+
},
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
defaultOptions: [{ max: DEFAULT_MAX }],
|
|
50
|
+
create(context, [{ max }]) {
|
|
51
|
+
const sourceCode = context.sourceCode;
|
|
52
|
+
const scopes = [];
|
|
53
|
+
const enter = (node) => {
|
|
54
|
+
scopes.push({ node, count: 0 });
|
|
55
|
+
};
|
|
56
|
+
const leave = () => {
|
|
57
|
+
const scope = scopes.pop();
|
|
58
|
+
if (scope === undefined || scope.count <= max) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const { node, count } = scope;
|
|
62
|
+
const signature = node.id ?? sourceCode.getFirstToken(node);
|
|
63
|
+
context.report({
|
|
64
|
+
loc: (signature ?? node).loc,
|
|
65
|
+
messageId: 'tooManyReturns',
|
|
66
|
+
data: {
|
|
67
|
+
name: nameOf(node),
|
|
68
|
+
count,
|
|
69
|
+
max,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
ArrowFunctionExpression: enter,
|
|
75
|
+
'ArrowFunctionExpression:exit': leave,
|
|
76
|
+
FunctionDeclaration: enter,
|
|
77
|
+
'FunctionDeclaration:exit': leave,
|
|
78
|
+
FunctionExpression: enter,
|
|
79
|
+
'FunctionExpression:exit': leave,
|
|
80
|
+
ReturnStatement() {
|
|
81
|
+
const scope = scopes[scopes.length - 1];
|
|
82
|
+
if (scope !== undefined) {
|
|
83
|
+
scope.count += 1;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type Options = [{
|
|
2
|
+
allow: string[];
|
|
3
|
+
}];
|
|
4
|
+
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"commentInBody", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
* Comments the toolchain reads rather than humans. Suppressing another rule or
|
|
7
|
+
* a type error inside a body is routine and says nothing about the code's shape,
|
|
8
|
+
* so these never count — the counterpart of qulice's `@checkstyle` exemption.
|
|
9
|
+
*/
|
|
10
|
+
const DIRECTIVES = [
|
|
11
|
+
'@ts-',
|
|
12
|
+
'@vite-ignore',
|
|
13
|
+
'c8 ignore',
|
|
14
|
+
'eslint',
|
|
15
|
+
'istanbul ignore',
|
|
16
|
+
'prettier-ignore',
|
|
17
|
+
'v8 ignore',
|
|
18
|
+
'webpackChunkName',
|
|
19
|
+
];
|
|
20
|
+
const directs = (comment, allow) => {
|
|
21
|
+
const text = comment.value.trim();
|
|
22
|
+
return [...DIRECTIVES, ...allow].some((prefix) => text.startsWith(prefix));
|
|
23
|
+
};
|
|
24
|
+
const encloses = (block, comment) => block.range[0] < comment.range[0] && comment.range[1] < block.range[1];
|
|
25
|
+
const span = (block) => block.range[1] - block.range[0];
|
|
26
|
+
/**
|
|
27
|
+
* The tightest block wrapping the comment, which is what decides whether it is
|
|
28
|
+
* merely annotating an empty one. An empty `catch` inside a busy function still
|
|
29
|
+
* earns its explanation, so the lookup cannot stop at the function body.
|
|
30
|
+
*/
|
|
31
|
+
const innermost = (blocks, comment) => blocks
|
|
32
|
+
.filter((block) => encloses(block, comment))
|
|
33
|
+
.reduce((tightest, block) => tightest === undefined || span(block) < span(tightest)
|
|
34
|
+
? block
|
|
35
|
+
: tightest, undefined);
|
|
36
|
+
exports.default = (0, createRule_1.createRule)({
|
|
37
|
+
name: 'no-comments-in-function-body',
|
|
38
|
+
meta: {
|
|
39
|
+
type: 'suggestion',
|
|
40
|
+
docs: {
|
|
41
|
+
description: 'Disallow comments inside function bodies, where they stand in for a name the code should carry itself.',
|
|
42
|
+
},
|
|
43
|
+
messages: {
|
|
44
|
+
commentInBody: 'A comment inside a function body signals code that needs a better name. Move it to a docblock above the function, or extract what it explains into a named function.',
|
|
45
|
+
},
|
|
46
|
+
schema: [
|
|
47
|
+
{
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
allow: { type: 'array', items: { type: 'string' } },
|
|
51
|
+
},
|
|
52
|
+
additionalProperties: false,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
defaultOptions: [{ allow: [] }],
|
|
57
|
+
create(context, [{ allow }]) {
|
|
58
|
+
const sourceCode = context.sourceCode;
|
|
59
|
+
const bodies = [];
|
|
60
|
+
const blocks = [];
|
|
61
|
+
const collect = (node) => {
|
|
62
|
+
if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
63
|
+
bodies.push(node.body);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
BlockStatement(node) {
|
|
68
|
+
blocks.push(node);
|
|
69
|
+
},
|
|
70
|
+
ArrowFunctionExpression: collect,
|
|
71
|
+
FunctionDeclaration: collect,
|
|
72
|
+
FunctionExpression: collect,
|
|
73
|
+
'Program:exit'() {
|
|
74
|
+
for (const comment of sourceCode.getAllComments()) {
|
|
75
|
+
const inside = bodies.some((body) => encloses(body, comment));
|
|
76
|
+
const block = innermost(blocks, comment);
|
|
77
|
+
const explains = block === undefined || block.body.length === 0;
|
|
78
|
+
if (inside && !explains && !directs(comment, allow)) {
|
|
79
|
+
context.report({ loc: comment.loc, messageId: 'commentInBody' });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
* Whether the branch leaves through a `throw` no matter what. A bare `throw`
|
|
7
|
+
* qualifies, as does a block whose last statement is one. The check does not
|
|
8
|
+
* recurse: a block ending in a nested `if` may or may not throw, and `else`
|
|
9
|
+
* still carries information there.
|
|
10
|
+
*/
|
|
11
|
+
const alwaysThrows = (branch) => {
|
|
12
|
+
if (branch.type === utils_1.AST_NODE_TYPES.ThrowStatement) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (branch.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return branch.body.at(-1)?.type === utils_1.AST_NODE_TYPES.ThrowStatement;
|
|
19
|
+
};
|
|
20
|
+
exports.default = (0, createRule_1.createRule)({
|
|
21
|
+
name: 'no-else-after-throw',
|
|
22
|
+
meta: {
|
|
23
|
+
type: 'suggestion',
|
|
24
|
+
docs: {
|
|
25
|
+
description: 'Disallow an else branch when the then branch always throws.',
|
|
26
|
+
},
|
|
27
|
+
messages: {
|
|
28
|
+
elseAfterThrow: "The 'then' branch always throws, so 'else' adds nothing but nesting. Drop it and let the alternative sit at the outer level.",
|
|
29
|
+
},
|
|
30
|
+
schema: [],
|
|
31
|
+
},
|
|
32
|
+
defaultOptions: [],
|
|
33
|
+
create(context) {
|
|
34
|
+
const sourceCode = context.sourceCode;
|
|
35
|
+
return {
|
|
36
|
+
IfStatement(node) {
|
|
37
|
+
if (node.alternate === null || !alwaysThrows(node.consequent)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const keyword = sourceCode.getTokenBefore(node.alternate);
|
|
41
|
+
context.report({
|
|
42
|
+
node: keyword ?? node.alternate,
|
|
43
|
+
messageId: 'elseAfterThrow',
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type Options = [{
|
|
2
|
+
objects: string[];
|
|
3
|
+
methods: string[];
|
|
4
|
+
}];
|
|
5
|
+
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"interpolatedMessage", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
6
|
+
name: string;
|
|
7
|
+
};
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,88 @@
|
|
|
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 OBJECTS = ['log', 'logger'];
|
|
6
|
+
const METHODS = [
|
|
7
|
+
'debug',
|
|
8
|
+
'error',
|
|
9
|
+
'fatal',
|
|
10
|
+
'info',
|
|
11
|
+
'log',
|
|
12
|
+
'trace',
|
|
13
|
+
'verbose',
|
|
14
|
+
'warn',
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* The name a call hangs off, as written. `logger.info` yields `logger`, and
|
|
18
|
+
* `this.logger.info` yields `logger` too, so a field and a local read alike.
|
|
19
|
+
*/
|
|
20
|
+
const receiver = (node) => {
|
|
21
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
22
|
+
return node.name;
|
|
23
|
+
}
|
|
24
|
+
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
25
|
+
!node.computed &&
|
|
26
|
+
node.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
27
|
+
return node.property.name;
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The message is the first argument that is not an object literal, which lands
|
|
33
|
+
* on the message under either convention: `info(msg, data)` as in Nest and
|
|
34
|
+
* winston, and `info(data, msg)` as in pino.
|
|
35
|
+
*/
|
|
36
|
+
const message = (args) => args.find((arg) => arg.type !== utils_1.AST_NODE_TYPES.ObjectExpression);
|
|
37
|
+
const computed = (node) => (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
38
|
+
node.expressions.length > 0) ||
|
|
39
|
+
(node.type === utils_1.AST_NODE_TYPES.BinaryExpression && node.operator === '+');
|
|
40
|
+
exports.default = (0, createRule_1.createRule)({
|
|
41
|
+
name: 'no-interpolated-log-message',
|
|
42
|
+
meta: {
|
|
43
|
+
type: 'suggestion',
|
|
44
|
+
docs: {
|
|
45
|
+
description: 'Require log messages to be constant, with the varying parts passed as structured data.',
|
|
46
|
+
},
|
|
47
|
+
messages: {
|
|
48
|
+
interpolatedMessage: 'A computed log message cannot be grouped or searched. Keep the message constant and pass the varying parts as structured data.',
|
|
49
|
+
},
|
|
50
|
+
schema: [
|
|
51
|
+
{
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
objects: { type: 'array', items: { type: 'string' } },
|
|
55
|
+
methods: { type: 'array', items: { type: 'string' } },
|
|
56
|
+
},
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
defaultOptions: [{ objects: [], methods: [] }],
|
|
62
|
+
create(context, [{ objects, methods }]) {
|
|
63
|
+
const logging = new Set([...OBJECTS, ...objects]);
|
|
64
|
+
const levels = new Set([...METHODS, ...methods]);
|
|
65
|
+
return {
|
|
66
|
+
CallExpression(node) {
|
|
67
|
+
const callee = node.callee;
|
|
68
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
69
|
+
callee.computed ||
|
|
70
|
+
callee.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
71
|
+
!levels.has(callee.property.name)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const name = receiver(callee.object);
|
|
75
|
+
if (name === undefined || !logging.has(name)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const argument = message(node.arguments);
|
|
79
|
+
if (argument !== undefined && computed(argument)) {
|
|
80
|
+
context.report({
|
|
81
|
+
node: argument,
|
|
82
|
+
messageId: 'interpolatedMessage',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tianjos/eslint-plugin-elegant",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Opinionated ESLint rules for elegant, behavior-rich TypeScript:
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|
|
7
7
|
"eslint-plugin",
|