@tianjos/eslint-plugin-elegant 0.5.0 → 0.6.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
@@ -84,7 +84,7 @@ The `recommended` config enables every custom rule plus two native ones,
84
84
  | `elegant/no-logic-in-constructor` | custom | Any constructor code beyond `this.field = value` stores and a `super(...)` call | `error` |
85
85
  | `elegant/no-getters-setters` | custom | `get`/`set` accessors (and `getX`/`setX` methods with `{ methods: true }`) | `error` |
86
86
  | `elegant/no-instanceof` | custom | Use of the `instanceof` operator | `error` |
87
- | `elegant/no-static-members` | custom | Static methods, properties, accessors, and blocks (`allowReadonly` to permit constants) | `error` |
87
+ | `elegant/no-static-members` | custom | Static methods, properties, accessors, and blocks (secondary constructors and Nest module factories excepted) | `error` |
88
88
  | `elegant/no-null` | custom | The `null` literal as a value (type annotations and direct `return null` excepted) | `error` |
89
89
  | `elegant/no-comments-in-function-body` | custom | Comments inside function bodies (directives and empty blocks excepted) | `error` |
90
90
  | `elegant/no-else-after-throw` | custom | An `else` branch when the `then` branch always throws | `error` |
@@ -212,8 +212,46 @@ the codebase.
212
212
  Static state and behavior cannot be injected, substituted, or mocked. Prefer
213
213
  instances (with dependency injection) and a module-level `const` for shared
214
214
  values. The `{ allowReadonly: true }` option permits `static readonly`
215
- constants. Note this also flags `static` factory methods (`static create()`),
216
- which are common; relax per-file if your design relies on them.
215
+ constants.
216
+
217
+ Two kinds of static are allowed by default, because neither is behaviour that
218
+ anyone would want to substitute.
219
+
220
+ **A secondary constructor** — a static whose declared return type is the class
221
+ itself. TypeScript cannot overload a constructor, so `static of(...): DueDate`
222
+ inside `DueDate` is the only way to write one, and calling it is
223
+ indistinguishable from calling `new`. That is what separates a named
224
+ constructor from a procedure that moved into a class:
225
+
226
+ ```ts
227
+ class DueDate {
228
+ static of(props: DueDateProps): DueDate {} // allowed
229
+ static parse(raw: unknown): DueDate {} // allowed
230
+ }
231
+
232
+ class DocumentFormatter {
233
+ static formatCNPJ(document: string): string {} // reported — a module function
234
+ }
235
+ ```
236
+
237
+ `this`, `Promise<Self>` and `Self | undefined` all count: a polymorphic, an
238
+ asynchronous and a failing constructor are still constructors. `Self | null`
239
+ does not, because `no-null-return` already owns that shape. The return type has
240
+ to be **written down** — the rule carries no type information, so an
241
+ unannotated `static create() { … }` stays reported. On a factory the annotation
242
+ is one word, and it is what makes the intent legible. Off via
243
+ `{ allowSelfReturning: false }`.
244
+
245
+ **A Nest module factory** — a static returning `DynamicModule` from a class
246
+ decorated with `@Module`. `forRoot`, `forRootAsync`, `register` and
247
+ `registerAsync` are mandated by the framework, not chosen by the design. Both
248
+ halves are required, so naming `DynamicModule` in a return type is not a way
249
+ out of the rule, and a module class gets no blanket exemption for its other
250
+ statics. Off via `{ allowModuleFactories: false }`.
251
+
252
+ Everything else still reports: static accessors (reading one is reaching for
253
+ static state, whatever it returns), `private static` helpers, and static
254
+ classes used as a namespace for functions.
217
255
 
218
256
  #### `no-null`
219
257
 
package/dist/index.d.ts CHANGED
@@ -42,6 +42,8 @@ declare const rules: {
42
42
  };
43
43
  'no-static-members': TSESLint.RuleModule<"staticMember", [{
44
44
  allowReadonly: boolean;
45
+ allowSelfReturning: boolean;
46
+ allowModuleFactories: boolean;
45
47
  }], unknown, TSESLint.RuleListener> & {
46
48
  name: string;
47
49
  };
@@ -1,6 +1,9 @@
1
- type Options = [{
1
+ type Allowances = {
2
2
  allowReadonly: boolean;
3
- }];
3
+ allowSelfReturning: boolean;
4
+ allowModuleFactories: boolean;
5
+ };
6
+ type Options = [Allowances];
4
7
  declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"staticMember", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
5
8
  name: string;
6
9
  };
@@ -1,6 +1,72 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("@typescript-eslint/utils");
3
4
  const createRule_1 = require("../utils/createRule");
