@ethlete/eslint-plugin 1.0.0-next.22 → 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 +23 -0
- package/package.json +4 -5
- package/src/configs/recommended.js +23 -10
- package/src/index.js +6 -1
- package/src/rules/angular-decorator-property-order.js +20 -20
- package/src/rules/class-constant-property.js +35 -14
- package/src/rules/class-member-order.js +77 -46
- package/src/rules/guard-return-newline.js +5 -4
- package/src/rules/inject-member-accessibility.js +22 -24
- package/src/rules/internals/angular-io.js +31 -14
- package/src/rules/internals/angular-member-visibility.js +16 -12
- package/src/rules/internals/angular-metadata-fix.js +22 -0
- package/src/rules/internals/import-resolution.js +191 -0
- package/src/rules/internals/member-accessibility-fix.js +26 -0
- package/src/rules/no-angular-router-api.js +23 -29
- package/src/rules/no-angular-seo-services.js +21 -16
- package/src/rules/no-cdk-import.js +7 -14
- package/src/rules/no-direct-dom-manipulation.js +17 -0
- package/src/rules/no-document-cookie.js +3 -0
- package/src/rules/no-effect-cleanup-return.js +15 -9
- package/src/rules/no-empty-angular-metadata-arrays.js +6 -36
- package/src/rules/no-enum.js +115 -0
- package/src/rules/no-inject-chain.js +3 -1
- package/src/rules/no-leading-underscore-class-member.js +33 -3
- package/src/rules/no-legacy-angular-decorators.js +17 -21
- package/src/rules/no-legacy-prepare-without-injector.js +42 -26
- package/src/rules/no-legacy-query-import.js +7 -16
- package/src/rules/no-locale-id.js +6 -18
- package/src/rules/no-member-alias.js +21 -0
- package/src/rules/no-native-html-input-name.js +1 -1
- package/src/rules/no-pipe-logic.js +11 -17
- package/src/rules/no-readonly-signal.js +13 -5
- package/src/rules/no-redundant-internal.js +21 -2
- package/src/rules/no-redundant-on-push-change-detection.js +32 -56
- 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 +7 -36
- 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 +2 -0
- package/src/rules/no-trivial-wrapper-method.js +13 -16
- package/src/rules/no-type-only-import.js +7 -2
- package/src/rules/no-typed-injected-element-ref.js +23 -19
- package/src/rules/no-unused-class-member.js +34 -14
- 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 +12 -16
- package/src/rules/prefer-present-tense-output.js +1 -1
- package/src/rules/prefer-rxjs-timer.js +3 -1
- package/src/rules/prefer-scroll-state.js +3 -67
- package/src/rules/prefer-static-boolean-properties.js +30 -0
- package/src/rules/prefer-viewport-size.js +1 -7
- package/src/rules/require-dollar-suffix.js +7 -4
- package/src/rules/require-form-submit.js +2 -28
- package/src/rules/require-on-push-change-detection.js +48 -36
- package/src/rules/require-view-encapsulation-none.js +48 -34
- package/src/rules/take-until-destroyed-last.js +64 -0
- package/src/rules/template-member-accessibility.js +12 -27
|
@@ -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
|
|
|
@@ -48,7 +50,7 @@ const LEGACY_SYMBOLS = {
|
|
|
48
50
|
queryStateErrorSignal: { to: '`query.error()`', docs: '/query/queries#the-query-object' },
|
|
49
51
|
queryStateLoadingSignal: { to: '`query.loading()`', docs: '/query/queries#the-query-object' },
|
|
50
52
|
validateWithV2Query: { to: '`validateWithQuery`', docs: '/query/errors#validating-against-the-server-as-the-user-types' }, // prettier-ignore
|
|
51
|
-
provideQueryClientForDevtools: { to: '`provideQueryDevtools()` from `@ethlete/query
|
|
53
|
+
provideQueryClientForDevtools: { to: '`provideQueryDevtools()` from `@ethlete/query`, which registers every client at once', docs: '/query-devtools/' }, // prettier-ignore
|
|
52
54
|
};
|
|
53
55
|
|
|
54
56
|
/** The current-system counterparts of the names the legacy system had to give up its half of. */
|
|
@@ -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) {
|
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* Disallows injecting Angular's LOCALE_ID token directly.
|
|
6
6
|
*
|
|
7
|
-
* @ethlete/core provides
|
|
8
|
-
* and exposes helper utilities for locale-aware formatting.
|
|
7
|
+
* @ethlete/core provides a signal-based locale store for application code.
|
|
9
8
|
*
|
|
10
9
|
* BAD:
|
|
11
10
|
* inject(LOCALE_ID)
|
|
12
|
-
* import { LOCALE_ID } from '@angular/core';
|
|
13
11
|
*
|
|
14
12
|
* GOOD:
|
|
15
13
|
* import { injectLocale } from '@ethlete/core';
|
|
16
14
|
* const locale = injectLocale();
|
|
17
15
|
*/
|
|
18
16
|
|
|
17
|
+
const { isImportedAs } = require('./internals/import-resolution');
|
|
18
|
+
|
|
19
19
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
20
20
|
const noLocaleId = {
|
|
21
21
|
meta: {
|
|
@@ -26,30 +26,18 @@ const noLocaleId = {
|
|
|
26
26
|
},
|
|
27
27
|
messages: {
|
|
28
28
|
noLocaleId:
|
|
29
|
-
"Do not inject 'LOCALE_ID' directly. Use 'injectLocale()' from '@ethlete/core'
|
|
29
|
+
"Do not inject 'LOCALE_ID' directly. Use the signal-based 'injectLocale()' API from '@ethlete/core' for application locale state.",
|
|
30
30
|
},
|
|
31
31
|
schema: [],
|
|
32
32
|
},
|
|
33
33
|
create(context) {
|
|
34
34
|
return {
|
|
35
35
|
CallExpression(node) {
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
const arg = node.arguments[0];
|
|
39
|
-
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')) {
|
|
40
38
|
context.report({ node, messageId: 'noLocaleId' });
|
|
41
39
|
}
|
|
42
40
|
},
|
|
43
|
-
|
|
44
|
-
ImportDeclaration(node) {
|
|
45
|
-
if (node.source.value !== '@angular/core') return;
|
|
46
|
-
for (const specifier of node.specifiers) {
|
|
47
|
-
if (specifier.type === 'ImportSpecifier' && specifier.imported.name === 'LOCALE_ID') {
|
|
48
|
-
context.report({ node, messageId: 'noLocaleId' });
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
41
|
};
|
|
54
42
|
},
|
|
55
43
|
};
|
|
@@ -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`.
|
|
@@ -79,6 +81,18 @@ const getMethodAliasInfo = (node) => {
|
|
|
79
81
|
return { sourceName: obj.property.name, propName: callee.property.name };
|
|
80
82
|
};
|
|
81
83
|
|
|
84
|
+
const CONTRACT_METHOD_NAMES = new Set(['blur', 'focus', 'reset']);
|
|
85
|
+
|
|
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';
|
|
94
|
+
}) ?? false;
|
|
95
|
+
|
|
82
96
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
83
97
|
const noMemberAlias = {
|
|
84
98
|
meta: {
|
|
@@ -119,6 +133,10 @@ const noMemberAlias = {
|
|
|
119
133
|
|
|
120
134
|
const sourceName = obj.property.name;
|
|
121
135
|
const propName = value.property.name;
|
|
136
|
+
const sourceMember = node.parent?.body?.find(
|
|
137
|
+
(member) => !member.computed && member.key?.type === 'Identifier' && member.key.name === sourceName,
|
|
138
|
+
);
|
|
139
|
+
if (node.accessibility === 'private' && sourceMember?.accessibility === 'private') return;
|
|
122
140
|
|
|
123
141
|
context.report({
|
|
124
142
|
node,
|
|
@@ -133,6 +151,9 @@ const noMemberAlias = {
|
|
|
133
151
|
|
|
134
152
|
const info = getMethodAliasInfo(node);
|
|
135
153
|
if (!info) return;
|
|
154
|
+
const classNode = node.parent?.parent;
|
|
155
|
+
if (node.override || classNode?.superClass || classNode?.implements?.length) return;
|
|
156
|
+
if (isInAngularClass(context.sourceCode, node) && CONTRACT_METHOD_NAMES.has(aliasName)) return;
|
|
136
157
|
|
|
137
158
|
// Only flag when names match — different names indicate an intentional
|
|
138
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 = {
|
|
@@ -39,6 +32,7 @@ const noPipeLogic = {
|
|
|
39
32
|
MethodDefinition(node) {
|
|
40
33
|
const key = /** @type {any} */ (node).key;
|
|
41
34
|
if (key.type !== 'Identifier' || key.name !== 'transform') return;
|
|
35
|
+
if (!node.value?.body || node.value.body.type === 'TSEmptyBodyFunctionExpression') return;
|
|
42
36
|
|
|
43
37
|
const classBody = node.parent;
|
|
44
38
|
if (!classBody || classBody.type !== 'ClassBody') return;
|
|
@@ -46,7 +40,7 @@ const noPipeLogic = {
|
|
|
46
40
|
const classNode = classBody.parent;
|
|
47
41
|
if (!classNode || (classNode.type !== 'ClassDeclaration' && classNode.type !== 'ClassExpression')) return;
|
|
48
42
|
|
|
49
|
-
if (!hasPipeDecorator(classNode)) return;
|
|
43
|
+
if (!hasPipeDecorator(context.sourceCode, classNode)) return;
|
|
50
44
|
|
|
51
45
|
context.report({ node, messageId: 'noLogicInTransform' });
|
|
52
46
|
},
|
|
@@ -62,7 +56,7 @@ const noPipeLogic = {
|
|
|
62
56
|
const classNode = classBody.parent;
|
|
63
57
|
if (!classNode || (classNode.type !== 'ClassDeclaration' && classNode.type !== 'ClassExpression')) return;
|
|
64
58
|
|
|
65
|
-
if (!hasPipeDecorator(classNode)) return;
|
|
59
|
+
if (!hasPipeDecorator(context.sourceCode, classNode)) return;
|
|
66
60
|
|
|
67
61
|
const value = /** @type {any} */ (node).value;
|
|
68
62
|
if (value && (value.type === 'ArrowFunctionExpression' || value.type === 'FunctionExpression')) {
|
|
@@ -50,6 +50,10 @@ const REACTIVE_APIS = new Set([
|
|
|
50
50
|
'effect',
|
|
51
51
|
'afterNextRender',
|
|
52
52
|
'afterRender',
|
|
53
|
+
'afterRenderEffect',
|
|
54
|
+
'resource',
|
|
55
|
+
'rxResource',
|
|
56
|
+
'httpResource',
|
|
53
57
|
// RxJS interop
|
|
54
58
|
'toSignal',
|
|
55
59
|
'toObservable',
|
|
@@ -83,7 +87,7 @@ const noReadonlySignal = {
|
|
|
83
87
|
let apiName = null;
|
|
84
88
|
|
|
85
89
|
// Simple call: signal(), input(), computed(), inject(), etc.
|
|
86
|
-
if (callee.type === 'Identifier' && REACTIVE_APIS.has(callee.name)) {
|
|
90
|
+
if (callee.type === 'Identifier' && (REACTIVE_APIS.has(callee.name) || callee.name.startsWith('inject'))) {
|
|
87
91
|
apiName = callee.name;
|
|
88
92
|
}
|
|
89
93
|
// Member call: input.required(), outputFromObservable.something(), etc.
|
|
@@ -101,13 +105,17 @@ const noReadonlySignal = {
|
|
|
101
105
|
messageId: 'noReadonlySignal',
|
|
102
106
|
data: { name: apiName },
|
|
103
107
|
fix(fixer) {
|
|
104
|
-
// Find and remove the "readonly" token
|
|
105
108
|
const sourceCode = context.sourceCode;
|
|
106
109
|
const tokens = sourceCode.getTokens(node);
|
|
107
|
-
const
|
|
110
|
+
const decoratorEnd = node.decorators?.at(-1)?.range[1] ?? node.range[0];
|
|
111
|
+
const readonlyToken = tokens.find(
|
|
112
|
+
(token) =>
|
|
113
|
+
token.value === 'readonly' && token.range[0] >= decoratorEnd && token.range[1] <= node.key.range[0],
|
|
114
|
+
);
|
|
108
115
|
if (!readonlyToken) return null;
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
const nextToken = sourceCode.getTokenAfter(readonlyToken);
|
|
117
|
+
const rangeEnd = nextToken ? nextToken.range[0] : readonlyToken.range[1];
|
|
118
|
+
return fixer.removeRange([readonlyToken.range[0], rangeEnd]);
|
|
111
119
|
},
|
|
112
120
|
});
|
|
113
121
|
}
|
|
@@ -40,6 +40,25 @@ const getMemberKind = (node) => {
|
|
|
40
40
|
return 'property';
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
45
|
+
* @param {import('eslint').Rule.Node} node
|
|
46
|
+
* @param {import('eslint').AST.Token} comment
|
|
47
|
+
*/
|
|
48
|
+
const buildInternalTagFix = (sourceCode, node, comment) => {
|
|
49
|
+
const commentText = sourceCode.getText(comment);
|
|
50
|
+
const remainingText = commentText
|
|
51
|
+
.replace(/^\s*\/\*\*?\s*@internal\s*\*\/\s*$/u, '')
|
|
52
|
+
.replace(/^\s*\*\s*@internal\s*\r?\n/mu, '')
|
|
53
|
+
.replace(/@internal\b\s*/u, '');
|
|
54
|
+
|
|
55
|
+
if (remainingText.trim() === '') {
|
|
56
|
+
return (fixer) => fixer.removeRange([comment.range[0], node.range[0]]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (fixer) => fixer.replaceText(comment, remainingText);
|
|
60
|
+
};
|
|
61
|
+
|
|
43
62
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
44
63
|
const noRedundantInternal = {
|
|
45
64
|
meta: {
|
|
@@ -68,7 +87,7 @@ const noRedundantInternal = {
|
|
|
68
87
|
return;
|
|
69
88
|
}
|
|
70
89
|
|
|
71
|
-
if (node.accessibility !== 'private'
|
|
90
|
+
if (node.accessibility !== 'private') {
|
|
72
91
|
return;
|
|
73
92
|
}
|
|
74
93
|
|
|
@@ -80,7 +99,7 @@ const noRedundantInternal = {
|
|
|
80
99
|
kind: getMemberKind(node),
|
|
81
100
|
name: getMemberName(node),
|
|
82
101
|
},
|
|
83
|
-
fix: (
|
|
102
|
+
fix: buildInternalTagFix(sourceCode, node, internalComment),
|
|
84
103
|
});
|
|
85
104
|
};
|
|
86
105
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { getAngularDecoratorName, isImportedAs } = require('./internals/import-resolution');
|
|
5
|
+
|
|
6
|
+
const { getMetadataEntryRemovalRange } = require('./internals/angular-metadata-fix');
|
|
7
|
+
|
|
4
8
|
/**
|
|
5
9
|
* Disallow explicit `changeDetection: ChangeDetectionStrategy.OnPush` on
|
|
6
10
|
* `@Component` decorators. Since Angular 22, OnPush is the default change
|
|
@@ -68,50 +72,15 @@ const getPropertyName = (key) => {
|
|
|
68
72
|
};
|
|
69
73
|
|
|
70
74
|
/**
|
|
71
|
-
* @param {import('eslint').
|
|
72
|
-
*/
|
|
73
|
-
const isComponentDecorator = (node) => {
|
|
74
|
-
const decorator = /** @type {any} */ (node);
|
|
75
|
-
if (decorator.type !== 'Decorator') return false;
|
|
76
|
-
|
|
77
|
-
const expression = decorator.expression;
|
|
78
|
-
if (expression.type === 'CallExpression') {
|
|
79
|
-
return expression.callee.type === 'Identifier' && expression.callee.name === 'Component';
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return expression.type === 'Identifier' && expression.name === 'Component';
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
/**
|
|
75
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
86
76
|
* @param {any} value
|
|
87
77
|
*/
|
|
88
|
-
const isOnPushValue = (value) =>
|
|
78
|
+
const isOnPushValue = (sourceCode, value) =>
|
|
89
79
|
value.type === 'MemberExpression' &&
|
|
90
|
-
value.
|
|
91
|
-
value.object.name === 'ChangeDetectionStrategy' &&
|
|
80
|
+
!value.computed &&
|
|
92
81
|
value.property.type === 'Identifier' &&
|
|
93
|
-
value.property.name === 'OnPush'
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Range that removes a property together with the comma that joins it to its
|
|
97
|
-
* siblings (leading comma when it is the last property, otherwise trailing).
|
|
98
|
-
*
|
|
99
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
100
|
-
* @param {any} metadata
|
|
101
|
-
* @param {any} property
|
|
102
|
-
*/
|
|
103
|
-
const getPropertyRemovalRange = (sourceCode, metadata, property) => {
|
|
104
|
-
const properties = metadata.properties.filter((entry) => entry.type === 'Property');
|
|
105
|
-
const index = properties.indexOf(property);
|
|
106
|
-
const openingBrace = sourceCode.getFirstToken(metadata);
|
|
107
|
-
const closingBrace = sourceCode.getLastToken(metadata);
|
|
108
|
-
|
|
109
|
-
if (!openingBrace || !closingBrace) return property.range;
|
|
110
|
-
if (properties.length === 1) return [openingBrace.range[1], closingBrace.range[0]];
|
|
111
|
-
if (index < properties.length - 1) return [property.range[0], properties[index + 1].range[0]];
|
|
112
|
-
|
|
113
|
-
return [properties[index - 1].range[1], property.range[1]];
|
|
114
|
-
};
|
|
82
|
+
value.property.name === 'OnPush' &&
|
|
83
|
+
isImportedAs(sourceCode, value.object, 'ChangeDetectionStrategy');
|
|
115
84
|
|
|
116
85
|
/**
|
|
117
86
|
* Range that removes a single named import specifier together with its comma.
|
|
@@ -126,6 +95,15 @@ const getSpecifierRemovalRange = (sourceCode, importNode, specifier) => {
|
|
|
126
95
|
|
|
127
96
|
// Sole specifier of the whole import — drop the entire declaration.
|
|
128
97
|
if (importNode.specifiers.length === 1) return importNode.range;
|
|
98
|
+
if (named.length === 1) {
|
|
99
|
+
const openingBrace = sourceCode.getTokenBefore(specifier, (token) => token.value === '{');
|
|
100
|
+
const closingBrace = sourceCode.getTokenAfter(specifier, (token) => token.value === '}');
|
|
101
|
+
const precedingComma = openingBrace ? sourceCode.getTokenBefore(openingBrace) : null;
|
|
102
|
+
|
|
103
|
+
if (openingBrace && closingBrace && precedingComma?.value === ',') {
|
|
104
|
+
return [precedingComma.range[0], closingBrace.range[1]];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
129
107
|
if (index < named.length - 1) return [specifier.range[0], named[index + 1].range[0]];
|
|
130
108
|
|
|
131
109
|
return [named[index - 1].range[1], specifier.range[1]];
|
|
@@ -137,18 +115,10 @@ const getSpecifierRemovalRange = (sourceCode, importNode, specifier) => {
|
|
|
137
115
|
* removing those properties leaves the import unused and safe to drop.
|
|
138
116
|
*
|
|
139
117
|
* @param {import('eslint').Scope.Reference} reference
|
|
118
|
+
* @param {Set<any>} removableIdentifiers
|
|
140
119
|
*/
|
|
141
|
-
const isOnPushChangeDetectionReference = (reference) =>
|
|
142
|
-
|
|
143
|
-
const member = identifier.parent;
|
|
144
|
-
if (!member || member.type !== 'MemberExpression' || member.object !== identifier) return false;
|
|
145
|
-
if (member.property.type !== 'Identifier' || member.property.name !== 'OnPush') return false;
|
|
146
|
-
|
|
147
|
-
const property = member.parent;
|
|
148
|
-
if (!property || property.type !== 'Property' || property.value !== member) return false;
|
|
149
|
-
|
|
150
|
-
return getPropertyName(property.key) === 'changeDetection';
|
|
151
|
-
};
|
|
120
|
+
const isOnPushChangeDetectionReference = (reference, removableIdentifiers) =>
|
|
121
|
+
removableIdentifiers.has(reference.identifier);
|
|
152
122
|
|
|
153
123
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
154
124
|
const noRedundantOnPushChangeDetection = {
|
|
@@ -177,10 +147,11 @@ const noRedundantOnPushChangeDetection = {
|
|
|
177
147
|
const sourceCode = context.sourceCode;
|
|
178
148
|
/** @type {any} */
|
|
179
149
|
let changeDetectionSpecifier = null;
|
|
150
|
+
const removableIdentifiers = new Set();
|
|
180
151
|
|
|
181
152
|
return {
|
|
182
153
|
Decorator(node) {
|
|
183
|
-
if (
|
|
154
|
+
if (getAngularDecoratorName(sourceCode, node) !== 'Component') return;
|
|
184
155
|
|
|
185
156
|
const expression = /** @type {any} */ (node).expression;
|
|
186
157
|
if (expression.type !== 'CallExpression' || expression.arguments.length === 0) return;
|
|
@@ -191,12 +162,13 @@ const noRedundantOnPushChangeDetection = {
|
|
|
191
162
|
const property = metadata.properties.find(
|
|
192
163
|
(entry) => entry.type === 'Property' && getPropertyName(entry.key) === 'changeDetection',
|
|
193
164
|
);
|
|
194
|
-
if (!property || !isOnPushValue(property.value)) return;
|
|
165
|
+
if (!property || !isOnPushValue(sourceCode, property.value)) return;
|
|
166
|
+
removableIdentifiers.add(property.value.object);
|
|
195
167
|
|
|
196
168
|
context.report({
|
|
197
169
|
node: property,
|
|
198
170
|
messageId: 'redundant',
|
|
199
|
-
fix: (fixer) => fixer.replaceTextRange(
|
|
171
|
+
fix: (fixer) => fixer.replaceTextRange(getMetadataEntryRemovalRange(sourceCode, metadata, property), ''),
|
|
200
172
|
});
|
|
201
173
|
},
|
|
202
174
|
|
|
@@ -219,8 +191,12 @@ const noRedundantOnPushChangeDetection = {
|
|
|
219
191
|
if (!changeDetectionSpecifier) return;
|
|
220
192
|
|
|
221
193
|
const [variable] = sourceCode.getDeclaredVariables(changeDetectionSpecifier);
|
|
222
|
-
if (!variable
|
|
223
|
-
if (
|
|
194
|
+
if (!variable) return;
|
|
195
|
+
if (
|
|
196
|
+
!variable.references.every((reference) => isOnPushChangeDetectionReference(reference, removableIdentifiers))
|
|
197
|
+
) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
224
200
|
|
|
225
201
|
const importNode = changeDetectionSpecifier.parent;
|
|
226
202
|
context.report({
|
|
@@ -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,10 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { getAngularDecoratorName } = require('./internals/import-resolution');
|
|
5
|
+
|
|
6
|
+
const { getMetadataEntryRemovalRange } = require('./internals/angular-metadata-fix');
|
|
7
|
+
|
|
4
8
|
/** @typedef {'Component' | 'Directive' | 'Pipe'} TDecoratorName */
|
|
5
9
|
|
|
6
10
|
/**
|
|
@@ -12,40 +16,6 @@ const getPropertyName = (key) => {
|
|
|
12
16
|
return null;
|
|
13
17
|
};
|
|
14
18
|
|
|
15
|
-
/**
|
|
16
|
-
* @param {import('eslint').Rule.Node} node
|
|
17
|
-
* @returns {TDecoratorName | null}
|
|
18
|
-
*/
|
|
19
|
-
const getDecoratorName = (node) => {
|
|
20
|
-
const decorator = /** @type {any} */ (node);
|
|
21
|
-
if (decorator.type !== 'Decorator') return null;
|
|
22
|
-
|
|
23
|
-
const expression = decorator.expression;
|
|
24
|
-
if (expression.type === 'CallExpression') {
|
|
25
|
-
return expression.callee.type === 'Identifier' ? expression.callee.name : null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return expression.type === 'Identifier' ? expression.name : null;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
33
|
-
* @param {any} metadata
|
|
34
|
-
* @param {any} property
|
|
35
|
-
*/
|
|
36
|
-
const getRemovalRange = (sourceCode, metadata, property) => {
|
|
37
|
-
const properties = metadata.properties.filter((entry) => entry.type === 'Property');
|
|
38
|
-
const propertyIndex = properties.indexOf(property);
|
|
39
|
-
const openingBrace = sourceCode.getFirstToken(metadata);
|
|
40
|
-
const closingBrace = sourceCode.getLastToken(metadata);
|
|
41
|
-
|
|
42
|
-
if (!openingBrace || !closingBrace) return property.range;
|
|
43
|
-
if (properties.length === 1) return [openingBrace.range[1], closingBrace.range[0]];
|
|
44
|
-
if (propertyIndex < properties.length - 1) return [property.range[0], properties[propertyIndex + 1].range[0]];
|
|
45
|
-
|
|
46
|
-
return [properties[propertyIndex - 1].range[1], property.range[1]];
|
|
47
|
-
};
|
|
48
|
-
|
|
49
19
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
50
20
|
const noStandaloneFlag = {
|
|
51
21
|
meta: {
|
|
@@ -65,7 +35,7 @@ const noStandaloneFlag = {
|
|
|
65
35
|
|
|
66
36
|
return {
|
|
67
37
|
Decorator(node) {
|
|
68
|
-
const decoratorName =
|
|
38
|
+
const decoratorName = getAngularDecoratorName(context.sourceCode, node);
|
|
69
39
|
if (decoratorName !== 'Component' && decoratorName !== 'Directive' && decoratorName !== 'Pipe') return;
|
|
70
40
|
|
|
71
41
|
const decorator = /** @type {any} */ (node);
|
|
@@ -78,12 +48,13 @@ const noStandaloneFlag = {
|
|
|
78
48
|
for (const property of metadata.properties) {
|
|
79
49
|
if (property.type !== 'Property') continue;
|
|
80
50
|
if (getPropertyName(property.key) !== 'standalone') continue;
|
|
51
|
+
if (property.value.type !== 'Literal' || property.value.value !== true) continue;
|
|
81
52
|
|
|
82
53
|
context.report({
|
|
83
54
|
node: property,
|
|
84
55
|
messageId: 'noStandalone',
|
|
85
56
|
fix(fixer) {
|
|
86
|
-
return fixer.replaceTextRange(
|
|
57
|
+
return fixer.replaceTextRange(getMetadataEntryRemovalRange(sourceCode, metadata, property), '');
|
|
87
58
|
},
|
|
88
59
|
});
|
|
89
60
|
}
|
|
@@ -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;
|
|
@@ -173,6 +173,7 @@ const noTrivialReturnType = {
|
|
|
173
173
|
|
|
174
174
|
const annotation = fn.returnType.typeAnnotation;
|
|
175
175
|
if (!TRIVIAL_TYPES.has(annotation.type)) return;
|
|
176
|
+
if (fn.body.type !== 'BlockStatement') return;
|
|
176
177
|
if (isSelfReferencing(fn)) return;
|
|
177
178
|
|
|
178
179
|
const label = TRIVIAL_LABELS[annotation.type] ?? annotation.type;
|
|
@@ -190,6 +191,7 @@ const noTrivialReturnType = {
|
|
|
190
191
|
return {
|
|
191
192
|
ArrowFunctionExpression: checkReturnType,
|
|
192
193
|
FunctionExpression: checkReturnType,
|
|
194
|
+
FunctionDeclaration: checkReturnType,
|
|
193
195
|
};
|
|
194
196
|
},
|
|
195
197
|
};
|