@ethlete/eslint-plugin 1.0.0-next.23 → 1.0.0-next.25
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 +26 -0
- package/migrations.json +12 -0
- package/package.json +13 -3
- package/src/configs/recommended.js +33 -11
- package/src/generators/generators.json +10 -0
- package/src/generators/restore-module-augmentation-interfaces/migration.js +50 -0
- package/src/generators/restore-module-augmentation-interfaces/restore-module-augmentation-interfaces.js +103 -0
- package/src/generators/restore-module-augmentation-interfaces/schema.json +14 -0
- package/src/index.js +10 -2
- 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/consistent-type-definitions.js +60 -0
- 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-csp-unsafe.js +362 -0
- 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 +42 -41
- 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
|
@@ -16,6 +16,16 @@ const getDirectMemberName = (node) => {
|
|
|
16
16
|
return node.key?.type === 'Identifier' ? node.key.name : null;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* @param {any} key
|
|
21
|
+
* @param {boolean} computed
|
|
22
|
+
*/
|
|
23
|
+
const getReferencedName = (key, computed) => {
|
|
24
|
+
if (!computed && key?.type === 'Identifier') return key.name;
|
|
25
|
+
if (key?.type === 'Literal' && typeof key.value === 'string') return key.value;
|
|
26
|
+
return null;
|
|
27
|
+
};
|
|
28
|
+
|
|
19
29
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
20
30
|
const noLeadingUnderscoreClassMember = {
|
|
21
31
|
meta: {
|
|
@@ -36,6 +46,7 @@ const noLeadingUnderscoreClassMember = {
|
|
|
36
46
|
* declaredNames: Set<string>,
|
|
37
47
|
* membersByName: Map<string, any[]>,
|
|
38
48
|
* memberReads: any[],
|
|
49
|
+
* untrackedNames: Set<string>,
|
|
39
50
|
* }>}
|
|
40
51
|
*/
|
|
41
52
|
const classStack = [];
|
|
@@ -66,6 +77,7 @@ const noLeadingUnderscoreClassMember = {
|
|
|
66
77
|
declaredNames: new Set(),
|
|
67
78
|
membersByName: new Map(),
|
|
68
79
|
memberReads: [],
|
|
80
|
+
untrackedNames: new Set(),
|
|
69
81
|
});
|
|
70
82
|
},
|
|
71
83
|
|
|
@@ -85,6 +97,7 @@ const noLeadingUnderscoreClassMember = {
|
|
|
85
97
|
newName.length > 0 &&
|
|
86
98
|
!frame.declaredNames.has(newName) &&
|
|
87
99
|
targetNameCounts.get(newName) === 1 &&
|
|
100
|
+
!frame.untrackedNames.has(oldName) &&
|
|
88
101
|
memberNodes.every((memberNode) => memberNode.accessibility === 'private');
|
|
89
102
|
|
|
90
103
|
for (const memberNode of memberNodes) {
|
|
@@ -115,10 +128,20 @@ const noLeadingUnderscoreClassMember = {
|
|
|
115
128
|
MemberExpression(node) {
|
|
116
129
|
const frame = classStack[classStack.length - 1];
|
|
117
130
|
if (!frame) return;
|
|
118
|
-
if (node.object?.type !== 'ThisExpression' || node.computed) return;
|
|
119
|
-
if (node.property?.type !== 'Identifier') return;
|
|
120
131
|
|
|
121
|
-
|
|
132
|
+
if (node.object?.type === 'ThisExpression' && !node.computed && node.property?.type === 'Identifier') {
|
|
133
|
+
frame.memberReads.push(node);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const name = getReferencedName(node.property, node.computed);
|
|
138
|
+
if (name) frame.untrackedNames.add(name);
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
'ObjectPattern > Property'(node) {
|
|
142
|
+
const frame = classStack[classStack.length - 1];
|
|
143
|
+
const name = getReferencedName(node.key, node.computed);
|
|
144
|
+
if (frame && name) frame.untrackedNames.add(name);
|
|
122
145
|
},
|
|
123
146
|
};
|
|
124
147
|
},
|
|
@@ -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 legacy Angular decorators in favour of their modern signal-based
|
|
6
8
|
* equivalents and the `host: {}` shorthand.
|
|
@@ -25,20 +27,6 @@
|
|
|
25
27
|
* → ✅ host: { '[class.active]': 'isActive', '(click)': 'onClick()' }
|
|
26
28
|
*/
|
|
27
29
|
|
|
28
|
-
/**
|
|
29
|
-
* Returns the identifier name of a decorator.
|
|
30
|
-
* Works for both `@Foo` (Identifier) and `@Foo(...)` (CallExpression).
|
|
31
|
-
*
|
|
32
|
-
* @param {any} decorator
|
|
33
|
-
* @returns {string | null}
|
|
34
|
-
*/
|
|
35
|
-
const getDecoratorName = (decorator) => {
|
|
36
|
-
const expr = decorator.expression;
|
|
37
|
-
if (expr.type === 'Identifier') return expr.name;
|
|
38
|
-
if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier') return expr.callee.name;
|
|
39
|
-
return null;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
30
|
/**
|
|
43
31
|
* Returns the static string key of a class member, or null for computed keys.
|
|
44
32
|
*
|
|
@@ -72,11 +60,12 @@ const getIndent = (sourceCode, node) => {
|
|
|
72
60
|
};
|
|
73
61
|
|
|
74
62
|
/**
|
|
63
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
75
64
|
* @param {any} classNode
|
|
76
65
|
*/
|
|
77
|
-
const getAngularMetadata = (classNode) => {
|
|
66
|
+
const getAngularMetadata = (sourceCode, classNode) => {
|
|
78
67
|
for (const decorator of classNode.decorators ?? []) {
|
|
79
|
-
const decoratorName =
|
|
68
|
+
const decoratorName = getAngularDecoratorName(sourceCode, decorator);
|
|
80
69
|
if (decoratorName !== 'Component' && decoratorName !== 'Directive') continue;
|
|
81
70
|
|
|
82
71
|
const expression = decorator.expression;
|
|
@@ -106,7 +95,7 @@ const findObjectProperty = (objectExpression, propertyName) =>
|
|
|
106
95
|
* @param {string} entryText
|
|
107
96
|
*/
|
|
108
97
|
const buildObjectTextWithAppendedProperty = (sourceCode, objectExpression, entryText) => {
|
|
109
|
-
const properties = objectExpression.properties
|
|
98
|
+
const properties = objectExpression.properties;
|
|
110
99
|
const isMultiline = Boolean(
|
|
111
100
|
objectExpression.loc && objectExpression.loc.start.line !== objectExpression.loc.end.line,
|
|
112
101
|
);
|
|
@@ -138,8 +127,7 @@ ${closingIndent}}`;
|
|
|
138
127
|
* @param {any} objectExpression
|
|
139
128
|
*/
|
|
140
129
|
const hasTrailingComma = (sourceCode, objectExpression) => {
|
|
141
|
-
const
|
|
142
|
-
const lastProperty = properties[properties.length - 1];
|
|
130
|
+
const lastProperty = objectExpression.properties.at(-1);
|
|
143
131
|
if (!lastProperty) return false;
|
|
144
132
|
|
|
145
133
|
const tokenAfter = sourceCode.getTokenAfter(lastProperty);
|
|
@@ -158,7 +146,7 @@ const buildHostBindingFix = (sourceCode, decoratorNode, memberNode, bindingKey,
|
|
|
158
146
|
const classNode = classBody?.parent;
|
|
159
147
|
if (!classNode) return null;
|
|
160
148
|
|
|
161
|
-
const metadata = getAngularMetadata(classNode);
|
|
149
|
+
const metadata = getAngularMetadata(sourceCode, classNode);
|
|
162
150
|
if (!metadata) return null;
|
|
163
151
|
|
|
164
152
|
const hostProperty = findObjectProperty(metadata, 'host');
|
|
@@ -312,7 +300,7 @@ const rule = {
|
|
|
312
300
|
|
|
313
301
|
Decorator(node) {
|
|
314
302
|
const anyNode = /** @type {any} */ (node);
|
|
315
|
-
const decoratorName =
|
|
303
|
+
const decoratorName = getAngularDecoratorName(context.sourceCode, anyNode);
|
|
316
304
|
if (!decoratorName) return;
|
|
317
305
|
|
|
318
306
|
// Only act on decorators attached to class members, not to the class itself
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* the types this rule leaves alone), enable `@typescript-eslint/no-deprecated` alongside it.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
const { MODULE_REFERENCE_SELECTOR, getModuleReference } = require('./internals/import-resolution');
|
|
23
|
+
|
|
22
24
|
const QUERY_PACKAGE = '@ethlete/query';
|
|
23
25
|
const DEFAULT_DOCS_BASE_URL = 'https://ethlete-sdk-docs.web.app';
|
|
24
26
|
|
|
@@ -60,13 +62,6 @@ const V2_SUCCESSORS = {
|
|
|
60
62
|
V2BearerAuthProvider: { to: '`createBearerAuthProvider` plus the secure creator templates', docs: '/query/auth' },
|
|
61
63
|
};
|
|
62
64
|
|
|
63
|
-
/** @param {any} specifier */
|
|
64
|
-
const importedName = (specifier) => {
|
|
65
|
-
if (specifier.type !== 'ImportSpecifier') return null;
|
|
66
|
-
|
|
67
|
-
return specifier.imported.name ?? specifier.imported.value ?? null;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
65
|
/** @param {string} name */
|
|
71
66
|
const isV2Symbol = (name) => /^(V2|AnyV2)[A-Z]/.test(name);
|
|
72
67
|
|
|
@@ -98,16 +93,12 @@ const noLegacyQueryImport = {
|
|
|
98
93
|
const docsBaseUrl = (options.docsBaseUrl ?? DEFAULT_DOCS_BASE_URL).replace(/\/$/, '');
|
|
99
94
|
|
|
100
95
|
return {
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
if (declaration.source.value !== QUERY_PACKAGE) return;
|
|
105
|
-
|
|
106
|
-
for (const specifier of declaration.specifiers) {
|
|
107
|
-
const name = importedName(specifier);
|
|
96
|
+
[MODULE_REFERENCE_SELECTOR](node) {
|
|
97
|
+
const reference = getModuleReference(node);
|
|
108
98
|
|
|
109
|
-
|
|
99
|
+
if (reference?.source !== QUERY_PACKAGE) return;
|
|
110
100
|
|
|
101
|
+
for (const { name, node: specifier } of reference.named) {
|
|
111
102
|
const successor = V2_SUCCESSORS[name] ?? LEGACY_SYMBOLS[name];
|
|
112
103
|
|
|
113
104
|
if (successor) {
|
|
@@ -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 =
|