@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 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,4 @@
1
+ declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"noInstanceof", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
2
+ name: string;
3
+ };
4
+ export default _default;
@@ -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,5 @@
1
+ type MessageIds = 'statement' | 'computation';
2
+ declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<MessageIds, [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
3
+ name: string;
4
+ };
5
+ export default _default;
@@ -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.1.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",