@tianjos/eslint-plugin-elegant 0.1.0 → 0.2.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 +18 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +9 -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/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,6 +78,9 @@ 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` |
|
|
81
84
|
| `max-params` | native | Functions declaring more than `max` parameters | `warn` (max 3) |
|
|
82
85
|
|
|
83
86
|
### Rule details
|
|
@@ -96,6 +99,21 @@ The `recommended` config enables every custom rule plus the native
|
|
|
96
99
|
- **`no-public-mutable-props`** — public state should be `readonly` so callers
|
|
97
100
|
cannot break an aggregate's invariants. `private`/`protected` members and
|
|
98
101
|
`readonly` members are allowed.
|
|
102
|
+
- **`no-logic-in-constructor`** — a constructor should only wire arguments to
|
|
103
|
+
fields. Validation, transformation, and I/O belong in a static factory or a
|
|
104
|
+
method, keeping object construction predictable. Parameter properties
|
|
105
|
+
(`constructor(private readonly x: T)`) and a leading `super(...)` are allowed;
|
|
106
|
+
computed right-hand sides (`this.x = x * 2`, `this.items = items.slice()`) and
|
|
107
|
+
any non-assignment statement are flagged.
|
|
108
|
+
- **`no-getters-setters`** — getters and setters turn objects into data bags;
|
|
109
|
+
prefer methods that expose behavior. Native `get`/`set` accessors (and
|
|
110
|
+
`accessor` fields) are always flagged. The opt-in `{ methods: true }` option
|
|
111
|
+
also flags conventional `getX`/`setX` methods — useful for strict Elegant
|
|
112
|
+
Objects style, but noisy around repositories and framework hooks, so it stays
|
|
113
|
+
off in `recommended`.
|
|
114
|
+
- **`no-instanceof`** — `instanceof` is type discrimination that belongs inside a
|
|
115
|
+
polymorphic method on the object. Pairs with `no-type-assertion` to keep
|
|
116
|
+
type-based branching out of the codebase.
|
|
99
117
|
|
|
100
118
|
## Configuration
|
|
101
119
|
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,17 @@ 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
|
+
};
|
|
20
31
|
};
|
|
21
32
|
type Plugin = {
|
|
22
33
|
meta: {
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,9 @@ 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"));
|
|
7
10
|
const no_null_return_1 = __importDefault(require("./rules/no-null-return"));
|
|
8
11
|
const no_public_mutable_props_1 = __importDefault(require("./rules/no-public-mutable-props"));
|
|
9
12
|
const no_type_assertion_1 = __importDefault(require("./rules/no-type-assertion"));
|
|
@@ -14,6 +17,9 @@ const rules = {
|
|
|
14
17
|
'no-type-assertion': no_type_assertion_1.default,
|
|
15
18
|
'no-null-return': no_null_return_1.default,
|
|
16
19
|
'no-public-mutable-props': no_public_mutable_props_1.default,
|
|
20
|
+
'no-logic-in-constructor': no_logic_in_constructor_1.default,
|
|
21
|
+
'no-getters-setters': no_getters_setters_1.default,
|
|
22
|
+
'no-instanceof': no_instanceof_1.default,
|
|
17
23
|
};
|
|
18
24
|
const plugin = {
|
|
19
25
|
meta: { name, version },
|
|
@@ -29,6 +35,9 @@ plugin.configs.recommended = {
|
|
|
29
35
|
'elegant/no-type-assertion': 'error',
|
|
30
36
|
'elegant/no-null-return': 'error',
|
|
31
37
|
'elegant/no-public-mutable-props': 'error',
|
|
38
|
+
'elegant/no-logic-in-constructor': 'error',
|
|
39
|
+
'elegant/no-getters-setters': 'error',
|
|
40
|
+
'elegant/no-instanceof': 'error',
|
|
32
41
|
'max-params': ['warn', { max: 3 }],
|
|
33
42
|
},
|
|
34
43
|
};
|
|
@@ -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
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tianjos/eslint-plugin-elegant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|