@tianjos/eslint-plugin-elegant 0.3.1 → 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 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 the native
72
- [`max-params`](https://eslint.org/docs/latest/rules/max-params) rule.
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,62 +86,304 @@ 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
 
90
- - **`no-boolean-param`** — a boolean argument almost always means the callee
91
- does two things. Prefer two intention-revealing functions or an options
92
- object. Flags both annotated (`flag: boolean`) and boolean-defaulted
93
- (`flag = false`) parameters.
94
- - **`max-class-methods`** a proxy for the Single Responsibility Principle.
95
- Constructors are not counted; getters and setters are.
96
- - **`no-type-assertion`** — assertions silence the type checker. Reach for a
97
- type guard, a generic, or a correctly typed value instead. `as const` is
98
- permitted because it narrows rather than widens.
99
- - **`no-null-return`** keeps absence out of return values; model it with an
100
- explicit domain type or throw.
101
- - **`no-public-mutable-props`** — public state should be `readonly` so callers
102
- cannot break an aggregate's invariants. `private`/`protected` members and
103
- `readonly` members are allowed.
104
- - **`no-logic-in-constructor`** a constructor should only wire arguments to
105
- fields. Validation, transformation, and I/O belong in a static factory or a
106
- method, keeping object construction predictable. Parameter properties
107
- (`constructor(private readonly x: T)`) and a leading `super(...)` are allowed;
108
- computed right-hand sides (`this.x = x * 2`, `this.items = items.slice()`) and
109
- any non-assignment statement are flagged.
110
- - **`no-getters-setters`** getters and setters turn objects into data bags;
111
- prefer methods that expose behavior. Native `get`/`set` accessors (and
112
- `accessor` fields) are always flagged. The opt-in `{ methods: true }` option
113
- also flags conventional `getX`/`setX` methods — useful for strict Elegant
114
- Objects style, but noisy around repositories and framework hooks, so it stays
115
- off in `recommended`.
116
- - **`no-instanceof`** `instanceof` is type discrimination that belongs inside a
117
- polymorphic method on the object. Pairs with `no-type-assertion` to keep
118
- type-based branching out of the codebase.
119
- - **`no-static-members`** static state and behavior cannot be injected,
120
- substituted, or mocked. Prefer instances (with dependency injection) and a
121
- module-level `const` for shared values. The `{ allowReadonly: true }` option
122
- permits `static readonly` constants. Note this also flags `static` factory
123
- methods (`static create()`), which are common; relax per-file if your design
124
- relies on them.
125
- - **`no-null`** — completes `no-null-return` by banning the `null` literal as a
126
- value everywhere (`const x = null`, `x === null`, `fn(null)`), pushing absence
127
- into explicit types or `undefined`. `null` in type positions (`string | null`)
128
- and a direct `return null` (owned by `no-null-return`) are left alone. This is
129
- strict and will flag idioms like `JSON.stringify(x, null, 2)` — relax it in the
130
- files where you interoperate with null-based APIs.
98
+ #### `no-boolean-param`
99
+
100
+ A boolean argument almost always means the callee does two things. Prefer two
101
+ intention-revealing functions or an options object. Flags both annotated
102
+ (`flag: boolean`) and boolean-defaulted (`flag = false`) parameters.
103
+
104
+ #### `max-class-methods`
105
+
106
+ A proxy for the Single Responsibility Principle. Constructors are not counted;
107
+ getters and setters are. Configurable via `{ max: number }` (default `10`).
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
+
169
+ #### `no-type-assertion`
170
+
171
+ Assertions silence the type checker. Reach for a type guard, a generic, or a
172
+ correctly typed value instead. `as const` is permitted because it narrows rather
173
+ than widens.
174
+
175
+ #### `no-null-return`
176
+
177
+ Keeps absence out of return values; model it with an explicit domain type or
178
+ throw.
179
+
180
+ #### `no-public-mutable-props`
181
+
182
+ Public state should be `readonly` so callers cannot break an aggregate's
183
+ invariants. `private`/`protected` members and `readonly` members are allowed.
184
+
185
+ #### `no-logic-in-constructor`
186
+
187
+ A constructor should only wire arguments to fields. Validation, transformation,
188
+ and I/O belong in a static factory or a method, keeping object construction
189
+ predictable. Parameter properties (`constructor(private readonly x: T)`) and a
190
+ leading `super(...)` are allowed; computed right-hand sides (`this.x = x * 2`,
191
+ `this.items = items.slice()`) and any non-assignment statement are flagged.
192
+
193
+ #### `no-getters-setters`
194
+
195
+ Getters and setters turn objects into data bags; prefer methods that expose
196
+ behavior. Native `get`/`set` accessors (and `accessor` fields) are always
197
+ flagged. The opt-in `{ methods: true }` option also flags conventional
198
+ `getX`/`setX` methods — useful for strict Elegant Objects style, but noisy
199
+ around repositories and framework hooks, so it stays off in `recommended`.
200
+
201
+ #### `no-instanceof`
202
+
203
+ `instanceof` is type discrimination that belongs inside a polymorphic method on
204
+ the object. Pairs with `no-type-assertion` to keep type-based branching out of
205
+ the codebase.
206
+
207
+ #### `no-static-members`
208
+
209
+ Static state and behavior cannot be injected, substituted, or mocked. Prefer
210
+ instances (with dependency injection) and a module-level `const` for shared
211
+ values. The `{ allowReadonly: true }` option permits `static readonly`
212
+ constants. Note this also flags `static` factory methods (`static create()`),
213
+ which are common; relax per-file if your design relies on them.
214
+
215
+ #### `no-null`
216
+
217
+ Completes `no-null-return` by banning the `null` literal as a value everywhere
218
+ (`const x = null`, `x === null`, `fn(null)`), pushing absence into explicit types
219
+ or `undefined`. `null` in type positions (`string | null`) and a direct
220
+ `return null` (owned by `no-null-return`) are left alone. This is strict and will
221
+ flag idioms like `JSON.stringify(x, null, 2)` — relax it in the files where you
222
+ interoperate with null-based APIs.
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`).
131
373
 
132
374
  ## Configuration
133
375
 
134
376
  ### Overriding thresholds
135
377
 
136
- `max-class-methods` and the native `max-params` both take a `max` option:
378
+ `max-class-methods`, `max-class-dependencies`, `max-class-fields`, and the
379
+ native `max-params` all take a `max` option:
137
380
 
138
381
  ```js
139
382
  rules: {
140
383
  ...elegant.configs.recommended.rules,
141
384
  'elegant/max-class-methods': ['warn', { max: 15 }],
385
+ 'elegant/max-class-dependencies': ['warn', { max: 6 }],
386
+ 'elegant/max-class-fields': ['warn', { max: 8 }],
142
387
  'max-params': ['warn', { max: 4 }],
143
388
  }
144
389
  ```
@@ -154,6 +399,9 @@ block scoped to your spec globs:
154
399
  rules: {
155
400
  'elegant/no-boolean-param': 'off',
156
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',
157
405
  'max-params': 'off',
158
406
  },
159
407
  }
@@ -166,6 +414,19 @@ The package ships a single CommonJS build that is consumable as both
166
414
  `import elegant from '@tianjos/eslint-plugin-elegant'`. The exported object
167
415
  exposes `{ meta, rules, configs }`.
168
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
+
169
430
  ## License
170
431
 
171
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,7 @@
1
+ type Options = [{
2
+ max: number;
3
+ }];
4
+ declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooManyReturns", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
5
+ name: string;
6
+ };
7
+ 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 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,4 @@
1
+ declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"elseAfterThrow", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
2
+ name: string;
3
+ };
4
+ export default _default;
@@ -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
+ });
@@ -1,7 +1,8 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
2
  /**
3
3
  * Factory for all rules in this plugin. Centralises the docs URL convention so
4
- * every rule links back to its documentation by name.
4
+ * every rule links back to its section in the README. The anchor matches the
5
+ * `#### \`<name>\`` heading GitHub generates for each rule under "Rule details".
5
6
  */
6
7
  export declare const createRule: <Options extends readonly unknown[], MessageIds extends string>({ meta, name, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<Options, MessageIds, unknown>>) => ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
7
8
  name: string;
@@ -4,6 +4,7 @@ exports.createRule = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  /**
6
6
  * Factory for all rules in this plugin. Centralises the docs URL convention so
7
- * every rule links back to its documentation by name.
7
+ * every rule links back to its section in the README. The anchor matches the
8
+ * `#### \`<name>\`` heading GitHub generates for each rule under "Rule details".
8
9
  */
9
- exports.createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://github.com/tianjos/eslint-plugin-elegant/blob/main/docs/rules/${name}.md`);
10
+ exports.createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://github.com/tianjos/eslint-plugin-elegant/blob/main/README.md#${name}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tianjos/eslint-plugin-elegant",
3
- "version": "0.3.1",
4
- "description": "Opinionated ESLint rules for elegant, behavior-rich TypeScript: no flag arguments, no type assertions, no null returns, no public mutable state, and small focused classes.",
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",