@tianjos/eslint-plugin-elegant 0.4.0 → 0.5.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 +145 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/rules/max-class-dependencies.js +5 -5
- package/dist/rules/max-class-fields.js +12 -12
- package/dist/rules/max-class-methods.js +2 -3
- package/dist/rules/max-returns.js +13 -15
- package/dist/rules/no-anonymous-param-type.d.ts +7 -0
- package/dist/rules/no-anonymous-param-type.js +120 -0
- package/dist/rules/no-interpolated-log-message.js +5 -6
- package/dist/rules/no-logic-in-constructor.js +6 -7
- package/dist/rules/no-null-return.js +3 -4
- package/dist/rules/no-null.js +8 -4
- package/dist/rules/no-property-alias.d.ts +7 -0
- package/dist/rules/no-property-alias.js +67 -0
- package/dist/rules/no-property-destructuring.d.ts +4 -0
- package/dist/rules/no-property-destructuring.js +59 -0
- package/dist/rules/no-public-mutable-props.js +7 -3
- package/dist/utils/locals.d.ts +13 -0
- package/dist/utils/locals.js +26 -0
- package/dist/utils/memberChain.d.ts +8 -0
- package/dist/utils/memberChain.js +22 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -90,6 +90,9 @@ The `recommended` config enables every custom rule plus two native ones,
|
|
|
90
90
|
| `elegant/no-else-after-throw` | custom | An `else` branch when the `then` branch always throws | `error` |
|
|
91
91
|
| `elegant/no-interpolated-log-message` | custom | Log messages built by interpolation or concatenation | `error` |
|
|
92
92
|
| `elegant/max-returns` | custom | Functions returning from more places than `max` | `warn` (max 3) |
|
|
93
|
+
| `elegant/no-property-alias` | custom | Locals that only rename a property of an object already in hand | `error` |
|
|
94
|
+
| `elegant/no-property-destructuring` | custom | Destructuring an object already in hand into locals | `error` |
|
|
95
|
+
| `elegant/no-anonymous-param-type` | custom | Parameters typed as an anonymous shape of `minMembers` or more properties | `error` (minMembers 2) |
|
|
93
96
|
| `max-params` | native | Functions declaring more than `max` parameters | `warn` (max 3) |
|
|
94
97
|
| `no-else-return` | native | An `else` branch when the `then` branch always returns (`allowElseIf: false`) | `error` |
|
|
95
98
|
|
|
@@ -371,6 +374,148 @@ method's key, or the `const` or class field holding an arrow — falling back to
|
|
|
371
374
|
`(anonymous)` for an inline callback. Configurable via `{ max: number }`
|
|
372
375
|
(default `3`).
|
|
373
376
|
|
|
377
|
+
#### `no-property-alias`
|
|
378
|
+
|
|
379
|
+
A local whose whole job is to hold `obj.status` is a second name for state the
|
|
380
|
+
object already exposes under a name of its own. It buys nothing and it costs a
|
|
381
|
+
reader the hop of proving the two are the same value. Ask the object where you
|
|
382
|
+
need the answer.
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
// reported
|
|
386
|
+
const objStatus = obj.status;
|
|
387
|
+
const authHeader = request.headers.authorization;
|
|
388
|
+
const region = this.cognitoRegion;
|
|
389
|
+
|
|
390
|
+
// passes
|
|
391
|
+
obj.status;
|
|
392
|
+
request.headers.authorization;
|
|
393
|
+
this.cognitoRegion;
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Only variable declarations are reported. `this.total = other.total` transfers
|
|
397
|
+
state rather than aliasing it, and a property in an object literal
|
|
398
|
+
(`{ id: dto.id }`) is how mappers are written; neither trips the rule.
|
|
399
|
+
|
|
400
|
+
Four shapes are never reported, because in each of them the local is doing real
|
|
401
|
+
work:
|
|
402
|
+
|
|
403
|
+
- **A reassigned local.** `let status = obj.status` followed by
|
|
404
|
+
`status = 'EXPIRED'` holds mutable state that no member access stands in for.
|
|
405
|
+
- **A local read inside a nested function.** TypeScript drops a narrowing of
|
|
406
|
+
`obj.prop` at the callback boundary but keeps it on a local, so inlining such
|
|
407
|
+
a declaration stops compiling:
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
const status = obj.status;
|
|
411
|
+
if (status === undefined) return [];
|
|
412
|
+
return obj.items.map((n) => n + status.length); // needs the local
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
- **A chain that is not a plain run of `.prop` accesses.** A computed link
|
|
416
|
+
(`repo.save.mock.calls[1][0].metadata`) or a call in the middle
|
|
417
|
+
(`resolveDates(query).startDate`) is not a property of an object in hand, and
|
|
418
|
+
repeating it reads worse than naming it.
|
|
419
|
+
- **An environment read.** `const topicArn = process.env.SNS_ERROR_TOPIC`
|
|
420
|
+
followed by a guard is fail-fast, and inlining it would read the environment
|
|
421
|
+
twice. Set `{ allowEnv: false }` to hold these to the same standard.
|
|
422
|
+
|
|
423
|
+
Its sibling `no-property-destructuring` covers the same reach-in written as a
|
|
424
|
+
pattern; together they say one thing, which is to ask the object.
|
|
425
|
+
|
|
426
|
+
This rule is the mirror image of ESLint's native
|
|
427
|
+
[`prefer-destructuring`](https://eslint.org/docs/latest/rules/prefer-destructuring),
|
|
428
|
+
which reports `const status = obj.status` and asks you to write
|
|
429
|
+
`const { status } = obj` instead. The two cannot both be on. `prefer-destructuring`
|
|
430
|
+
is off by default, so there is nothing to undo unless you enabled it — and note
|
|
431
|
+
that it only fires when the local and the property share a name, leaving the
|
|
432
|
+
renaming majority (`const objStatus = obj.status`) unreported either way.
|
|
433
|
+
|
|
434
|
+
#### `no-property-destructuring`
|
|
435
|
+
|
|
436
|
+
`const { status, enabled } = obj` is `no-property-alias` written as a pattern:
|
|
437
|
+
the object already names its own state, and the locals are a second set of
|
|
438
|
+
names for it. This rule covers the pattern form, and only when the thing being
|
|
439
|
+
destructured is an object you already hold — a name, `this`, or a run of plain
|
|
440
|
+
`.prop` accesses rooted at one of those.
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
// reported
|
|
444
|
+
const { status, enabled } = obj;
|
|
445
|
+
const { access_token, expires_in } = response.data;
|
|
446
|
+
const { region, poolId } = this.config;
|
|
447
|
+
|
|
448
|
+
// passes — none of these was an object in hand
|
|
449
|
+
function create({ id, name }) {}
|
|
450
|
+
for (const { id, total } of rows) {}
|
|
451
|
+
const { csvContent } = await service.exportCsv(query);
|
|
452
|
+
const { startDate } = resolveDates(query);
|
|
453
|
+
const [rows, total] = await repo.findAndCount();
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Parameter patterns, loop bindings, and `catch` bindings are how you receive a
|
|
457
|
+
value rather than reach into one, so they never come up. Neither does
|
|
458
|
+
`ArrayPattern`: `const [rows, total] = ...` names the halves of a tuple that
|
|
459
|
+
carries no names of its own.
|
|
460
|
+
|
|
461
|
+
Four shapes are never reported, because in each of them the pattern is doing
|
|
462
|
+
work no member access does:
|
|
463
|
+
|
|
464
|
+
- **A rest element.** `const { authorization: _auth, ...safe } = headers`
|
|
465
|
+
constructs a new object by omission. There is nothing to inline it into.
|
|
466
|
+
- **A default value.** `const { max = 3 } = options` inlines to
|
|
467
|
+
`options.max ?? 3`, repeating the fallback at every use site.
|
|
468
|
+
- **A local read inside a nested function**, for the narrowing reason spelled
|
|
469
|
+
out under `no-property-alias`.
|
|
470
|
+
- **A local reassigned later.** `let { status } = obj` followed by
|
|
471
|
+
`status = 'EXPIRED'` holds mutable state of its own.
|
|
472
|
+
|
|
473
|
+
Renaming on the way out (`const { ingestion: failure } = row`) is still
|
|
474
|
+
copying, and so is a single property. Width makes no difference: a pattern
|
|
475
|
+
pulling four fields off an `input` is usually the sign that the method wanted
|
|
476
|
+
the object, not the fields.
|
|
477
|
+
|
|
478
|
+
#### `no-anonymous-param-type`
|
|
479
|
+
|
|
480
|
+
`max-params` and `no-boolean-param` both push you towards an options object —
|
|
481
|
+
and an options object typed inline is a bag that got away with it. The
|
|
482
|
+
parameter count went down, the coupling did not, and the shape has nowhere to
|
|
483
|
+
grow behaviour. Give it a name and it can become a value object; leave it
|
|
484
|
+
anonymous and it stays a struct.
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
// reported
|
|
488
|
+
private toResponse(group: { id: string; name: string; members: number }) {}
|
|
489
|
+
async createFundingProducts(data: { originCode: string; productId: string }) {}
|
|
490
|
+
chart(rows: Array<{ day: string; count: string }>) {}
|
|
491
|
+
constructor(private readonly config: { host: string; port: number }) {}
|
|
492
|
+
|
|
493
|
+
// passes
|
|
494
|
+
async register(input: RegisterProposal) {}
|
|
495
|
+
function charge(amount: number, currency: string) {}
|
|
496
|
+
ingest(raw: Record<string, unknown>) {}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
A shape counts wherever it hides in the annotation — on its own, in a union
|
|
500
|
+
with `null`, intersected onto a named type, in an array, or inside a generic
|
|
501
|
+
argument such as `Array<{ … }>`. A parameter is reported once however many
|
|
502
|
+
shapes it holds, and each offending parameter is reported separately.
|
|
503
|
+
Destructuring in the signature (`function create({ id }: { id: string })`)
|
|
504
|
+
does not hide the bag, and neither does a default value.
|
|
505
|
+
|
|
506
|
+
**Inline callbacks are never reported.** `res.body.items.map((i: { ccbNumber: string; total: number }) => i.total)`
|
|
507
|
+
annotates whatever the callee yields; when that value has no type to borrow, an
|
|
508
|
+
inline shape is the only way to type it at all.
|
|
509
|
+
|
|
510
|
+
Configurable via `{ minMembers: number }` (default `2`). At the default, a
|
|
511
|
+
one-property parameter like `opts?: { required?: boolean }` passes — naming a
|
|
512
|
+
single field is usually ceremony rather than design. Set `minMembers: 1` to
|
|
513
|
+
hold those to the same standard.
|
|
514
|
+
|
|
515
|
+
Unlike its neighbours, this rule does not have a mechanical fix: it asks you to
|
|
516
|
+
introduce a named type and decide where it lives. That is a design change, so
|
|
517
|
+
expect adoption to cost more than a find-and-replace.
|
|
518
|
+
|
|
374
519
|
## Configuration
|
|
375
520
|
|
|
376
521
|
### Overriding thresholds
|
package/dist/index.d.ts
CHANGED
|
@@ -67,6 +67,19 @@ declare const rules: {
|
|
|
67
67
|
}], unknown, TSESLint.RuleListener> & {
|
|
68
68
|
name: string;
|
|
69
69
|
};
|
|
70
|
+
'no-property-alias': TSESLint.RuleModule<"aliasesProperty", [{
|
|
71
|
+
allowEnv: boolean;
|
|
72
|
+
}], unknown, TSESLint.RuleListener> & {
|
|
73
|
+
name: string;
|
|
74
|
+
};
|
|
75
|
+
'no-property-destructuring': TSESLint.RuleModule<"destructuresObject", [], unknown, TSESLint.RuleListener> & {
|
|
76
|
+
name: string;
|
|
77
|
+
};
|
|
78
|
+
'no-anonymous-param-type': TSESLint.RuleModule<"anonymousParamType", [{
|
|
79
|
+
minMembers: number;
|
|
80
|
+
}], unknown, TSESLint.RuleListener> & {
|
|
81
|
+
name: string;
|
|
82
|
+
};
|
|
70
83
|
};
|
|
71
84
|
type Plugin = {
|
|
72
85
|
meta: {
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
|
+
const no_anonymous_param_type_1 = __importDefault(require("./rules/no-anonymous-param-type"));
|
|
5
6
|
const max_class_dependencies_1 = __importDefault(require("./rules/max-class-dependencies"));
|
|
6
7
|
const max_class_fields_1 = __importDefault(require("./rules/max-class-fields"));
|
|
7
8
|
const max_returns_1 = __importDefault(require("./rules/max-returns"));
|
|
@@ -15,9 +16,15 @@ const no_interpolated_log_message_1 = __importDefault(require("./rules/no-interp
|
|
|
15
16
|
const no_logic_in_constructor_1 = __importDefault(require("./rules/no-logic-in-constructor"));
|
|
16
17
|
const no_null_1 = __importDefault(require("./rules/no-null"));
|
|
17
18
|
const no_null_return_1 = __importDefault(require("./rules/no-null-return"));
|
|
19
|
+
const no_property_alias_1 = __importDefault(require("./rules/no-property-alias"));
|
|
20
|
+
const no_property_destructuring_1 = __importDefault(require("./rules/no-property-destructuring"));
|
|
18
21
|
const no_public_mutable_props_1 = __importDefault(require("./rules/no-public-mutable-props"));
|
|
19
22
|
const no_static_members_1 = __importDefault(require("./rules/no-static-members"));
|
|
20
23
|
const no_type_assertion_1 = __importDefault(require("./rules/no-type-assertion"));
|
|
24
|
+
// require() of a JSON file yields `any`, so there is no honest type to reach
|
|
25
|
+
// for here. Importing it instead would put package.json inside the emitted
|
|
26
|
+
// tree and move dist/index.js, which the "exports" map pins.
|
|
27
|
+
// eslint-disable-next-line elegant/no-type-assertion
|
|
21
28
|
const { name, version } = require('../package.json');
|
|
22
29
|
const rules = {
|
|
23
30
|
'no-boolean-param': no_boolean_param_1.default,
|
|
@@ -36,6 +43,9 @@ const rules = {
|
|
|
36
43
|
'no-else-after-throw': no_else_after_throw_1.default,
|
|
37
44
|
'no-interpolated-log-message': no_interpolated_log_message_1.default,
|
|
38
45
|
'max-returns': max_returns_1.default,
|
|
46
|
+
'no-property-alias': no_property_alias_1.default,
|
|
47
|
+
'no-property-destructuring': no_property_destructuring_1.default,
|
|
48
|
+
'no-anonymous-param-type': no_anonymous_param_type_1.default,
|
|
39
49
|
};
|
|
40
50
|
const plugin = {
|
|
41
51
|
meta: { name, version },
|
|
@@ -62,6 +72,9 @@ plugin.configs.recommended = {
|
|
|
62
72
|
'elegant/no-comments-in-function-body': 'error',
|
|
63
73
|
'elegant/no-else-after-throw': 'error',
|
|
64
74
|
'elegant/no-interpolated-log-message': 'error',
|
|
75
|
+
'elegant/no-property-alias': 'error',
|
|
76
|
+
'elegant/no-property-destructuring': 'error',
|
|
77
|
+
'elegant/no-anonymous-param-type': ['error', { minMembers: 2 }],
|
|
65
78
|
'max-params': ['warn', { max: 3 }],
|
|
66
79
|
'no-else-return': ['error', { allowElseIf: false }],
|
|
67
80
|
},
|
|
@@ -39,9 +39,10 @@ const injected = (body, sourceCode) => {
|
|
|
39
39
|
};
|
|
40
40
|
const instantiated = (node, sourceCode) => {
|
|
41
41
|
const root = sourceCode.getText(node.callee);
|
|
42
|
-
const args = node.typeArguments;
|
|
43
42
|
return {
|
|
44
|
-
key: `${root}${
|
|
43
|
+
key: `${root}${node.typeArguments === undefined
|
|
44
|
+
? ''
|
|
45
|
+
: sourceCode.getText(node.typeArguments)}`,
|
|
45
46
|
root,
|
|
46
47
|
};
|
|
47
48
|
};
|
|
@@ -95,12 +96,11 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
95
96
|
if (dependencies.size <= max) {
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
98
|
-
const classNode = node.parent;
|
|
99
99
|
context.report({
|
|
100
|
-
node:
|
|
100
|
+
node: node.parent.id ?? node,
|
|
101
101
|
messageId: 'tooManyDependencies',
|
|
102
102
|
data: {
|
|
103
|
-
name:
|
|
103
|
+
name: node.parent.id?.name ?? '(anonymous)',
|
|
104
104
|
count: dependencies.size,
|
|
105
105
|
max,
|
|
106
106
|
names: [...dependencies].join(', '),
|
|
@@ -7,16 +7,16 @@ const isField = (member) => member.type === utils_1.AST_NODE_TYPES.PropertyDefin
|
|
|
7
7
|
member.type === utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition ||
|
|
8
8
|
member.type === utils_1.AST_NODE_TYPES.AccessorProperty;
|
|
9
9
|
const named = (key, sourceCode) => key.type === utils_1.AST_NODE_TYPES.Identifier ? key.name : sourceCode.getText(key);
|
|
10
|
+
/** Every property declared in the class body. */
|
|
11
|
+
const declared = (member, sourceCode) => isField(member) && !member.static ? [named(member.key, sourceCode)] : [];
|
|
10
12
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* The same, minus the decorated ones: `@Column`, `@IsString` and friends map a
|
|
14
|
+
* field to a table or a payload, which is framework shape rather than state the
|
|
15
|
+
* class chose to carry.
|
|
14
16
|
*/
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
? [named(member.key, sourceCode)]
|
|
19
|
-
: [];
|
|
17
|
+
const declaredUndecorated = (member, sourceCode) => isField(member) && member.decorators.length > 0
|
|
18
|
+
? []
|
|
19
|
+
: declared(member, sourceCode);
|
|
20
20
|
/**
|
|
21
21
|
* Constructor parameter properties. `ignoreDecorated` deliberately does not
|
|
22
22
|
* reach them: a decorator on a parameter is injection (`@Inject(TOKEN)`), so the
|
|
@@ -55,21 +55,21 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
55
55
|
defaultOptions: [{ max: DEFAULT_MAX, ignoreDecorated: true }],
|
|
56
56
|
create(context, [{ max, ignoreDecorated }]) {
|
|
57
57
|
const sourceCode = context.sourceCode;
|
|
58
|
+
const fieldsOf = ignoreDecorated ? declaredUndecorated : declared;
|
|
58
59
|
return {
|
|
59
60
|
ClassBody(node) {
|
|
60
61
|
const fields = node.body.flatMap((member) => [
|
|
61
|
-
...
|
|
62
|
+
...fieldsOf(member, sourceCode),
|
|
62
63
|
...promoted(member, sourceCode),
|
|
63
64
|
]);
|
|
64
65
|
if (fields.length <= max) {
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
67
|
-
const classNode = node.parent;
|
|
68
68
|
context.report({
|
|
69
|
-
node:
|
|
69
|
+
node: node.parent.id ?? node,
|
|
70
70
|
messageId: 'tooManyFields',
|
|
71
71
|
data: {
|
|
72
|
-
name:
|
|
72
|
+
name: node.parent.id?.name ?? '(anonymous)',
|
|
73
73
|
count: fields.length,
|
|
74
74
|
max,
|
|
75
75
|
names: fields.join(', '),
|
|
@@ -32,10 +32,9 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
32
32
|
if (methods.length <= max) {
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
|
-
const
|
|
36
|
-
const name = classNode.id?.name ?? '(anonymous)';
|
|
35
|
+
const name = node.parent.id?.name ?? '(anonymous)';
|
|
37
36
|
context.report({
|
|
38
|
-
node:
|
|
37
|
+
node: node.parent.id ?? node,
|
|
39
38
|
messageId: 'tooManyMethods',
|
|
40
39
|
data: { name, count: methods.length, max },
|
|
41
40
|
});
|
|
@@ -12,17 +12,16 @@ const nameOf = (node) => {
|
|
|
12
12
|
if (node.id !== null) {
|
|
13
13
|
return node.id.name;
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
parent.type === utils_1.AST_NODE_TYPES.
|
|
18
|
-
parent.
|
|
19
|
-
|
|
20
|
-
parent.key.
|
|
21
|
-
return parent.key.name;
|
|
15
|
+
if ((node.parent.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
|
|
16
|
+
node.parent.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
|
|
17
|
+
node.parent.type === utils_1.AST_NODE_TYPES.Property) &&
|
|
18
|
+
!node.parent.computed &&
|
|
19
|
+
node.parent.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
20
|
+
return node.parent.key.name;
|
|
22
21
|
}
|
|
23
|
-
if (parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
24
|
-
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
25
|
-
return parent.id.name;
|
|
22
|
+
if (node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
23
|
+
node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
24
|
+
return node.parent.id.name;
|
|
26
25
|
}
|
|
27
26
|
return '(anonymous)';
|
|
28
27
|
};
|
|
@@ -58,14 +57,13 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
58
57
|
if (scope === undefined || scope.count <= max) {
|
|
59
58
|
return;
|
|
60
59
|
}
|
|
61
|
-
const
|
|
62
|
-
const signature = node.id ?? sourceCode.getFirstToken(node);
|
|
60
|
+
const signature = scope.node.id ?? sourceCode.getFirstToken(scope.node);
|
|
63
61
|
context.report({
|
|
64
|
-
loc: (signature ?? node).loc,
|
|
62
|
+
loc: (signature ?? scope.node).loc,
|
|
65
63
|
messageId: 'tooManyReturns',
|
|
66
64
|
data: {
|
|
67
|
-
name: nameOf(node),
|
|
68
|
-
count,
|
|
65
|
+
name: nameOf(scope.node),
|
|
66
|
+
count: scope.count,
|
|
69
67
|
max,
|
|
70
68
|
},
|
|
71
69
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type Options = [{
|
|
2
|
+
minMembers: number;
|
|
3
|
+
}];
|
|
4
|
+
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"anonymousParamType", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,120 @@
|
|
|
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 DEFAULT_MIN_MEMBERS = 2;
|
|
6
|
+
const bindingOf = (param) => {
|
|
7
|
+
if (param.type === utils_1.AST_NODE_TYPES.TSParameterProperty) {
|
|
8
|
+
return bindingOf(param.parameter);
|
|
9
|
+
}
|
|
10
|
+
const target = param.type === utils_1.AST_NODE_TYPES.AssignmentPattern ? param.left : param;
|
|
11
|
+
switch (target.type) {
|
|
12
|
+
case utils_1.AST_NODE_TYPES.ArrayPattern:
|
|
13
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
14
|
+
case utils_1.AST_NODE_TYPES.ObjectPattern:
|
|
15
|
+
case utils_1.AST_NODE_TYPES.RestElement:
|
|
16
|
+
return target;
|
|
17
|
+
default:
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const nameOf = (binding) => binding.type === utils_1.AST_NODE_TYPES.Identifier ? binding.name : '(destructured)';
|
|
22
|
+
/** The type nodes a shape can hide inside, one level down. */
|
|
23
|
+
const reachableFrom = (node) => {
|
|
24
|
+
switch (node.type) {
|
|
25
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference:
|
|
26
|
+
return node.typeArguments?.params ?? [];
|
|
27
|
+
case utils_1.AST_NODE_TYPES.TSUnionType:
|
|
28
|
+
case utils_1.AST_NODE_TYPES.TSIntersectionType:
|
|
29
|
+
return node.types;
|
|
30
|
+
case utils_1.AST_NODE_TYPES.TSArrayType:
|
|
31
|
+
return [node.elementType];
|
|
32
|
+
default:
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The first anonymous shape of at least `minMembers` properties reachable from
|
|
38
|
+
* a type annotation. Shapes hide behind generic arguments as readily as they
|
|
39
|
+
* sit in the open — `Array<{ day; count }>` is as unnamed as `{ day; count }`.
|
|
40
|
+
*/
|
|
41
|
+
const shapeIn = (node, minMembers) => {
|
|
42
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
43
|
+
return node.members.length >= minMembers ? node : undefined;
|
|
44
|
+
}
|
|
45
|
+
for (const inner of reachableFrom(node)) {
|
|
46
|
+
const shape = shapeIn(inner, minMembers);
|
|
47
|
+
if (shape !== undefined) {
|
|
48
|
+
return shape;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Whether the signature is a function handed straight to a call. Such a
|
|
55
|
+
* parameter annotates whatever the callee yields — often an untyped response
|
|
56
|
+
* body — so an inline shape is the only way to type it at all.
|
|
57
|
+
*/
|
|
58
|
+
const isInlineCallback = (node) => (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
59
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
|
|
60
|
+
node.parent.type === utils_1.AST_NODE_TYPES.CallExpression;
|
|
61
|
+
exports.default = (0, createRule_1.createRule)({
|
|
62
|
+
name: 'no-anonymous-param-type',
|
|
63
|
+
meta: {
|
|
64
|
+
type: 'suggestion',
|
|
65
|
+
docs: {
|
|
66
|
+
description: 'Disallow parameters typed as an anonymous shape. Name the shape so it can carry behaviour instead of being a bag.',
|
|
67
|
+
},
|
|
68
|
+
messages: {
|
|
69
|
+
anonymousParamType: "Parameter '{{name}}' is typed as an anonymous shape of {{count}} properties. Name the shape so it can carry behaviour instead of being a bag.",
|
|
70
|
+
},
|
|
71
|
+
schema: [
|
|
72
|
+
{
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
minMembers: { type: 'integer', minimum: 1 },
|
|
76
|
+
},
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
defaultOptions: [{ minMembers: DEFAULT_MIN_MEMBERS }],
|
|
82
|
+
create(context, [{ minMembers }]) {
|
|
83
|
+
const check = (node) => {
|
|
84
|
+
if (isInlineCallback(node)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
for (const param of node.params) {
|
|
88
|
+
const binding = bindingOf(param);
|
|
89
|
+
if (binding === undefined) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const annotation = binding.typeAnnotation?.typeAnnotation;
|
|
93
|
+
if (annotation === undefined) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const shape = shapeIn(annotation, minMembers);
|
|
97
|
+
if (shape === undefined) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
context.report({
|
|
101
|
+
node: param,
|
|
102
|
+
messageId: 'anonymousParamType',
|
|
103
|
+
data: {
|
|
104
|
+
name: nameOf(binding),
|
|
105
|
+
count: shape.members.length,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
ArrowFunctionExpression: check,
|
|
112
|
+
FunctionDeclaration: check,
|
|
113
|
+
FunctionExpression: check,
|
|
114
|
+
TSDeclareFunction: check,
|
|
115
|
+
TSEmptyBodyFunctionExpression: check,
|
|
116
|
+
TSFunctionType: check,
|
|
117
|
+
TSMethodSignature: check,
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
});
|
|
@@ -64,14 +64,13 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
64
64
|
const levels = new Set([...METHODS, ...methods]);
|
|
65
65
|
return {
|
|
66
66
|
CallExpression(node) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
callee.
|
|
70
|
-
callee.property.
|
|
71
|
-
!levels.has(callee.property.name)) {
|
|
67
|
+
if (node.callee.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
68
|
+
node.callee.computed ||
|
|
69
|
+
node.callee.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
70
|
+
!levels.has(node.callee.property.name)) {
|
|
72
71
|
return;
|
|
73
72
|
}
|
|
74
|
-
const name = receiver(callee.object);
|
|
73
|
+
const name = receiver(node.callee.object);
|
|
75
74
|
if (name === undefined || !logging.has(name)) {
|
|
76
75
|
return;
|
|
77
76
|
}
|
|
@@ -49,19 +49,18 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
49
49
|
context.report({ node: statement, messageId: 'statement' });
|
|
50
50
|
continue;
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
if (isSuperCall(expression)) {
|
|
52
|
+
if (isSuperCall(statement.expression)) {
|
|
54
53
|
continue;
|
|
55
54
|
}
|
|
56
|
-
if (expression.type !== utils_1.AST_NODE_TYPES.AssignmentExpression ||
|
|
57
|
-
expression.operator !== '=' ||
|
|
58
|
-
!isThisMember(expression.left)) {
|
|
55
|
+
if (statement.expression.type !== utils_1.AST_NODE_TYPES.AssignmentExpression ||
|
|
56
|
+
statement.expression.operator !== '=' ||
|
|
57
|
+
!isThisMember(statement.expression.left)) {
|
|
59
58
|
context.report({ node: statement, messageId: 'statement' });
|
|
60
59
|
continue;
|
|
61
60
|
}
|
|
62
|
-
if (!isPlainValue(expression.right)) {
|
|
61
|
+
if (!isPlainValue(statement.expression.right)) {
|
|
63
62
|
context.report({
|
|
64
|
-
node: expression.right,
|
|
63
|
+
node: statement.expression.right,
|
|
65
64
|
messageId: 'computation',
|
|
66
65
|
});
|
|
67
66
|
}
|
|
@@ -18,10 +18,9 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
18
18
|
create(context) {
|
|
19
19
|
return {
|
|
20
20
|
ReturnStatement(node) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
argument.
|
|
24
|
-
argument.raw === 'null') {
|
|
21
|
+
if (node.argument?.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
22
|
+
node.argument.value === null &&
|
|
23
|
+
node.argument.raw === 'null') {
|
|
25
24
|
context.report({ node, messageId: 'noNullReturn' });
|
|
26
25
|
}
|
|
27
26
|
},
|
package/dist/rules/no-null.js
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const utils_1 = require("@typescript-eslint/utils");
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
/**
|
|
6
|
+
* A `null` sitting directly in a `return`. That shape is the domain of
|
|
7
|
+
* `no-null-return`, which reports it with its own message; reporting it here
|
|
8
|
+
* too would double up on one line of code.
|
|
9
|
+
*/
|
|
10
|
+
const isDirectReturn = (node) => node.parent.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
|
|
11
|
+
node.parent.argument === node;
|
|
5
12
|
exports.default = (0, createRule_1.createRule)({
|
|
6
13
|
name: 'no-null',
|
|
7
14
|
meta: {
|
|
@@ -21,10 +28,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
21
28
|
if (node.value !== null || node.raw !== 'null') {
|
|
22
29
|
return;
|
|
23
30
|
}
|
|
24
|
-
|
|
25
|
-
const { parent } = node;
|
|
26
|
-
if (parent.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
|
|
27
|
-
parent.argument === node) {
|
|
31
|
+
if (isDirectReturn(node)) {
|
|
28
32
|
return;
|
|
29
33
|
}
|
|
30
34
|
context.report({ node, messageId: 'noNull' });
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type Options = [{
|
|
2
|
+
allowEnv: boolean;
|
|
3
|
+
}];
|
|
4
|
+
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"aliasesProperty", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,67 @@
|
|
|
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 locals_1 = require("../utils/locals");
|
|
6
|
+
const memberChain_1 = require("../utils/memberChain");
|
|
7
|
+
const DEFAULT_ALLOW_ENV = true;
|
|
8
|
+
/** Whether the chain reads `process.env.SOMETHING`. */
|
|
9
|
+
const isEnvRead = (node) => node.object.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
10
|
+
!node.object.computed &&
|
|
11
|
+
node.object.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
12
|
+
node.object.object.name === 'process' &&
|
|
13
|
+
node.object.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
14
|
+
node.object.property.name === 'env';
|
|
15
|
+
exports.default = (0, createRule_1.createRule)({
|
|
16
|
+
name: 'no-property-alias',
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'suggestion',
|
|
19
|
+
docs: {
|
|
20
|
+
description: 'Disallow locals that only rename a property of an object already in hand. Ask the object where the value is needed instead.',
|
|
21
|
+
},
|
|
22
|
+
messages: {
|
|
23
|
+
aliasesProperty: "'{{name}}' only renames '{{path}}'. Ask the object where you need the value instead of copying its state into a local.",
|
|
24
|
+
},
|
|
25
|
+
schema: [
|
|
26
|
+
{
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
allowEnv: { type: 'boolean' },
|
|
30
|
+
},
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
defaultOptions: [{ allowEnv: DEFAULT_ALLOW_ENV }],
|
|
36
|
+
create(context, [{ allowEnv }]) {
|
|
37
|
+
return {
|
|
38
|
+
VariableDeclarator(node) {
|
|
39
|
+
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (node.init === null ||
|
|
43
|
+
node.init.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
44
|
+
!(0, memberChain_1.isObjectInHand)(node.init)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (allowEnv && isEnvRead(node.init)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const [variable] = context.sourceCode.getDeclaredVariables(node);
|
|
51
|
+
if (variable === undefined ||
|
|
52
|
+
(0, locals_1.isReassigned)(variable) ||
|
|
53
|
+
(0, locals_1.escapesIntoFunction)(variable)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
context.report({
|
|
57
|
+
node,
|
|
58
|
+
messageId: 'aliasesProperty',
|
|
59
|
+
data: {
|
|
60
|
+
name: node.id.name,
|
|
61
|
+
path: context.sourceCode.getText(node.init),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
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 locals_1 = require("../utils/locals");
|
|
6
|
+
const memberChain_1 = require("../utils/memberChain");
|
|
7
|
+
/**
|
|
8
|
+
* Whether the pattern collects the properties it does not name into a rest
|
|
9
|
+
* object. That is construction by omission, not a copy of state anyone can
|
|
10
|
+
* replace with a member access.
|
|
11
|
+
*/
|
|
12
|
+
const hasRestElement = (node) => node.properties.some((property) => property.type === utils_1.AST_NODE_TYPES.RestElement);
|
|
13
|
+
/**
|
|
14
|
+
* Whether any property carries a default. `options.max ?? 3` repeats the
|
|
15
|
+
* fallback at every use site, so the pattern is holding a decision rather than
|
|
16
|
+
* just a copy.
|
|
17
|
+
*/
|
|
18
|
+
const hasDefault = (node) => node.properties.some((property) => property.type === utils_1.AST_NODE_TYPES.Property &&
|
|
19
|
+
property.value.type === utils_1.AST_NODE_TYPES.AssignmentPattern);
|
|
20
|
+
exports.default = (0, createRule_1.createRule)({
|
|
21
|
+
name: 'no-property-destructuring',
|
|
22
|
+
meta: {
|
|
23
|
+
type: 'suggestion',
|
|
24
|
+
docs: {
|
|
25
|
+
description: 'Disallow destructuring an object already in hand. Ask the object where each value is needed instead.',
|
|
26
|
+
},
|
|
27
|
+
messages: {
|
|
28
|
+
destructuresObject: "Destructuring '{{name}}' copies its state into {{count}} locals. Ask the object where you need each value instead.",
|
|
29
|
+
},
|
|
30
|
+
schema: [],
|
|
31
|
+
},
|
|
32
|
+
defaultOptions: [],
|
|
33
|
+
create(context) {
|
|
34
|
+
return {
|
|
35
|
+
ObjectPattern(node) {
|
|
36
|
+
if (node.parent.type !== utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
37
|
+
node.parent.init === null ||
|
|
38
|
+
!(0, memberChain_1.isObjectInHand)(node.parent.init)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (hasRestElement(node) || hasDefault(node)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const variables = context.sourceCode.getDeclaredVariables(node.parent);
|
|
45
|
+
if (variables.some((variable) => (0, locals_1.isReassigned)(variable) || (0, locals_1.escapesIntoFunction)(variable))) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
context.report({
|
|
49
|
+
node: node.parent,
|
|
50
|
+
messageId: 'destructuresObject',
|
|
51
|
+
data: {
|
|
52
|
+
name: context.sourceCode.getText(node.parent.init),
|
|
53
|
+
count: node.properties.length,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
});
|
|
@@ -12,6 +12,12 @@ const keyName = (key) => {
|
|
|
12
12
|
}
|
|
13
13
|
return 'property';
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* A constructor parameter property the outside world can write to. Only
|
|
17
|
+
* constructor parameter properties carry an accessibility modifier, so reading
|
|
18
|
+
* one is enough to know the field is public.
|
|
19
|
+
*/
|
|
20
|
+
const isPublicMutable = (node) => node.accessibility === 'public' && !node.readonly;
|
|
15
21
|
exports.default = (0, createRule_1.createRule)({
|
|
16
22
|
name: 'no-public-mutable-props',
|
|
17
23
|
meta: {
|
|
@@ -38,9 +44,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
38
44
|
});
|
|
39
45
|
},
|
|
40
46
|
TSParameterProperty(node) {
|
|
41
|
-
|
|
42
|
-
if (node.accessibility !== 'public' ||
|
|
43
|
-
node.readonly) {
|
|
47
|
+
if (!isPublicMutable(node)) {
|
|
44
48
|
return;
|
|
45
49
|
}
|
|
46
50
|
const target = node.parameter.type === utils_1.AST_NODE_TYPES.AssignmentPattern
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Whether anything writes to the variable after its declaration. Such a local
|
|
4
|
+
* holds mutable state that no member access stands in for, so it is not a copy
|
|
5
|
+
* of the object's state to begin with.
|
|
6
|
+
*/
|
|
7
|
+
export declare const isReassigned: (variable: TSESLint.Scope.Variable) => boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the variable is read from inside a nested function. TypeScript drops
|
|
10
|
+
* a narrowing of `obj.prop` at the callback boundary but keeps it on a local,
|
|
11
|
+
* so such a declaration is load-bearing: inlining it stops compiling.
|
|
12
|
+
*/
|
|
13
|
+
export declare const escapesIntoFunction: (variable: TSESLint.Scope.Variable) => boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.escapesIntoFunction = exports.isReassigned = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Whether anything writes to the variable after its declaration. Such a local
|
|
6
|
+
* holds mutable state that no member access stands in for, so it is not a copy
|
|
7
|
+
* of the object's state to begin with.
|
|
8
|
+
*/
|
|
9
|
+
const isReassigned = (variable) => variable.references.some((reference) => reference.isWrite() && !reference.init);
|
|
10
|
+
exports.isReassigned = isReassigned;
|
|
11
|
+
/**
|
|
12
|
+
* Whether the variable is read from inside a nested function. TypeScript drops
|
|
13
|
+
* a narrowing of `obj.prop` at the callback boundary but keeps it on a local,
|
|
14
|
+
* so such a declaration is load-bearing: inlining it stops compiling.
|
|
15
|
+
*/
|
|
16
|
+
const escapesIntoFunction = (variable) => variable.references.some((reference) => {
|
|
17
|
+
let scope = reference.from;
|
|
18
|
+
while (scope !== null && scope !== variable.scope) {
|
|
19
|
+
if (scope.type === 'function') {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
scope = scope.upper;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
});
|
|
26
|
+
exports.escapesIntoFunction = escapesIntoFunction;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Whether the expression denotes an object the reader already holds: a name,
|
|
4
|
+
* `this`, or a run of plain `.prop` accesses rooted at one of those. A computed
|
|
5
|
+
* link (`calls[1][0].metadata`) or a call in the middle
|
|
6
|
+
* (`resolveDates(query).startDate`) fails — nobody gains by inlining those.
|
|
7
|
+
*/
|
|
8
|
+
export declare const isObjectInHand: (node: TSESTree.Node) => boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isObjectInHand = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
/**
|
|
6
|
+
* Whether the expression denotes an object the reader already holds: a name,
|
|
7
|
+
* `this`, or a run of plain `.prop` accesses rooted at one of those. A computed
|
|
8
|
+
* link (`calls[1][0].metadata`) or a call in the middle
|
|
9
|
+
* (`resolveDates(query).startDate`) fails — nobody gains by inlining those.
|
|
10
|
+
*/
|
|
11
|
+
const isObjectInHand = (node) => {
|
|
12
|
+
let current = node;
|
|
13
|
+
while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
14
|
+
if (current.computed) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
current = current.object;
|
|
18
|
+
}
|
|
19
|
+
return (current.type === utils_1.AST_NODE_TYPES.Identifier ||
|
|
20
|
+
current.type === utils_1.AST_NODE_TYPES.ThisExpression);
|
|
21
|
+
};
|
|
22
|
+
exports.isObjectInHand = isObjectInHand;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tianjos/eslint-plugin-elegant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsc -p tsconfig.json",
|
|
44
|
+
"lint": "npm run build && eslint .",
|
|
44
45
|
"typecheck": "tsc -p tsconfig.test.json",
|
|
45
46
|
"test": "jest",
|
|
46
47
|
"release": "standard-version",
|