5
+ /**
6
+ * Whether the annotation denotes the class itself. TypeScript has no secondary
7
+ * constructors, so `static of(...): DueDate` inside `DueDate` is the only way
8
+ * to write one — and calling it is indistinguishable from calling `new`, which
9
+ * is what separates a named constructor from a procedure that moved into a
10
+ * class.
11
+ *
12
+ * `this`, `Promise<Self>` and `Self | undefined` all count: a polymorphic, an
13
+ * asynchronous and a failing constructor are still constructors. `Self | null`
14
+ * deliberately does not, because `no-null-return` already owns that shape.
15
+ */
16
+ const isNamed = (node, name) => node.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
17
+ node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
18
+ node.typeName.name === name;
19
+ /** What a `Promise<T>` resolves to, or the node itself. */
20
+ const awaited = (node) => isNamed(node, 'Promise') && node.type === utils_1.AST_NODE_TYPES.TSTypeReference
21
+ ? (node.typeArguments?.params[0] ?? node)
22
+ : node;
23
+ const isSelf = (node, className) => {
24
+ const resolved = awaited(node);
25
+ if (resolved.type === utils_1.AST_NODE_TYPES.TSThisType) {
26
+ return true;
27
+ }
28
+ if (resolved.type === utils_1.AST_NODE_TYPES.TSUnionType) {
29
+ return (resolved.types.some((member) => isSelf(member, className)) &&
30
+ resolved.types.every((member) => isSelf(member, className) ||
31
+ member.type === utils_1.AST_NODE_TYPES.TSUndefinedKeyword));
32
+ }
33
+ return isNamed(resolved, className);
34
+ };
35
+ /**
36
+ * A Nest module factory: `DynamicModule` returned from a class Nest recognises
37
+ * as a module. Both halves are required, so `DynamicModule` cannot become a
38
+ * general escape from the rule by being named in a return type.
39
+ */
40
+ const isModuleFactory = (method) => {
41
+ const annotation = method.value.returnType?.typeAnnotation;
42
+ if (annotation === undefined || !isNamed(awaited(annotation), 'DynamicModule')) {
43
+ return false;
44
+ }
45
+ return method.parent.parent.decorators.some((decorator) => decorator.expression.type === utils_1.AST_NODE_TYPES.CallExpression
46
+ ? isModuleIdentifier(decorator.expression.callee)
47
+ : isModuleIdentifier(decorator.expression));
48
+ };
49
+ const isModuleIdentifier = (node) => node.type === utils_1.AST_NODE_TYPES.Identifier && node.name === 'Module';
50
+ /**
51
+ * A secondary constructor's return, read from the annotation alone. The rule
52
+ * carries no type information, so an unannotated static stays reported: on a
53
+ * factory the annotation is one word, and it is what makes the intent legible.
54
+ */
55
+ const returnsSelf = (method, className) => {
56
+ const annotation = method.value.returnType?.typeAnnotation;
57
+ return (className !== undefined &&
58
+ annotation !== undefined &&
59
+ isSelf(annotation, className));
60
+ };
61
+ /**
62
+ * A static the rule lets through: a secondary constructor, or a module factory
63
+ * the framework demands. An accessor is neither, whatever it returns — reading
64
+ * one is reaching for static state.
65
+ */
66
+ const isPermitted = (node, allow) => node.kind === 'method' &&
67
+ ((allow.allowSelfReturning &&
68
+ returnsSelf(node, node.parent.parent.id?.name)) ||
69
+ (allow.allowModuleFactories && isModuleFactory(node)));
4
70
  exports.default = (0, createRule_1.createRule)({
5
71
  name: 'no-static-members',
6
72
  meta: {
@@ -14,24 +80,34 @@ exports.default = (0, createRule_1.createRule)({
14
80
  schema: [
15
81
  {
16
82
  type: 'object',
17
- properties: { allowReadonly: { type: 'boolean' } },
83
+ properties: {
84
+ allowReadonly: { type: 'boolean' },
85
+ allowSelfReturning: { type: 'boolean' },
86
+ allowModuleFactories: { type: 'boolean' },
87
+ },
18
88
  additionalProperties: false,
19
89
  },
20
90
  ],
21
91
  },
22
- defaultOptions: [{ allowReadonly: false }],
23
- create(context, [{ allowReadonly }]) {
92
+ defaultOptions: [
93
+ {
94
+ allowReadonly: false,
95
+ allowSelfReturning: true,
96
+ allowModuleFactories: true,
97
+ },
98
+ ],
99
+ create(context, [allow]) {
24
100
  const reportKey = (key) => {
25
101
  context.report({ node: key, messageId: 'staticMember' });
26
102
  };
27
103
  return {
28
104
  MethodDefinition(node) {
29
- if (node.static) {
105
+ if (node.static && !isPermitted(node, allow)) {
30
106
  reportKey(node.key);
31
107
  }
32
108
  },
33
109
  PropertyDefinition(node) {
34
- if (node.static && !(allowReadonly && node.readonly)) {
110
+ if (node.static && !(allow.allowReadonly && node.readonly)) {
35
111
  reportKey(node.key);
36
112
  }
37
113
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tianjos/eslint-plugin-elegant",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Opinionated ESLint rules for elegant, behavior-rich TypeScript: honest types, encapsulated state, small uncoupled classes, guard-clause flow, and structured logging. Built for NestJS and DDD codebases.",
5
5
  "keywords": [
6
6
  "eslint",
@@ -44,7 +44,8 @@
44
44
  "lint": "npm run build && eslint .",
45
45
  "typecheck": "tsc -p tsconfig.test.json",
46
46
  "test": "jest",
47
- "release": "standard-version",
47
+ "release": "standard-version --release-as minor",
48
+ "release:patch": "standard-version --release-as patch",
48
49
  "prepublishOnly": "npm run build && npm test"
49
50
  },
50
51
  "dependencies": {