@tianjos/eslint-plugin-elegant 0.1.0 → 0.3.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 +32 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +15 -0
- package/dist/rules/no-getters-setters.d.ts +8 -0
- package/dist/rules/no-getters-setters.js +56 -0
- package/dist/rules/no-instanceof.d.ts +4 -0
- package/dist/rules/no-instanceof.js +24 -0
- package/dist/rules/no-logic-in-constructor.d.ts +5 -0
- package/dist/rules/no-logic-in-constructor.js +78 -0
- package/dist/rules/no-null.d.ts +4 -0
- package/dist/rules/no-null.js +34 -0
- package/dist/rules/no-static-members.d.ts +7 -0
- package/dist/rules/no-static-members.js +48 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,6 +78,11 @@ The `recommended` config enables every custom rule plus the native
|
|
|
78
78
|
| `elegant/no-type-assertion` | custom | `value as T` and `<T>value` assertions (`as const` is allowed) | `error` |
|
|
79
79
|
| `elegant/no-null-return` | custom | `return null` statements | `error` |
|
|
80
80
|
| `elegant/no-public-mutable-props` | custom | Public, non-`readonly` class properties and public constructor parameter props | `error` |
|
|
81
|
+
| `elegant/no-logic-in-constructor` | custom | Any constructor code beyond `this.field = value` stores and a `super(...)` call | `error` |
|
|
82
|
+
| `elegant/no-getters-setters` | custom | `get`/`set` accessors (and `getX`/`setX` methods with `{ methods: true }`) | `error` |
|
|
83
|
+
| `elegant/no-instanceof` | custom | Use of the `instanceof` operator | `error` |
|
|
84
|
+
| `elegant/no-static-members` | custom | Static methods, properties, accessors, and blocks (`allowReadonly` to permit constants) | `error` |
|
|
85
|
+
| `elegant/no-null` | custom | The `null` literal as a value (type annotations and direct `return null` excepted) | `error` |
|
|
81
86
|
| `max-params` | native | Functions declaring more than `max` parameters | `warn` (max 3) |
|
|
82
87
|
|
|
83
88
|
### Rule details
|
|
@@ -96,6 +101,33 @@ The `recommended` config enables every custom rule plus the native
|
|
|
96
101
|
- **`no-public-mutable-props`** — public state should be `readonly` so callers
|
|
97
102
|
cannot break an aggregate's invariants. `private`/`protected` members and
|
|
98
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.
|
|
99
131
|
|
|
100
132
|
## Configuration
|
|
101
133
|
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,25 @@ declare const rules: {
|
|
|
17
17
|
'no-public-mutable-props': TSESLint.RuleModule<"mutableProp", [], unknown, TSESLint.RuleListener> & {
|
|
18
18
|
name: string;
|
|
19
19
|
};
|
|
20
|
+
'no-logic-in-constructor': TSESLint.RuleModule<"statement" | "computation", [], unknown, TSESLint.RuleListener> & {
|
|
21
|
+
name: string;
|
|
22
|
+
};
|
|
23
|
+
'no-getters-setters': TSESLint.RuleModule<"accessor" | "namedAccessor", [{
|
|
24
|
+
methods: boolean;
|
|
25
|
+
}], unknown, TSESLint.RuleListener> & {
|
|
26
|
+
name: string;
|
|
27
|
+
};
|
|
28
|
+
'no-instanceof': TSESLint.RuleModule<"noInstanceof", [], unknown, TSESLint.RuleListener> & {
|
|
29
|
+
name: string;
|
|
30
|
+
};
|
|
31
|
+
'no-static-members': TSESLint.RuleModule<"staticMember", [{
|
|
32
|
+
allowReadonly: boolean;
|
|
33
|
+
}], unknown, TSESLint.RuleListener> & {
|
|
34
|
+
name: string;
|
|
35
|
+
};
|
|
36
|
+
'no-null': TSESLint.RuleModule<"noNull", [], unknown, TSESLint.RuleListener> & {
|
|
37
|
+
name: string;
|
|
38
|
+
};
|
|
20
39
|
};
|
|
21
40
|
type Plugin = {
|
|
22
41
|
meta: {
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
const max_class_methods_1 = __importDefault(require("./rules/max-class-methods"));
|
|
6
6
|
const no_boolean_param_1 = __importDefault(require("./rules/no-boolean-param"));
|
|
7
|
+
const no_getters_setters_1 = __importDefault(require("./rules/no-getters-setters"));
|
|
8
|
+
const no_instanceof_1 = __importDefault(require("./rules/no-instanceof"));
|
|
9
|
+
const no_logic_in_constructor_1 = __importDefault(require("./rules/no-logic-in-constructor"));
|
|
10
|
+
const no_null_1 = __importDefault(require("./rules/no-null"));
|
|
7
11
|
const no_null_return_1 = __importDefault(require("./rules/no-null-return"));
|
|
8
12
|
const no_public_mutable_props_1 = __importDefault(require("./rules/no-public-mutable-props"));
|
|
13
|
+
const no_static_members_1 = __importDefault(require("./rules/no-static-members"));
|
|
9
14
|
const no_type_assertion_1 = __importDefault(require("./rules/no-type-assertion"));
|
|
10
15
|
const { name, version } = require('../package.json');
|
|
11
16
|
const rules = {
|
|
@@ -14,6 +19,11 @@ const rules = {
|
|
|
14
19
|
'no-type-assertion': no_type_assertion_1.default,
|
|
15
20
|
'no-null-return': no_null_return_1.default,
|
|
16
21
|
'no-public-mutable-props': no_public_mutable_props_1.default,
|
|
22
|
+
'no-logic-in-constructor': no_logic_in_constructor_1.default,
|
|
23
|
+
'no-getters-setters': no_getters_setters_1.default,
|
|
24
|
+
'no-instanceof': no_instanceof_1.default,
|
|
25
|
+
'no-static-members': no_static_members_1.default,
|
|
26
|
+
'no-null': no_null_1.default,
|
|
17
27
|
};
|
|
18
28
|
const plugin = {
|
|
19
29
|
meta: { name, version },
|
|
@@ -29,6 +39,11 @@ plugin.configs.recommended = {
|
|
|
29
39
|
'elegant/no-type-assertion': 'error',
|
|
30
40
|
'elegant/no-null-return': 'error',
|
|
31
41
|
'elegant/no-public-mutable-props': 'error',
|
|
42
|
+
'elegant/no-logic-in-constructor': 'error',
|
|
43
|
+
'elegant/no-getters-setters': 'error',
|
|
44
|
+
'elegant/no-instanceof': 'error',
|
|
45
|
+
'elegant/no-static-members': 'error',
|
|
46
|
+
'elegant/no-null': 'error',
|
|
32
47
|
'max-params': ['warn', { max: 3 }],
|
|
33
48
|
},
|
|
34
49
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type Options = [{
|
|
2
|
+
methods: boolean;
|
|
3
|
+
}];
|
|
4
|
+
type MessageIds = 'accessor' | 'namedAccessor';
|
|
5
|
+
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<MessageIds, Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
6
|
+
name: string;
|
|
7
|
+
};
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,56 @@
|
|
|
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 NAMED_ACCESSOR = /^(get|set)[A-Z]/;
|
|
6
|
+
exports.default = (0, createRule_1.createRule)({
|
|
7
|
+
name: 'no-getters-setters',
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Disallow getters and setters, which expose objects as data bags instead of behavior-rich abstractions.',
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
accessor: 'Avoid `get`/`set` accessors. Expose behavior through intention-revealing methods, not property access.',
|
|
15
|
+
namedAccessor: "Method '{{name}}' reads like a getter/setter. Expose behavior, not state.",
|
|
16
|
+
},
|
|
17
|
+
schema: [
|
|
18
|
+
{
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: { methods: { type: 'boolean' } },
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
defaultOptions: [{ methods: false }],
|
|
26
|
+
create(context, [{ methods }]) {
|
|
27
|
+
const reportKey = (key) => {
|
|
28
|
+
context.report({ node: key, messageId: 'accessor' });
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
'MethodDefinition[kind="get"], MethodDefinition[kind="set"]'(node) {
|
|
32
|
+
reportKey(node.key);
|
|
33
|
+
},
|
|
34
|
+
'TSAbstractMethodDefinition[kind="get"], TSAbstractMethodDefinition[kind="set"]'(node) {
|
|
35
|
+
reportKey(node.key);
|
|
36
|
+
},
|
|
37
|
+
AccessorProperty(node) {
|
|
38
|
+
reportKey(node.key);
|
|
39
|
+
},
|
|
40
|
+
TSAbstractAccessorProperty(node) {
|
|
41
|
+
reportKey(node.key);
|
|
42
|
+
},
|
|
43
|
+
'MethodDefinition[kind="method"]'(node) {
|
|
44
|
+
if (methods &&
|
|
45
|
+
node.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
46
|
+
NAMED_ACCESSOR.test(node.key.name)) {
|
|
47
|
+
context.report({
|
|
48
|
+
node: node.key,
|
|
49
|
+
messageId: 'namedAccessor',
|
|
50
|
+
data: { name: node.key.name },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const createRule_1 = require("../utils/createRule");
|
|
4
|
+
exports.default = (0, createRule_1.createRule)({
|
|
5
|
+
name: 'no-instanceof',
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'suggestion',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Disallow the `instanceof` operator. Type discrimination breaks polymorphism; let the object decide via a method instead.',
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
noInstanceof: 'Avoid `instanceof`. Replace type discrimination with a polymorphic method on the object.',
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
defaultOptions: [],
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
'BinaryExpression[operator="instanceof"]'(node) {
|
|
20
|
+
context.report({ node, messageId: 'noInstanceof' });
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
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 isThisMember = (node) => node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
6
|
+
node.object.type === utils_1.AST_NODE_TYPES.ThisExpression;
|
|
7
|
+
const isSuperCall = (node) => node.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
8
|
+
node.callee.type === utils_1.AST_NODE_TYPES.Super;
|
|
9
|
+
/** Strip assertions/non-null so `this.x = y as T` is judged by its inner value. */
|
|
10
|
+
const unwrap = (node) => {
|
|
11
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
12
|
+
node.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
|
|
13
|
+
node.type === utils_1.AST_NODE_TYPES.TSTypeAssertion) {
|
|
14
|
+
return unwrap(node.expression);
|
|
15
|
+
}
|
|
16
|
+
return node;
|
|
17
|
+
};
|
|
18
|
+
/** A value the constructor merely *stores*, as opposed to one it *computes*. */
|
|
19
|
+
const isPlainValue = (node) => {
|
|
20
|
+
const inner = unwrap(node);
|
|
21
|
+
switch (inner.type) {
|
|
22
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
23
|
+
case utils_1.AST_NODE_TYPES.Literal:
|
|
24
|
+
case utils_1.AST_NODE_TYPES.ThisExpression:
|
|
25
|
+
case utils_1.AST_NODE_TYPES.MemberExpression:
|
|
26
|
+
return true;
|
|
27
|
+
default:
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
exports.default = (0, createRule_1.createRule)({
|
|
32
|
+
name: 'no-logic-in-constructor',
|
|
33
|
+
meta: {
|
|
34
|
+
type: 'suggestion',
|
|
35
|
+
docs: {
|
|
36
|
+
description: 'Disallow logic in constructors. A constructor may only assign its arguments to fields; computation belongs in a static factory or a method.',
|
|
37
|
+
},
|
|
38
|
+
messages: {
|
|
39
|
+
statement: 'Constructors must be code-free: only `this.field = value` assignments and a `super(...)` call are allowed here.',
|
|
40
|
+
computation: 'Constructor assignments must store a plain value, not compute one. Move the logic to a static factory or use a default parameter value.',
|
|
41
|
+
},
|
|
42
|
+
schema: [],
|
|
43
|
+
},
|
|
44
|
+
defaultOptions: [],
|
|
45
|
+
create(context) {
|
|
46
|
+
const checkBody = (body) => {
|
|
47
|
+
for (const statement of body) {
|
|
48
|
+
if (statement.type !== utils_1.AST_NODE_TYPES.ExpressionStatement) {
|
|
49
|
+
context.report({ node: statement, messageId: 'statement' });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const { expression } = statement;
|
|
53
|
+
if (isSuperCall(expression)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (expression.type !== utils_1.AST_NODE_TYPES.AssignmentExpression ||
|
|
57
|
+
expression.operator !== '=' ||
|
|
58
|
+
!isThisMember(expression.left)) {
|
|
59
|
+
context.report({ node: statement, messageId: 'statement' });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!isPlainValue(expression.right)) {
|
|
63
|
+
context.report({
|
|
64
|
+
node: expression.right,
|
|
65
|
+
messageId: 'computation',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
'MethodDefinition[kind="constructor"]'(node) {
|
|
72
|
+
if (node.value.body) {
|
|
73
|
+
checkBody(node.value.body.body);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
exports.default = (0, createRule_1.createRule)({
|
|
6
|
+
name: 'no-null',
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'suggestion',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Disallow the `null` literal as a value. Model absence with an explicit domain type, an Optional/Maybe, or by throwing. Null inside type annotations is allowed; direct `return null` is owned by `no-null-return`.',
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
noNull: 'Avoid the null literal. Model absence explicitly instead of leaking null into the code.',
|
|
14
|
+
},
|
|
15
|
+
schema: [],
|
|
16
|
+
},
|
|
17
|
+
defaultOptions: [],
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
Literal(node) {
|
|
21
|
+
if (node.value !== null || node.raw !== 'null') {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
// Direct `return null;` is the domain of `no-null-return`.
|
|
25
|
+
const { parent } = node;
|
|
26
|
+
if (parent.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
|
|
27
|
+
parent.argument === node) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
context.report({ node, messageId: 'noNull' });
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type Options = [{
|
|
2
|
+
allowReadonly: boolean;
|
|
3
|
+
}];
|
|
4
|
+
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"staticMember", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const createRule_1 = require("../utils/createRule");
|
|
4
|
+
exports.default = (0, createRule_1.createRule)({
|
|
5
|
+
name: 'no-static-members',
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'suggestion',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Disallow static members. Static state and behavior cannot be injected, substituted, or mocked; prefer instances and a module-level value when you need a constant.',
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
staticMember: 'Avoid static members. Use an injectable instance, or a module-level constant for shared values.',
|
|
13
|
+
},
|
|
14
|
+
schema: [
|
|
15
|
+
{
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: { allowReadonly: { type: 'boolean' } },
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
defaultOptions: [{ allowReadonly: false }],
|
|
23
|
+
create(context, [{ allowReadonly }]) {
|
|
24
|
+
const reportKey = (key) => {
|
|
25
|
+
context.report({ node: key, messageId: 'staticMember' });
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
MethodDefinition(node) {
|
|
29
|
+
if (node.static) {
|
|
30
|
+
reportKey(node.key);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
PropertyDefinition(node) {
|
|
34
|
+
if (node.static && !(allowReadonly && node.readonly)) {
|
|
35
|
+
reportKey(node.key);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
AccessorProperty(node) {
|
|
39
|
+
if (node.static) {
|
|
40
|
+
reportKey(node.key);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
StaticBlock(node) {
|
|
44
|
+
context.report({ node, messageId: 'staticMember' });
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tianjos/eslint-plugin-elegant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|