@ethlete/eslint-plugin 1.0.0-next.23 → 1.0.0-next.24
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/CHANGELOG.md +12 -0
- package/package.json +2 -2
- package/src/configs/recommended.js +3 -0
- package/src/index.js +2 -0
- package/src/rules/angular-decorator-property-order.js +20 -20
- package/src/rules/class-constant-property.js +8 -14
- package/src/rules/class-member-order.js +77 -46
- package/src/rules/inject-member-accessibility.js +5 -3
- package/src/rules/internals/angular-io.js +12 -13
- package/src/rules/internals/angular-member-visibility.js +11 -7
- package/src/rules/internals/import-resolution.js +191 -0
- package/src/rules/no-angular-router-api.js +23 -29
- package/src/rules/no-angular-seo-services.js +14 -9
- package/src/rules/no-cdk-import.js +7 -14
- package/src/rules/no-document-cookie.js +3 -0
- package/src/rules/no-effect-cleanup-return.js +7 -3
- package/src/rules/no-empty-angular-metadata-arrays.js +3 -17
- package/src/rules/no-inject-chain.js +3 -1
- package/src/rules/no-leading-underscore-class-member.js +26 -3
- package/src/rules/no-legacy-angular-decorators.js +9 -21
- package/src/rules/no-legacy-query-import.js +6 -15
- package/src/rules/no-locale-id.js +4 -4
- package/src/rules/no-member-alias.js +11 -7
- package/src/rules/no-native-html-input-name.js +1 -1
- package/src/rules/no-pipe-logic.js +10 -17
- package/src/rules/no-redundant-on-push-change-detection.js +9 -23
- package/src/rules/no-rxjs-in-effect.js +12 -8
- package/src/rules/no-screaming-case-local.js +1 -0
- package/src/rules/no-standalone-flag.js +3 -17
- package/src/rules/no-subscribe-in-pipe.js +3 -2
- package/src/rules/no-template-literal-before-inline-template.js +3 -14
- package/src/rules/no-trivial-return-type.js +1 -0
- package/src/rules/no-trivial-wrapper-method.js +10 -16
- package/src/rules/no-typed-injected-element-ref.js +14 -17
- package/src/rules/no-unused-class-member.js +6 -9
- package/src/rules/prefer-clone-equal.js +8 -6
- package/src/rules/prefer-concise-angular-host-directives.js +23 -29
- package/src/rules/prefer-concise-angular-style-metadata.js +8 -16
- package/src/rules/prefer-element-dimensions.js +9 -10
- package/src/rules/prefer-linked-signal.js +7 -4
- package/src/rules/prefer-match-media.js +10 -14
- package/src/rules/prefer-present-tense-output.js +1 -1
- package/src/rules/prefer-rxjs-timer.js +3 -1
- package/src/rules/require-dollar-suffix.js +7 -4
- package/src/rules/require-on-push-change-detection.js +17 -25
- package/src/rules/require-view-encapsulation-none.js +17 -25
- package/src/rules/take-until-destroyed-last.js +64 -0
- package/src/rules/template-member-accessibility.js +8 -6
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* const locale = injectLocale();
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
const { isImportedAs } = require('./internals/import-resolution');
|
|
18
|
+
|
|
17
19
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
18
20
|
const noLocaleId = {
|
|
19
21
|
meta: {
|
|
@@ -31,10 +33,8 @@ const noLocaleId = {
|
|
|
31
33
|
create(context) {
|
|
32
34
|
return {
|
|
33
35
|
CallExpression(node) {
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
const arg = node.arguments[0];
|
|
37
|
-
if (arg?.type === 'Identifier' && arg.name === 'LOCALE_ID') {
|
|
36
|
+
if (!isImportedAs(context.sourceCode, node.callee, 'inject')) return;
|
|
37
|
+
if (isImportedAs(context.sourceCode, node.arguments[0], 'LOCALE_ID')) {
|
|
38
38
|
context.report({ node, messageId: 'noLocaleId' });
|
|
39
39
|
}
|
|
40
40
|
},
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { getAngularDecoratorName } = require('./internals/import-resolution');
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Disallows class members that exist solely as aliases for a nested property or
|
|
6
8
|
* zero-arg method of another class member accessed via `this`.
|
|
@@ -81,12 +83,14 @@ const getMethodAliasInfo = (node) => {
|
|
|
81
83
|
|
|
82
84
|
const CONTRACT_METHOD_NAMES = new Set(['blur', 'focus', 'reset']);
|
|
83
85
|
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
/**
|
|
87
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
88
|
+
* @param {any} methodNode
|
|
89
|
+
*/
|
|
90
|
+
const isInAngularClass = (sourceCode, methodNode) =>
|
|
91
|
+
methodNode.parent?.parent?.decorators?.some((/** @type {any} */ decorator) => {
|
|
92
|
+
const name = getAngularDecoratorName(sourceCode, decorator);
|
|
93
|
+
return name === 'Component' || name === 'Directive';
|
|
90
94
|
}) ?? false;
|
|
91
95
|
|
|
92
96
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
@@ -149,7 +153,7 @@ const noMemberAlias = {
|
|
|
149
153
|
if (!info) return;
|
|
150
154
|
const classNode = node.parent?.parent;
|
|
151
155
|
if (node.override || classNode?.superClass || classNode?.implements?.length) return;
|
|
152
|
-
if (isInAngularClass(node) && CONTRACT_METHOD_NAMES.has(aliasName)) return;
|
|
156
|
+
if (isInAngularClass(context.sourceCode, node) && CONTRACT_METHOD_NAMES.has(aliasName)) return;
|
|
153
157
|
|
|
154
158
|
// Only flag when names match — different names indicate an intentional
|
|
155
159
|
// API rename, which is not a pure alias.
|
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { getAngularDecoratorName } = require('./internals/import-resolution');
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
|
-
*
|
|
6
|
-
* @param {
|
|
7
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
8
|
+
* @param {any} classNode
|
|
7
9
|
*/
|
|
8
|
-
const hasPipeDecorator = (classNode) =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (expr.type === 'CallExpression') {
|
|
13
|
-
return expr.callee.type === 'Identifier' && expr.callee.name === 'Pipe';
|
|
14
|
-
}
|
|
15
|
-
if (expr.type === 'Identifier') {
|
|
16
|
-
return expr.name === 'Pipe';
|
|
17
|
-
}
|
|
18
|
-
return false;
|
|
19
|
-
});
|
|
20
|
-
};
|
|
10
|
+
const hasPipeDecorator = (sourceCode, classNode) =>
|
|
11
|
+
(classNode.decorators ?? []).some(
|
|
12
|
+
(/** @type {any} */ decorator) => getAngularDecoratorName(sourceCode, decorator) === 'Pipe',
|
|
13
|
+
);
|
|
21
14
|
|
|
22
15
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
23
16
|
const noPipeLogic = {
|
|
@@ -47,7 +40,7 @@ const noPipeLogic = {
|
|
|
47
40
|
const classNode = classBody.parent;
|
|
48
41
|
if (!classNode || (classNode.type !== 'ClassDeclaration' && classNode.type !== 'ClassExpression')) return;
|
|
49
42
|
|
|
50
|
-
if (!hasPipeDecorator(classNode)) return;
|
|
43
|
+
if (!hasPipeDecorator(context.sourceCode, classNode)) return;
|
|
51
44
|
|
|
52
45
|
context.report({ node, messageId: 'noLogicInTransform' });
|
|
53
46
|
},
|
|
@@ -63,7 +56,7 @@ const noPipeLogic = {
|
|
|
63
56
|
const classNode = classBody.parent;
|
|
64
57
|
if (!classNode || (classNode.type !== 'ClassDeclaration' && classNode.type !== 'ClassExpression')) return;
|
|
65
58
|
|
|
66
|
-
if (!hasPipeDecorator(classNode)) return;
|
|
59
|
+
if (!hasPipeDecorator(context.sourceCode, classNode)) return;
|
|
67
60
|
|
|
68
61
|
const value = /** @type {any} */ (node).value;
|
|
69
62
|
if (value && (value.type === 'ArrowFunctionExpression' || value.type === 'FunctionExpression')) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { getAngularDecoratorName, isImportedAs } = require('./internals/import-resolution');
|
|
5
|
+
|
|
4
6
|
const { getMetadataEntryRemovalRange } = require('./internals/angular-metadata-fix');
|
|
5
7
|
|
|
6
8
|
/**
|
|
@@ -70,29 +72,15 @@ const getPropertyName = (key) => {
|
|
|
70
72
|
};
|
|
71
73
|
|
|
72
74
|
/**
|
|
73
|
-
* @param {import('eslint').
|
|
74
|
-
*/
|
|
75
|
-
const isComponentDecorator = (node) => {
|
|
76
|
-
const decorator = /** @type {any} */ (node);
|
|
77
|
-
if (decorator.type !== 'Decorator') return false;
|
|
78
|
-
|
|
79
|
-
const expression = decorator.expression;
|
|
80
|
-
if (expression.type === 'CallExpression') {
|
|
81
|
-
return expression.callee.type === 'Identifier' && expression.callee.name === 'Component';
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return expression.type === 'Identifier' && expression.name === 'Component';
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/**
|
|
75
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
88
76
|
* @param {any} value
|
|
89
77
|
*/
|
|
90
|
-
const isOnPushValue = (
|
|
78
|
+
const isOnPushValue = (sourceCode, value) =>
|
|
91
79
|
value.type === 'MemberExpression' &&
|
|
92
|
-
value.
|
|
93
|
-
value.object.name === localName &&
|
|
80
|
+
!value.computed &&
|
|
94
81
|
value.property.type === 'Identifier' &&
|
|
95
|
-
value.property.name === 'OnPush'
|
|
82
|
+
value.property.name === 'OnPush' &&
|
|
83
|
+
isImportedAs(sourceCode, value.object, 'ChangeDetectionStrategy');
|
|
96
84
|
|
|
97
85
|
/**
|
|
98
86
|
* Range that removes a single named import specifier together with its comma.
|
|
@@ -159,12 +147,11 @@ const noRedundantOnPushChangeDetection = {
|
|
|
159
147
|
const sourceCode = context.sourceCode;
|
|
160
148
|
/** @type {any} */
|
|
161
149
|
let changeDetectionSpecifier = null;
|
|
162
|
-
let changeDetectionLocalName = 'ChangeDetectionStrategy';
|
|
163
150
|
const removableIdentifiers = new Set();
|
|
164
151
|
|
|
165
152
|
return {
|
|
166
153
|
Decorator(node) {
|
|
167
|
-
if (
|
|
154
|
+
if (getAngularDecoratorName(sourceCode, node) !== 'Component') return;
|
|
168
155
|
|
|
169
156
|
const expression = /** @type {any} */ (node).expression;
|
|
170
157
|
if (expression.type !== 'CallExpression' || expression.arguments.length === 0) return;
|
|
@@ -175,7 +162,7 @@ const noRedundantOnPushChangeDetection = {
|
|
|
175
162
|
const property = metadata.properties.find(
|
|
176
163
|
(entry) => entry.type === 'Property' && getPropertyName(entry.key) === 'changeDetection',
|
|
177
164
|
);
|
|
178
|
-
if (!property || !isOnPushValue(property.value
|
|
165
|
+
if (!property || !isOnPushValue(sourceCode, property.value)) return;
|
|
179
166
|
removableIdentifiers.add(property.value.object);
|
|
180
167
|
|
|
181
168
|
context.report({
|
|
@@ -195,7 +182,6 @@ const noRedundantOnPushChangeDetection = {
|
|
|
195
182
|
specifier.imported.name === 'ChangeDetectionStrategy'
|
|
196
183
|
) {
|
|
197
184
|
changeDetectionSpecifier = specifier;
|
|
198
|
-
changeDetectionLocalName = specifier.local.name;
|
|
199
185
|
return;
|
|
200
186
|
}
|
|
201
187
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { ANGULAR_CORE, getImportedName } = require('./internals/import-resolution');
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Disallows calling .subscribe() inside Angular's effect() or computed() callbacks.
|
|
6
8
|
*
|
|
@@ -22,6 +24,11 @@
|
|
|
22
24
|
* .subscribe();
|
|
23
25
|
*/
|
|
24
26
|
|
|
27
|
+
/**
|
|
28
|
+
* @param {any} node
|
|
29
|
+
*/
|
|
30
|
+
const isStringLiteral = (node) => node?.type === 'Literal' && typeof node.value === 'string';
|
|
31
|
+
|
|
25
32
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
26
33
|
const noRxjsInEffect = {
|
|
27
34
|
meta: {
|
|
@@ -50,13 +57,9 @@ const noRxjsInEffect = {
|
|
|
50
57
|
if (current.type === 'ArrowFunctionExpression' || current.type === 'FunctionExpression') {
|
|
51
58
|
crossedFunctionBoundary = true;
|
|
52
59
|
}
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
current.callee.type === 'Identifier' &&
|
|
57
|
-
(current.callee.name === 'effect' || current.callee.name === 'computed')
|
|
58
|
-
) {
|
|
59
|
-
return current.callee.name;
|
|
60
|
+
if (crossedFunctionBoundary && current.type === 'CallExpression') {
|
|
61
|
+
const calleeName = getImportedName(context.sourceCode, current.callee, ANGULAR_CORE);
|
|
62
|
+
if (calleeName === 'effect' || calleeName === 'computed') return calleeName;
|
|
60
63
|
}
|
|
61
64
|
current = current.parent;
|
|
62
65
|
}
|
|
@@ -69,7 +72,8 @@ const noRxjsInEffect = {
|
|
|
69
72
|
if (
|
|
70
73
|
callee.type === 'MemberExpression' &&
|
|
71
74
|
callee.property.type === 'Identifier' &&
|
|
72
|
-
callee.property.name === 'subscribe'
|
|
75
|
+
callee.property.name === 'subscribe' &&
|
|
76
|
+
!isStringLiteral(node.arguments[0])
|
|
73
77
|
) {
|
|
74
78
|
const reactiveContext = getReactiveContext(node);
|
|
75
79
|
if (reactiveContext) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { getAngularDecoratorName } = require('./internals/import-resolution');
|
|
5
|
+
|
|
4
6
|
const { getMetadataEntryRemovalRange } = require('./internals/angular-metadata-fix');
|
|
5
7
|
|
|
6
8
|
/** @typedef {'Component' | 'Directive' | 'Pipe'} TDecoratorName */
|
|
@@ -14,22 +16,6 @@ const getPropertyName = (key) => {
|
|
|
14
16
|
return null;
|
|
15
17
|
};
|
|
16
18
|
|
|
17
|
-
/**
|
|
18
|
-
* @param {import('eslint').Rule.Node} node
|
|
19
|
-
* @returns {TDecoratorName | null}
|
|
20
|
-
*/
|
|
21
|
-
const getDecoratorName = (node) => {
|
|
22
|
-
const decorator = /** @type {any} */ (node);
|
|
23
|
-
if (decorator.type !== 'Decorator') return null;
|
|
24
|
-
|
|
25
|
-
const expression = decorator.expression;
|
|
26
|
-
if (expression.type === 'CallExpression') {
|
|
27
|
-
return expression.callee.type === 'Identifier' ? expression.callee.name : null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return expression.type === 'Identifier' ? expression.name : null;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
19
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
34
20
|
const noStandaloneFlag = {
|
|
35
21
|
meta: {
|
|
@@ -49,7 +35,7 @@ const noStandaloneFlag = {
|
|
|
49
35
|
|
|
50
36
|
return {
|
|
51
37
|
Decorator(node) {
|
|
52
|
-
const decoratorName =
|
|
38
|
+
const decoratorName = getAngularDecoratorName(context.sourceCode, node);
|
|
53
39
|
if (decoratorName !== 'Component' && decoratorName !== 'Directive' && decoratorName !== 'Pipe') return;
|
|
54
40
|
|
|
55
41
|
const decorator = /** @type {any} */ (node);
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* obs$.pipe(...operators).subscribe(); // ✅ subscribe on the result of pipe
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
const { isImportedAs } = require('./internals/import-resolution');
|
|
23
|
+
|
|
22
24
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
23
25
|
const noSubscribeInPipe = {
|
|
24
26
|
meta: {
|
|
@@ -48,8 +50,7 @@ const noSubscribeInPipe = {
|
|
|
48
50
|
// be flagged even if the Observable itself is passed into a .pipe() operator.
|
|
49
51
|
if (
|
|
50
52
|
current.type === 'NewExpression' &&
|
|
51
|
-
current.callee
|
|
52
|
-
current.callee.name === 'Observable'
|
|
53
|
+
isImportedAs(context.sourceCode, current.callee, 'Observable', 'rxjs')
|
|
53
54
|
) {
|
|
54
55
|
return false;
|
|
55
56
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { getAngularDecoratorName } = require('./internals/import-resolution');
|
|
5
|
+
|
|
4
6
|
const ts = require('typescript');
|
|
5
7
|
|
|
6
8
|
/**
|
|
@@ -92,19 +94,6 @@ const getPropertyName = (key) => {
|
|
|
92
94
|
return null;
|
|
93
95
|
};
|
|
94
96
|
|
|
95
|
-
/**
|
|
96
|
-
* @param {any} node
|
|
97
|
-
*/
|
|
98
|
-
const getDecoratorName = (node) => {
|
|
99
|
-
const expression = node.expression;
|
|
100
|
-
if (!expression) return null;
|
|
101
|
-
if (expression.type === 'CallExpression') {
|
|
102
|
-
return expression.callee.type === 'Identifier' ? expression.callee.name : null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return expression.type === 'Identifier' ? expression.name : null;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
97
|
/**
|
|
109
98
|
* The inline template text, or null when the property is not a plain non-empty string literal.
|
|
110
99
|
*
|
|
@@ -147,7 +136,7 @@ const noTemplateLiteralBeforeInlineTemplate = {
|
|
|
147
136
|
|
|
148
137
|
/** @param {import('eslint').Rule.Node} node */
|
|
149
138
|
Decorator(node) {
|
|
150
|
-
const decoratorName =
|
|
139
|
+
const decoratorName = getAngularDecoratorName(context.sourceCode, node);
|
|
151
140
|
if (decoratorName !== 'Component' && decoratorName !== 'Directive') return;
|
|
152
141
|
|
|
153
142
|
const expression = /** @type {any} */ (node).expression;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { getAngularDecoratorName } = require('./internals/import-resolution');
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Disallows trivial wrapper methods that do nothing but forward all their
|
|
6
8
|
* arguments to another call.
|
|
@@ -34,22 +36,14 @@ const CONTRACT_METHOD_NAMES = new Set(['focus', 'blur', 'reset']);
|
|
|
34
36
|
const isSimpleParam = (param) => param.type === 'Identifier';
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
|
-
*
|
|
38
|
-
* @param {
|
|
39
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
40
|
+
* @param {any} methodNode
|
|
39
41
|
*/
|
|
40
|
-
const isInAngularClass = (methodNode) =>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return decorators.some((decorator) => {
|
|
47
|
-
const expression = decorator.expression;
|
|
48
|
-
const callee = expression.type === 'CallExpression' ? expression.callee : expression;
|
|
49
|
-
|
|
50
|
-
return callee.type === 'Identifier' && (callee.name === 'Component' || callee.name === 'Directive');
|
|
51
|
-
});
|
|
52
|
-
};
|
|
42
|
+
const isInAngularClass = (sourceCode, methodNode) =>
|
|
43
|
+
methodNode.parent?.parent?.decorators?.some((/** @type {any} */ decorator) => {
|
|
44
|
+
const name = getAngularDecoratorName(sourceCode, decorator);
|
|
45
|
+
return name === 'Component' || name === 'Directive';
|
|
46
|
+
}) ?? false;
|
|
53
47
|
|
|
54
48
|
/**
|
|
55
49
|
* Returns the single CallExpression in the method body when the method is a
|
|
@@ -127,7 +121,7 @@ const noTrivialWrapperMethod = {
|
|
|
127
121
|
const classNode = node.parent?.parent;
|
|
128
122
|
if (node.override || classNode?.superClass || classNode?.implements?.length) return;
|
|
129
123
|
if (node.static && methodName === 'ngTemplateContextGuard') return;
|
|
130
|
-
if (CONTRACT_METHOD_NAMES.has(methodName) && isInAngularClass(node)) return;
|
|
124
|
+
if (CONTRACT_METHOD_NAMES.has(methodName) && isInAngularClass(context.sourceCode, node)) return;
|
|
131
125
|
|
|
132
126
|
const callee = callExpr.callee;
|
|
133
127
|
const targetName =
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { isImportedAs } = require('./internals/import-resolution');
|
|
5
|
+
|
|
4
6
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
5
7
|
const noTypedInjectedElementRef = {
|
|
6
8
|
meta: {
|
|
@@ -20,30 +22,25 @@ const noTypedInjectedElementRef = {
|
|
|
20
22
|
create(context) {
|
|
21
23
|
return {
|
|
22
24
|
CallExpression(node) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
25
|
+
const src = context.sourceCode;
|
|
26
|
+
if (!isImportedAs(src, node.callee, 'inject')) return;
|
|
26
27
|
|
|
27
28
|
const firstArg = node.arguments[0];
|
|
28
29
|
if (!firstArg) return;
|
|
29
30
|
|
|
30
|
-
const isPlainElementRef =
|
|
31
|
+
const isPlainElementRef = isImportedAs(src, firstArg, 'ElementRef');
|
|
31
32
|
const isInstantiatedElementRef =
|
|
32
|
-
firstArg.type === 'TSInstantiationExpression' &&
|
|
33
|
-
firstArg.expression &&
|
|
34
|
-
firstArg.expression.type === 'Identifier' &&
|
|
35
|
-
firstArg.expression.name === 'ElementRef';
|
|
33
|
+
firstArg.type === 'TSInstantiationExpression' && isImportedAs(src, firstArg.expression, 'ElementRef');
|
|
36
34
|
|
|
37
35
|
if (!isPlainElementRef && !isInstantiatedElementRef) return;
|
|
38
36
|
|
|
39
|
-
const
|
|
37
|
+
const elementRef = src.getText(isPlainElementRef ? firstArg : firstArg.expression);
|
|
40
38
|
const existingTypeParams = node.typeParameters || node.typeArguments;
|
|
41
39
|
const injectType = existingTypeParams?.params?.[0];
|
|
42
40
|
const hasTypedElementRef =
|
|
43
41
|
existingTypeParams?.params?.length === 1 &&
|
|
44
42
|
injectType?.type === 'TSTypeReference' &&
|
|
45
|
-
injectType.typeName
|
|
46
|
-
injectType.typeName.name === 'ElementRef' &&
|
|
43
|
+
src.getText(injectType.typeName) === elementRef &&
|
|
47
44
|
(injectType.typeArguments || injectType.typeParameters)?.params?.length > 0;
|
|
48
45
|
|
|
49
46
|
if (isPlainElementRef && !hasTypedElementRef) {
|
|
@@ -53,8 +50,8 @@ const noTypedInjectedElementRef = {
|
|
|
53
50
|
messageId: 'missingGeneric',
|
|
54
51
|
fix(fixer) {
|
|
55
52
|
return existingTypeParams
|
|
56
|
-
? fixer.replaceText(existingTypeParams,
|
|
57
|
-
: fixer.insertTextAfter(node.callee,
|
|
53
|
+
? fixer.replaceText(existingTypeParams, `<${elementRef}<HTMLElement>>`)
|
|
54
|
+
: fixer.insertTextAfter(node.callee, `<${elementRef}<HTMLElement>>`);
|
|
58
55
|
},
|
|
59
56
|
});
|
|
60
57
|
return;
|
|
@@ -71,9 +68,9 @@ const noTypedInjectedElementRef = {
|
|
|
71
68
|
messageId: 'missingGeneric',
|
|
72
69
|
fix(fixer) {
|
|
73
70
|
const typeFix = existingTypeParams
|
|
74
|
-
? fixer.replaceText(existingTypeParams,
|
|
75
|
-
: fixer.insertTextAfter(node.callee,
|
|
76
|
-
return [typeFix, fixer.replaceText(firstArg,
|
|
71
|
+
? fixer.replaceText(existingTypeParams, `<${elementRef}${tokenTypeParamText}>`)
|
|
72
|
+
: fixer.insertTextAfter(node.callee, `<${elementRef}${tokenTypeParamText}>`);
|
|
73
|
+
return [typeFix, fixer.replaceText(firstArg, elementRef)];
|
|
77
74
|
},
|
|
78
75
|
});
|
|
79
76
|
} else {
|
|
@@ -82,7 +79,7 @@ const noTypedInjectedElementRef = {
|
|
|
82
79
|
node,
|
|
83
80
|
messageId: 'missingGeneric',
|
|
84
81
|
fix(fixer) {
|
|
85
|
-
return fixer.replaceText(firstArg,
|
|
82
|
+
return fixer.replaceText(firstArg, elementRef);
|
|
86
83
|
},
|
|
87
84
|
});
|
|
88
85
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { getAngularDecoratorName } = require('./internals/import-resolution');
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Disallows class members that are declared but never read within the class body.
|
|
6
8
|
*
|
|
@@ -42,22 +44,17 @@ const extractIdentifiers = (str) => str.match(/\b[a-zA-Z_$][a-zA-Z0-9_$]*\b/g) ?
|
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* Returns info about the Angular decorators applied to a class node.
|
|
47
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
45
48
|
* @param {any} classNode
|
|
46
49
|
* @returns {{ isComponent: boolean, isAngularClass: boolean }}
|
|
47
50
|
*/
|
|
48
|
-
const getDecoratorInfo = (classNode) => {
|
|
51
|
+
const getDecoratorInfo = (sourceCode, classNode) => {
|
|
49
52
|
const decorators = classNode.decorators ?? [];
|
|
50
53
|
let isComponent = false;
|
|
51
54
|
let isAngularClass = false;
|
|
52
55
|
|
|
53
56
|
for (const dec of decorators) {
|
|
54
|
-
const
|
|
55
|
-
const name =
|
|
56
|
-
expr?.type === 'CallExpression' && expr.callee?.type === 'Identifier'
|
|
57
|
-
? expr.callee.name
|
|
58
|
-
: expr?.type === 'Identifier'
|
|
59
|
-
? expr.name
|
|
60
|
-
: null;
|
|
57
|
+
const name = getAngularDecoratorName(sourceCode, dec);
|
|
61
58
|
|
|
62
59
|
if (name && ANGULAR_DECORATORS.has(name)) {
|
|
63
60
|
isAngularClass = true;
|
|
@@ -167,7 +164,7 @@ const noUnusedClassMember = {
|
|
|
167
164
|
return {
|
|
168
165
|
ClassBody(node) {
|
|
169
166
|
const classNode = node.parent;
|
|
170
|
-
const { isComponent, isAngularClass } = getDecoratorInfo(classNode);
|
|
167
|
+
const { isComponent, isAngularClass } = getDecoratorInfo(context.sourceCode, classNode);
|
|
171
168
|
const hostBound = getHostBoundMembers(classNode);
|
|
172
169
|
classStack.push({
|
|
173
170
|
classBody: node,
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* import { clone, equal } from '@ethlete/core';
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
const { MODULE_REFERENCE_SELECTOR, getModuleReference, isGlobalReference } = require('./internals/import-resolution');
|
|
29
|
+
|
|
28
30
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
29
31
|
const preferCloneEqual = {
|
|
30
32
|
meta: {
|
|
@@ -70,21 +72,21 @@ const preferCloneEqual = {
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
// ── structuredClone(expr) ─────────────────────────────────────────────
|
|
73
|
-
if (
|
|
75
|
+
if (isGlobalReference(context.sourceCode, callee) && callee.name === 'structuredClone') {
|
|
74
76
|
context.report({ node, messageId: 'preferClone', data: { method: 'structuredClone()' } });
|
|
75
77
|
}
|
|
76
78
|
},
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
+
[MODULE_REFERENCE_SELECTOR](node) {
|
|
81
|
+
const reference = getModuleReference(node);
|
|
82
|
+
if (!reference) return;
|
|
83
|
+
const src = reference.source;
|
|
80
84
|
|
|
81
85
|
// ── lodash cloneDeep / isEqual ────────────────────────────────────────
|
|
82
86
|
// import { cloneDeep } from 'lodash' / 'lodash-es'
|
|
83
87
|
// import { isEqual } from 'lodash' / 'lodash-es'
|
|
84
88
|
if (src === 'lodash' || src === 'lodash-es') {
|
|
85
|
-
for (const
|
|
86
|
-
if (specifier.type !== 'ImportSpecifier') continue;
|
|
87
|
-
const name = specifier.imported.name;
|
|
89
|
+
for (const { name } of reference.named) {
|
|
88
90
|
if (name === 'cloneDeep') {
|
|
89
91
|
context.report({ node, messageId: 'preferClone', data: { method: 'lodash cloneDeep' } });
|
|
90
92
|
} else if (name === 'isEqual') {
|