@tianjos/eslint-plugin-elegant 0.3.2 → 0.5.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 +385 -3
- package/dist/index.d.ts +44 -0
- package/dist/index.js +32 -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-class-methods.js +2 -3
- package/dist/rules/max-returns.d.ts +7 -0
- package/dist/rules/max-returns.js +86 -0
- package/dist/rules/no-anonymous-param-type.d.ts +7 -0
- package/dist/rules/no-anonymous-param-type.js +120 -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 +87 -0
- package/dist/rules/no-logic-in-constructor.js +6 -7
- package/dist/rules/no-null-return.js +3 -4
- package/dist/rules/no-null.js +8 -4
- package/dist/rules/no-property-alias.d.ts +7 -0
- package/dist/rules/no-property-alias.js +67 -0
- package/dist/rules/no-property-destructuring.d.ts +4 -0
- package/dist/rules/no-property-destructuring.js +59 -0
- package/dist/rules/no-public-mutable-props.js +7 -3
- package/dist/utils/locals.d.ts +13 -0
- package/dist/utils/locals.js +26 -0
- package/dist/utils/memberChain.d.ts +8 -0
- package/dist/utils/memberChain.js +22 -0
- package/package.json +3 -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,15 @@ 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) |
|
|
93
|
+
| `elegant/no-property-alias` | custom | Locals that only rename a property of an object already in hand | `error` |
|
|
94
|
+
| `elegant/no-property-destructuring` | custom | Destructuring an object already in hand into locals | `error` |
|
|
95
|
+
| `elegant/no-anonymous-param-type` | custom | Parameters typed as an anonymous shape of `minMembers` or more properties | `error` (minMembers 2) |
|
|
86
96
|
| `max-params` | native | Functions declaring more than `max` parameters | `warn` (max 3) |
|
|
97
|
+
| `no-else-return` | native | An `else` branch when the `then` branch always returns (`allowElseIf: false`) | `error` |
|
|
87
98
|
|
|
88
99
|
### Rule details
|
|
89
100
|
|
|
@@ -98,6 +109,66 @@ intention-revealing functions or an options object. Flags both annotated
|
|
|
98
109
|
A proxy for the Single Responsibility Principle. Constructors are not counted;
|
|
99
110
|
getters and setters are. Configurable via `{ max: number }` (default `10`).
|
|
100
111
|
|
|
112
|
+
#### `max-class-dependencies`
|
|
113
|
+
|
|
114
|
+
The coupling counterpart to `max-class-methods`: a class that needs six
|
|
115
|
+
collaborators to do its job is coordinating, not modelling. Counts the distinct
|
|
116
|
+
types annotated on constructor parameters plus every type instantiated with
|
|
117
|
+
`new` inside the class body — so a dependency hidden behind `new HttpClient()`
|
|
118
|
+
weighs the same as an injected one.
|
|
119
|
+
|
|
120
|
+
De-duplication keeps type arguments, so `Repository<Order>` and
|
|
121
|
+
`Repository<Customer>` count as two collaborators while the same `Clock`
|
|
122
|
+
injected twice counts as one. Nested classes are budgeted independently of their
|
|
123
|
+
host.
|
|
124
|
+
|
|
125
|
+
Three things never count: primitives and inline types (they are not type
|
|
126
|
+
references), a default list of ambient built-ins (`Date`, `Map`, `Set`,
|
|
127
|
+
`Promise`, `Error`, `Array`, `RegExp`, `URL`, `WeakMap`, `WeakSet`), and
|
|
128
|
+
exceptions raised with `throw new ...`. That last exclusion is what makes the
|
|
129
|
+
rule usable in NestJS, where `throw new NotFoundException()` is routine and says
|
|
130
|
+
nothing about a class's design.
|
|
131
|
+
|
|
132
|
+
Configurable via `{ max: number, ignore: string[] }` (default `max: 4`).
|
|
133
|
+
`ignore` adds to the built-in list — reach for it when an ambient concern such
|
|
134
|
+
as `Logger` or `ConfigService` is in every constructor and you would rather not
|
|
135
|
+
budget for it:
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
'elegant/max-class-dependencies': ['warn', { max: 4, ignore: ['Logger'] }],
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### `max-class-fields`
|
|
142
|
+
|
|
143
|
+
The third axis of class size, after methods and collaborators: a class carrying
|
|
144
|
+
a dozen fields is a record with a namespace, not a model. Counts instance fields
|
|
145
|
+
declared in the body — plain, `abstract`, or `accessor` — plus every constructor
|
|
146
|
+
parameter property. Methods and accessors belong to `max-class-methods`, and
|
|
147
|
+
`static` members to `no-static-members`, so neither is counted here.
|
|
148
|
+
|
|
149
|
+
Decorated properties are skipped by default. `@Column`, `@IsString` and
|
|
150
|
+
`@ApiProperty` map a field to a table or a payload, so a DTO or an ORM entity
|
|
151
|
+
declares one field per column by design and has no business inside a budget:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
class CreateOrderDto {
|
|
155
|
+
@IsString() customerId: string; // not counted
|
|
156
|
+
@IsInt() quantity: number; // not counted
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**The exemption stops at the constructor.** A decorator on a parameter is
|
|
161
|
+
injection, not mapping, so `@Inject(TOKEN) private readonly repo: Repo` stays
|
|
162
|
+
inside the budget — otherwise a service wired entirely through tokens would
|
|
163
|
+
count zero fields, which is exactly the class the rule exists to catch. Set
|
|
164
|
+
`{ ignoreDecorated: false }` to budget mapped properties too.
|
|
165
|
+
|
|
166
|
+
Configurable via `{ max: number, ignoreDecorated: boolean }` (default `max: 5`,
|
|
167
|
+
`ignoreDecorated: true`). The default leaves room for the four collaborators
|
|
168
|
+
`max-class-dependencies` allows plus one field of genuine state; past that the
|
|
169
|
+
two rules deliberately overlap, because a class over both budgets is over-sized
|
|
170
|
+
on both axes.
|
|
171
|
+
|
|
101
172
|
#### `no-type-assertion`
|
|
102
173
|
|
|
103
174
|
Assertions silence the type checker. Reach for a type guard, a generic, or a
|
|
@@ -153,16 +224,311 @@ or `undefined`. `null` in type positions (`string | null`) and a direct
|
|
|
153
224
|
flag idioms like `JSON.stringify(x, null, 2)` — relax it in the files where you
|
|
154
225
|
interoperate with null-based APIs.
|
|
155
226
|
|
|
227
|
+
#### `no-comments-in-function-body`
|
|
228
|
+
|
|
229
|
+
A comment inside a body is usually a name that never got written: it labels a
|
|
230
|
+
run of statements that wanted to be its own function. Move the explanation to a
|
|
231
|
+
docblock above the function, or extract what it describes and let the call read
|
|
232
|
+
as the sentence the comment was trying to be.
|
|
233
|
+
|
|
234
|
+
Applies to every function with a block body — methods, constructors, function
|
|
235
|
+
declarations, and arrow functions. Comments outside a body are untouched, so
|
|
236
|
+
docblocks, module-level notes, and comments between class members are fine. Each
|
|
237
|
+
comment is attributed to the innermost function containing it, so one in a
|
|
238
|
+
nested arrow is reported once, against that arrow.
|
|
239
|
+
|
|
240
|
+
Two things are never reported:
|
|
241
|
+
|
|
242
|
+
- **Directives**, which the toolchain reads rather than a human:
|
|
243
|
+
`eslint-disable*`, `@ts-expect-error`, `@ts-ignore`, `prettier-ignore`,
|
|
244
|
+
`istanbul ignore`, `c8 ignore`, `v8 ignore`, `webpackChunkName`,
|
|
245
|
+
`@vite-ignore`. Extend the list with `{ allow: string[] }` for project
|
|
246
|
+
conventions such as `@codegen`.
|
|
247
|
+
- **Comments alone in an empty block**, where there is no code to name and the
|
|
248
|
+
comment is the only thing explaining the silence. The check looks at the
|
|
249
|
+
innermost block, not the function, so an empty `catch` keeps its note even
|
|
250
|
+
inside a busy function:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
function run(): void {
|
|
254
|
+
try {
|
|
255
|
+
go();
|
|
256
|
+
} catch {
|
|
257
|
+
// the failure is expected here
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### `no-else-after-throw`
|
|
263
|
+
|
|
264
|
+
When the `then` branch throws, control never reaches what follows, so `else`
|
|
265
|
+
carries no information and only deepens nesting. Drop it and let the alternative
|
|
266
|
+
sit at the outer level, where it reads as the normal path rather than one of two
|
|
267
|
+
symmetric cases:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
// before
|
|
271
|
+
if (amount < 0) {
|
|
272
|
+
throw new NegativeAmount(amount);
|
|
273
|
+
} else {
|
|
274
|
+
process(amount);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// after
|
|
278
|
+
if (amount < 0) {
|
|
279
|
+
throw new NegativeAmount(amount);
|
|
280
|
+
}
|
|
281
|
+
process(amount);
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
A branch counts as always throwing when it is a bare `throw` or a block whose
|
|
285
|
+
**last** statement is one. The check does not recurse, which is deliberate: a
|
|
286
|
+
block ending in a nested `if` may or may not throw, and there `else` still says
|
|
287
|
+
something.
|
|
288
|
+
|
|
289
|
+
`else if` is flagged too, since the same rewrite applies. The rule has no
|
|
290
|
+
options and no autofix — dedenting a block reliably is the formatter's job, not
|
|
291
|
+
a linter's.
|
|
292
|
+
|
|
293
|
+
Its sibling for `return` is the native
|
|
294
|
+
[`no-else-return`](https://eslint.org/docs/latest/rules/no-else-return), which
|
|
295
|
+
`recommended` enables with `allowElseIf: false` to match. Prefer the native
|
|
296
|
+
rule's defaults? Override it in one line:
|
|
297
|
+
|
|
298
|
+
```js
|
|
299
|
+
'no-else-return': 'error',
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
#### `no-interpolated-log-message`
|
|
303
|
+
|
|
304
|
+
A log message should be a constant, with everything that varies passed as
|
|
305
|
+
structured data. This is not a style preference: `` `order ${id} confirmed` ``
|
|
306
|
+
produces one distinct message per order, which no aggregator can group, and it
|
|
307
|
+
buries `id` inside prose instead of leaving it as a field you can filter on.
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
// before — N messages, and the id is not queryable
|
|
311
|
+
this.logger.log(`order ${id} confirmed for ${customer}`);
|
|
312
|
+
|
|
313
|
+
// after — one message, two fields
|
|
314
|
+
this.logger.log('order confirmed', { orderId: id, customer });
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The rule looks only at the **message argument**, which it takes to be the first
|
|
318
|
+
argument that is not an object literal. That lands on the message under either
|
|
319
|
+
convention — `info(message, data)` as in NestJS and winston, `info(data,
|
|
320
|
+
message)` as in pino — so the rule never dictates where your data goes. An
|
|
321
|
+
interpolated *later* argument is left alone, since that position is context
|
|
322
|
+
rather than the message.
|
|
323
|
+
|
|
324
|
+
Flagged: template literals with expressions, and `+` concatenation. A plain
|
|
325
|
+
identifier passes, so `logger.info(message)` is fine — chasing that would need
|
|
326
|
+
type information, which no rule in this plugin requires.
|
|
327
|
+
|
|
328
|
+
A call counts as logging when the method is a level (`log`, `info`, `warn`,
|
|
329
|
+
`error`, `debug`, `verbose`, `trace`, `fatal`) and the receiver is named `logger`
|
|
330
|
+
or `log`, whether local or a field (`this.logger.info`). Both lists are widened
|
|
331
|
+
with `{ objects: string[], methods: string[] }`:
|
|
332
|
+
|
|
333
|
+
```js
|
|
334
|
+
'elegant/no-interpolated-log-message': ['error', { objects: ['audit'] }],
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**Known limitation.** When the first argument is an identifier, it is taken for
|
|
338
|
+
the message, so pino's error form slips through:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
logger.error(err, `order ${id} failed`); // not reported
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Telling that apart from `logger.info(message)` needs type information. The case
|
|
345
|
+
is pinned by a test so the behaviour is deliberate rather than accidental.
|
|
346
|
+
|
|
347
|
+
#### `max-returns`
|
|
348
|
+
|
|
349
|
+
A port of Checkstyle's `ReturnCount`, but not of its threshold. qulice sets it
|
|
350
|
+
to `1` — a single exit — which reads well in Java and badly here, because it
|
|
351
|
+
outlaws the guard clause that `no-else-after-throw` in this very preset pushes
|
|
352
|
+
you towards. Two rules in one config should not disagree.
|
|
353
|
+
|
|
354
|
+
At `max: 3` the rule stops arguing about single exit and measures sprawl
|
|
355
|
+
instead. Two or three guards followed by a final `return` pass; a function
|
|
356
|
+
leaving from six different places is the one worth splitting.
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
// passes — idiomatic guards
|
|
360
|
+
function charge(amount: number): number {
|
|
361
|
+
if (amount < 0) return 0;
|
|
362
|
+
if (amount > limit) return limit;
|
|
363
|
+
return amount;
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Every function gets its own budget, so a callback's exits are never charged to
|
|
368
|
+
the function hosting it. Bare `return;` counts — leaving early is leaving,
|
|
369
|
+
value or not. An arrow with an expression body has no `return` statement at all
|
|
370
|
+
and never trips the rule.
|
|
371
|
+
|
|
372
|
+
Functions are reported by the name that binds them — a declaration's own, a
|
|
373
|
+
method's key, or the `const` or class field holding an arrow — falling back to
|
|
374
|
+
`(anonymous)` for an inline callback. Configurable via `{ max: number }`
|
|
375
|
+
(default `3`).
|
|
376
|
+
|
|
377
|
+
#### `no-property-alias`
|
|
378
|
+
|
|
379
|
+
A local whose whole job is to hold `obj.status` is a second name for state the
|
|
380
|
+
object already exposes under a name of its own. It buys nothing and it costs a
|
|
381
|
+
reader the hop of proving the two are the same value. Ask the object where you
|
|
382
|
+
need the answer.
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
// reported
|
|
386
|
+
const objStatus = obj.status;
|
|
387
|
+
const authHeader = request.headers.authorization;
|
|
388
|
+
const region = this.cognitoRegion;
|
|
389
|
+
|
|
390
|
+
// passes
|
|
391
|
+
obj.status;
|
|
392
|
+
request.headers.authorization;
|
|
393
|
+
this.cognitoRegion;
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Only variable declarations are reported. `this.total = other.total` transfers
|
|
397
|
+
state rather than aliasing it, and a property in an object literal
|
|
398
|
+
(`{ id: dto.id }`) is how mappers are written; neither trips the rule.
|
|
399
|
+
|
|
400
|
+
Four shapes are never reported, because in each of them the local is doing real
|
|
401
|
+
work:
|
|
402
|
+
|
|
403
|
+
- **A reassigned local.** `let status = obj.status` followed by
|
|
404
|
+
`status = 'EXPIRED'` holds mutable state that no member access stands in for.
|
|
405
|
+
- **A local read inside a nested function.** TypeScript drops a narrowing of
|
|
406
|
+
`obj.prop` at the callback boundary but keeps it on a local, so inlining such
|
|
407
|
+
a declaration stops compiling:
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
const status = obj.status;
|
|
411
|
+
if (status === undefined) return [];
|
|
412
|
+
return obj.items.map((n) => n + status.length); // needs the local
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
- **A chain that is not a plain run of `.prop` accesses.** A computed link
|
|
416
|
+
(`repo.save.mock.calls[1][0].metadata`) or a call in the middle
|
|
417
|
+
(`resolveDates(query).startDate`) is not a property of an object in hand, and
|
|
418
|
+
repeating it reads worse than naming it.
|
|
419
|
+
- **An environment read.** `const topicArn = process.env.SNS_ERROR_TOPIC`
|
|
420
|
+
followed by a guard is fail-fast, and inlining it would read the environment
|
|
421
|
+
twice. Set `{ allowEnv: false }` to hold these to the same standard.
|
|
422
|
+
|
|
423
|
+
Its sibling `no-property-destructuring` covers the same reach-in written as a
|
|
424
|
+
pattern; together they say one thing, which is to ask the object.
|
|
425
|
+
|
|
426
|
+
This rule is the mirror image of ESLint's native
|
|
427
|
+
[`prefer-destructuring`](https://eslint.org/docs/latest/rules/prefer-destructuring),
|
|
428
|
+
which reports `const status = obj.status` and asks you to write
|
|
429
|
+
`const { status } = obj` instead. The two cannot both be on. `prefer-destructuring`
|
|
430
|
+
is off by default, so there is nothing to undo unless you enabled it — and note
|
|
431
|
+
that it only fires when the local and the property share a name, leaving the
|
|
432
|
+
renaming majority (`const objStatus = obj.status`) unreported either way.
|
|
433
|
+
|
|
434
|
+
#### `no-property-destructuring`
|
|
435
|
+
|
|
436
|
+
`const { status, enabled } = obj` is `no-property-alias` written as a pattern:
|
|
437
|
+
the object already names its own state, and the locals are a second set of
|
|
438
|
+
names for it. This rule covers the pattern form, and only when the thing being
|
|
439
|
+
destructured is an object you already hold — a name, `this`, or a run of plain
|
|
440
|
+
`.prop` accesses rooted at one of those.
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
// reported
|
|
444
|
+
const { status, enabled } = obj;
|
|
445
|
+
const { access_token, expires_in } = response.data;
|
|
446
|
+
const { region, poolId } = this.config;
|
|
447
|
+
|
|
448
|
+
// passes — none of these was an object in hand
|
|
449
|
+
function create({ id, name }) {}
|
|
450
|
+
for (const { id, total } of rows) {}
|
|
451
|
+
const { csvContent } = await service.exportCsv(query);
|
|
452
|
+
const { startDate } = resolveDates(query);
|
|
453
|
+
const [rows, total] = await repo.findAndCount();
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Parameter patterns, loop bindings, and `catch` bindings are how you receive a
|
|
457
|
+
value rather than reach into one, so they never come up. Neither does
|
|
458
|
+
`ArrayPattern`: `const [rows, total] = ...` names the halves of a tuple that
|
|
459
|
+
carries no names of its own.
|
|
460
|
+
|
|
461
|
+
Four shapes are never reported, because in each of them the pattern is doing
|
|
462
|
+
work no member access does:
|
|
463
|
+
|
|
464
|
+
- **A rest element.** `const { authorization: _auth, ...safe } = headers`
|
|
465
|
+
constructs a new object by omission. There is nothing to inline it into.
|
|
466
|
+
- **A default value.** `const { max = 3 } = options` inlines to
|
|
467
|
+
`options.max ?? 3`, repeating the fallback at every use site.
|
|
468
|
+
- **A local read inside a nested function**, for the narrowing reason spelled
|
|
469
|
+
out under `no-property-alias`.
|
|
470
|
+
- **A local reassigned later.** `let { status } = obj` followed by
|
|
471
|
+
`status = 'EXPIRED'` holds mutable state of its own.
|
|
472
|
+
|
|
473
|
+
Renaming on the way out (`const { ingestion: failure } = row`) is still
|
|
474
|
+
copying, and so is a single property. Width makes no difference: a pattern
|
|
475
|
+
pulling four fields off an `input` is usually the sign that the method wanted
|
|
476
|
+
the object, not the fields.
|
|
477
|
+
|
|
478
|
+
#### `no-anonymous-param-type`
|
|
479
|
+
|
|
480
|
+
`max-params` and `no-boolean-param` both push you towards an options object —
|
|
481
|
+
and an options object typed inline is a bag that got away with it. The
|
|
482
|
+
parameter count went down, the coupling did not, and the shape has nowhere to
|
|
483
|
+
grow behaviour. Give it a name and it can become a value object; leave it
|
|
484
|
+
anonymous and it stays a struct.
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
// reported
|
|
488
|
+
private toResponse(group: { id: string; name: string; members: number }) {}
|
|
489
|
+
async createFundingProducts(data: { originCode: string; productId: string }) {}
|
|
490
|
+
chart(rows: Array<{ day: string; count: string }>) {}
|
|
491
|
+
constructor(private readonly config: { host: string; port: number }) {}
|
|
492
|
+
|
|
493
|
+
// passes
|
|
494
|
+
async register(input: RegisterProposal) {}
|
|
495
|
+
function charge(amount: number, currency: string) {}
|
|
496
|
+
ingest(raw: Record<string, unknown>) {}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
A shape counts wherever it hides in the annotation — on its own, in a union
|
|
500
|
+
with `null`, intersected onto a named type, in an array, or inside a generic
|
|
501
|
+
argument such as `Array<{ … }>`. A parameter is reported once however many
|
|
502
|
+
shapes it holds, and each offending parameter is reported separately.
|
|
503
|
+
Destructuring in the signature (`function create({ id }: { id: string })`)
|
|
504
|
+
does not hide the bag, and neither does a default value.
|
|
505
|
+
|
|
506
|
+
**Inline callbacks are never reported.** `res.body.items.map((i: { ccbNumber: string; total: number }) => i.total)`
|
|
507
|
+
annotates whatever the callee yields; when that value has no type to borrow, an
|
|
508
|
+
inline shape is the only way to type it at all.
|
|
509
|
+
|
|
510
|
+
Configurable via `{ minMembers: number }` (default `2`). At the default, a
|
|
511
|
+
one-property parameter like `opts?: { required?: boolean }` passes — naming a
|
|
512
|
+
single field is usually ceremony rather than design. Set `minMembers: 1` to
|
|
513
|
+
hold those to the same standard.
|
|
514
|
+
|
|
515
|
+
Unlike its neighbours, this rule does not have a mechanical fix: it asks you to
|
|
516
|
+
introduce a named type and decide where it lives. That is a design change, so
|
|
517
|
+
expect adoption to cost more than a find-and-replace.
|
|
518
|
+
|
|
156
519
|
## Configuration
|
|
157
520
|
|
|
158
521
|
### Overriding thresholds
|
|
159
522
|
|
|
160
|
-
`max-class-methods
|
|
523
|
+
`max-class-methods`, `max-class-dependencies`, `max-class-fields`, and the
|
|
524
|
+
native `max-params` all take a `max` option:
|
|
161
525
|
|
|
162
526
|
```js
|
|
163
527
|
rules: {
|
|
164
528
|
...elegant.configs.recommended.rules,
|
|
165
529
|
'elegant/max-class-methods': ['warn', { max: 15 }],
|
|
530
|
+
'elegant/max-class-dependencies': ['warn', { max: 6 }],
|
|
531
|
+
'elegant/max-class-fields': ['warn', { max: 8 }],
|
|
166
532
|
'max-params': ['warn', { max: 4 }],
|
|
167
533
|
}
|
|
168
534
|
```
|
|
@@ -178,6 +544,9 @@ block scoped to your spec globs:
|
|
|
178
544
|
rules: {
|
|
179
545
|
'elegant/no-boolean-param': 'off',
|
|
180
546
|
'elegant/max-class-methods': 'off',
|
|
547
|
+
'elegant/max-class-dependencies': 'off',
|
|
548
|
+
'elegant/max-class-fields': 'off',
|
|
549
|
+
'elegant/no-comments-in-function-body': 'off',
|
|
181
550
|
'max-params': 'off',
|
|
182
551
|
},
|
|
183
552
|
}
|
|
@@ -190,6 +559,19 @@ The package ships a single CommonJS build that is consumable as both
|
|
|
190
559
|
`import elegant from '@tianjos/eslint-plugin-elegant'`. The exported object
|
|
191
560
|
exposes `{ meta, rules, configs }`.
|
|
192
561
|
|
|
562
|
+
## Prior art
|
|
563
|
+
|
|
564
|
+
This plugin is a TypeScript adaptation of [Elegant Objects](https://www.elegantobjects.org/)
|
|
565
|
+
(Yegor Bugayenko) and [qulice](https://github.com/yegor256/qulice) — the Java
|
|
566
|
+
quality enforcer that codifies those principles on top of Checkstyle and PMD.
|
|
567
|
+
Rules such as `no-logic-in-constructor` (qulice's `ConstructorsCodeFreeCheck`),
|
|
568
|
+
`max-class-dependencies` (Checkstyle's `ClassDataAbstractionCoupling`),
|
|
569
|
+
`max-class-fields` (PMD's `TooManyFields`),
|
|
570
|
+
`no-comments-in-function-body` (`MethodBodyCommentsCheck`), `no-null`,
|
|
571
|
+
`no-getters-setters`, and `no-static-members` are ports of that
|
|
572
|
+
philosophy. The concepts are reimplemented from scratch against the TypeScript
|
|
573
|
+
AST; no qulice code is used.
|
|
574
|
+
|
|
193
575
|
## License
|
|
194
576
|
|
|
195
577
|
[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,38 @@ 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
|
+
};
|
|
70
|
+
'no-property-alias': TSESLint.RuleModule<"aliasesProperty", [{
|
|
71
|
+
allowEnv: boolean;
|
|
72
|
+
}], unknown, TSESLint.RuleListener> & {
|
|
73
|
+
name: string;
|
|
74
|
+
};
|
|
75
|
+
'no-property-destructuring': TSESLint.RuleModule<"destructuresObject", [], unknown, TSESLint.RuleListener> & {
|
|
76
|
+
name: string;
|
|
77
|
+
};
|
|
78
|
+
'no-anonymous-param-type': TSESLint.RuleModule<"anonymousParamType", [{
|
|
79
|
+
minMembers: number;
|
|
80
|
+
}], unknown, TSESLint.RuleListener> & {
|
|
81
|
+
name: string;
|
|
82
|
+
};
|
|
39
83
|
};
|
|
40
84
|
type Plugin = {
|
|
41
85
|
meta: {
|
package/dist/index.js
CHANGED
|
@@ -2,20 +2,35 @@
|
|
|
2
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
|
+
const no_anonymous_param_type_1 = __importDefault(require("./rules/no-anonymous-param-type"));
|
|
6
|
+
const max_class_dependencies_1 = __importDefault(require("./rules/max-class-dependencies"));
|
|
7
|
+
const max_class_fields_1 = __importDefault(require("./rules/max-class-fields"));
|
|
8
|
+
const max_returns_1 = __importDefault(require("./rules/max-returns"));
|
|
5
9
|
const max_class_methods_1 = __importDefault(require("./rules/max-class-methods"));
|
|
6
10
|
const no_boolean_param_1 = __importDefault(require("./rules/no-boolean-param"));
|
|
11
|
+
const no_comments_in_function_body_1 = __importDefault(require("./rules/no-comments-in-function-body"));
|
|
12
|
+
const no_else_after_throw_1 = __importDefault(require("./rules/no-else-after-throw"));
|
|
7
13
|
const no_getters_setters_1 = __importDefault(require("./rules/no-getters-setters"));
|
|
8
14
|
const no_instanceof_1 = __importDefault(require("./rules/no-instanceof"));
|
|
15
|
+
const no_interpolated_log_message_1 = __importDefault(require("./rules/no-interpolated-log-message"));
|
|
9
16
|
const no_logic_in_constructor_1 = __importDefault(require("./rules/no-logic-in-constructor"));
|
|
10
17
|
const no_null_1 = __importDefault(require("./rules/no-null"));
|
|
11
18
|
const no_null_return_1 = __importDefault(require("./rules/no-null-return"));
|
|
19
|
+
const no_property_alias_1 = __importDefault(require("./rules/no-property-alias"));
|
|
20
|
+
const no_property_destructuring_1 = __importDefault(require("./rules/no-property-destructuring"));
|
|
12
21
|
const no_public_mutable_props_1 = __importDefault(require("./rules/no-public-mutable-props"));
|
|
13
22
|
const no_static_members_1 = __importDefault(require("./rules/no-static-members"));
|
|
14
23
|
const no_type_assertion_1 = __importDefault(require("./rules/no-type-assertion"));
|
|
24
|
+
// require() of a JSON file yields `any`, so there is no honest type to reach
|
|
25
|
+
// for here. Importing it instead would put package.json inside the emitted
|
|
26
|
+
// tree and move dist/index.js, which the "exports" map pins.
|
|
27
|
+
// eslint-disable-next-line elegant/no-type-assertion
|
|
15
28
|
const { name, version } = require('../package.json');
|
|
16
29
|
const rules = {
|
|
17
30
|
'no-boolean-param': no_boolean_param_1.default,
|
|
18
31
|
'max-class-methods': max_class_methods_1.default,
|
|
32
|
+
'max-class-dependencies': max_class_dependencies_1.default,
|
|
33
|
+
'max-class-fields': max_class_fields_1.default,
|
|
19
34
|
'no-type-assertion': no_type_assertion_1.default,
|
|
20
35
|
'no-null-return': no_null_return_1.default,
|
|
21
36
|
'no-public-mutable-props': no_public_mutable_props_1.default,
|
|
@@ -24,6 +39,13 @@ const rules = {
|
|
|
24
39
|
'no-instanceof': no_instanceof_1.default,
|
|
25
40
|
'no-static-members': no_static_members_1.default,
|
|
26
41
|
'no-null': no_null_1.default,
|
|
42
|
+
'no-comments-in-function-body': no_comments_in_function_body_1.default,
|
|
43
|
+
'no-else-after-throw': no_else_after_throw_1.default,
|
|
44
|
+
'no-interpolated-log-message': no_interpolated_log_message_1.default,
|
|
45
|
+
'max-returns': max_returns_1.default,
|
|
46
|
+
'no-property-alias': no_property_alias_1.default,
|
|
47
|
+
'no-property-destructuring': no_property_destructuring_1.default,
|
|
48
|
+
'no-anonymous-param-type': no_anonymous_param_type_1.default,
|
|
27
49
|
};
|
|
28
50
|
const plugin = {
|
|
29
51
|
meta: { name, version },
|
|
@@ -36,6 +58,9 @@ plugin.configs.recommended = {
|
|
|
36
58
|
rules: {
|
|
37
59
|
'elegant/no-boolean-param': 'error',
|
|
38
60
|
'elegant/max-class-methods': ['warn', { max: 10 }],
|
|
61
|
+
'elegant/max-class-dependencies': ['warn', { max: 4 }],
|
|
62
|
+
'elegant/max-class-fields': ['warn', { max: 5 }],
|
|
63
|
+
'elegant/max-returns': ['warn', { max: 3 }],
|
|
39
64
|
'elegant/no-type-assertion': 'error',
|
|
40
65
|
'elegant/no-null-return': 'error',
|
|
41
66
|
'elegant/no-public-mutable-props': 'error',
|
|
@@ -44,7 +69,14 @@ plugin.configs.recommended = {
|
|
|
44
69
|
'elegant/no-instanceof': 'error',
|
|
45
70
|
'elegant/no-static-members': 'error',
|
|
46
71
|
'elegant/no-null': 'error',
|
|
72
|
+
'elegant/no-comments-in-function-body': 'error',
|
|
73
|
+
'elegant/no-else-after-throw': 'error',
|
|
74
|
+
'elegant/no-interpolated-log-message': 'error',
|
|
75
|
+
'elegant/no-property-alias': 'error',
|
|
76
|
+
'elegant/no-property-destructuring': 'error',
|
|
77
|
+
'elegant/no-anonymous-param-type': ['error', { minMembers: 2 }],
|
|
47
78
|
'max-params': ['warn', { max: 3 }],
|
|
79
|
+
'no-else-return': ['error', { allowElseIf: false }],
|
|
48
80
|
},
|
|
49
81
|
};
|
|
50
82
|
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;
|