@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 +41 -3
- package/dist/index.d.ts +2 -0
- package/dist/rules/no-static-members.d.ts +5 -2
- package/dist/rules/no-static-members.js +81 -5
- package/package.json +3 -2
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 (
|
|
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.
|
|
216
|
-
|
|
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
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
type
|
|
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: {
|
|
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: [
|
|
23
|
-
|
|
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.
|
|
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": {
|