@ethlete/eslint-plugin 1.0.0-next.25 → 1.0.0-next.26
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 +18 -0
- package/package.json +2 -1
- package/src/configs/recommended.js +7 -16
- package/src/rules/angular-decorator-property-order.js +24 -7
- package/src/rules/class-constant-property.js +1 -1
- package/src/rules/class-member-order.js +60 -8
- package/src/rules/enforce-routing-view-naming.js +34 -23
- package/src/rules/guard-return-newline.js +4 -7
- package/src/rules/internals/angular-member-visibility.js +24 -2
- package/src/rules/internals/angular-metadata-fix.js +231 -1
- package/src/rules/internals/angular-version.js +50 -0
- package/src/rules/internals/implemented-contract-members.js +26 -11
- package/src/rules/no-angular-router-api.js +86 -33
- package/src/rules/no-angular-seo-services.js +1 -0
- package/src/rules/no-async-await.js +5 -1
- package/src/rules/no-cdk-import.js +32 -7
- package/src/rules/no-direct-dom-manipulation.js +50 -28
- package/src/rules/no-document-cookie.js +51 -8
- package/src/rules/no-dom-query.js +5 -2
- package/src/rules/no-empty-angular-metadata-arrays.js +2 -0
- package/src/rules/no-empty-newlines-between-imports.js +4 -1
- package/src/rules/no-impure-top-level-provider.js +42 -7
- package/src/rules/no-leading-underscore-class-member.js +27 -2
- package/src/rules/no-legacy-angular-decorators.js +11 -6
- package/src/rules/no-legacy-prepare-without-injector.js +57 -12
- package/src/rules/no-legacy-query-import.js +61 -16
- package/src/rules/no-member-alias.js +0 -5
- package/src/rules/no-native-observers.js +89 -24
- package/src/rules/no-pipe-logic.js +1 -2
- package/src/rules/no-readonly-signal.js +1 -5
- package/src/rules/no-redundant-internal.js +5 -2
- package/src/rules/no-redundant-on-push-change-detection.js +3 -48
- package/src/rules/no-rxjs-in-effect.js +7 -5
- package/src/rules/no-screaming-case-local.js +3 -5
- package/src/rules/no-standalone-flag.js +1 -0
- package/src/rules/no-subscribe-with-body.js +30 -1
- package/src/rules/no-template-literal-before-inline-template.js +6 -0
- package/src/rules/no-trivial-return-type.js +18 -1
- package/src/rules/no-unused-class-member.js +1 -1
- package/src/rules/no-window-location.js +60 -22
- package/src/rules/prefer-clone-equal.js +61 -8
- package/src/rules/prefer-concise-angular-host-directives.js +39 -11
- package/src/rules/prefer-concise-angular-style-metadata.js +10 -2
- package/src/rules/prefer-element-dimensions.js +264 -11
- package/src/rules/prefer-match-media.js +17 -3
- package/src/rules/prefer-rxjs-timer.js +26 -4
- package/src/rules/prefer-scroll-state.js +0 -4
- package/src/rules/prefer-static-boolean-properties.js +1 -0
- package/src/rules/prefer-viewport-size.js +153 -16
- package/src/rules/require-dollar-suffix.js +11 -3
- package/src/rules/require-form-submit.js +4 -1
- package/src/rules/require-on-push-change-detection.js +7 -259
- package/src/rules/require-view-encapsulation-none.js +6 -214
- package/src/rules/template-member-accessibility.js +12 -4
|
@@ -92,6 +92,56 @@ const noLegacyQueryImport = {
|
|
|
92
92
|
const options = context.options[0] ?? {};
|
|
93
93
|
const docsBaseUrl = (options.docsBaseUrl ?? DEFAULT_DOCS_BASE_URL).replace(/\/$/, '');
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} name
|
|
97
|
+
* @param {any} node
|
|
98
|
+
*/
|
|
99
|
+
const reportName = (name, node) => {
|
|
100
|
+
const successor =
|
|
101
|
+
(Object.hasOwn(V2_SUCCESSORS, name) ? V2_SUCCESSORS[name] : null) ??
|
|
102
|
+
(Object.hasOwn(LEGACY_SYMBOLS, name) ? LEGACY_SYMBOLS[name] : null);
|
|
103
|
+
|
|
104
|
+
if (successor) {
|
|
105
|
+
context.report({
|
|
106
|
+
node,
|
|
107
|
+
messageId: 'successor',
|
|
108
|
+
data: { name, to: successor.to, docs: `${docsBaseUrl}${successor.docs}` },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!isV2Symbol(name)) return;
|
|
115
|
+
|
|
116
|
+
context.report({
|
|
117
|
+
node,
|
|
118
|
+
messageId: 'legacySystem',
|
|
119
|
+
data: { name, docs: `${docsBaseUrl}/query/migrating-from-v2` },
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {any} identifier
|
|
125
|
+
* @returns {{ name: string, node: any } | null}
|
|
126
|
+
*/
|
|
127
|
+
const getNamespaceMember = (identifier) => {
|
|
128
|
+
const parent = identifier.parent;
|
|
129
|
+
|
|
130
|
+
if (parent?.type === 'MemberExpression' && parent.object === identifier) {
|
|
131
|
+
if (!parent.computed && parent.property.type === 'Identifier')
|
|
132
|
+
return { name: parent.property.name, node: parent };
|
|
133
|
+
if (parent.property.type === 'Literal' && typeof parent.property.value === 'string') {
|
|
134
|
+
return { name: parent.property.value, node: parent };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (parent?.type === 'TSQualifiedName' && parent.left === identifier) {
|
|
139
|
+
return { name: parent.right.name, node: parent };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return null;
|
|
143
|
+
};
|
|
144
|
+
|
|
95
145
|
return {
|
|
96
146
|
[MODULE_REFERENCE_SELECTOR](node) {
|
|
97
147
|
const reference = getModuleReference(node);
|
|
@@ -99,25 +149,20 @@ const noLegacyQueryImport = {
|
|
|
99
149
|
if (reference?.source !== QUERY_PACKAGE) return;
|
|
100
150
|
|
|
101
151
|
for (const { name, node: specifier } of reference.named) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (successor) {
|
|
105
|
-
context.report({
|
|
106
|
-
node: specifier,
|
|
107
|
-
messageId: 'successor',
|
|
108
|
-
data: { name, to: successor.to, docs: `${docsBaseUrl}${successor.docs}` },
|
|
109
|
-
});
|
|
152
|
+
reportName(name, specifier);
|
|
153
|
+
}
|
|
110
154
|
|
|
111
|
-
|
|
112
|
-
}
|
|
155
|
+
if (node.type !== 'ImportDeclaration') return;
|
|
113
156
|
|
|
114
|
-
|
|
157
|
+
for (const specifier of node.specifiers) {
|
|
158
|
+
if (specifier.type !== 'ImportNamespaceSpecifier') continue;
|
|
115
159
|
|
|
116
|
-
context.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
160
|
+
for (const variable of context.sourceCode.getDeclaredVariables(specifier)) {
|
|
161
|
+
for (const variableReference of variable.references) {
|
|
162
|
+
const member = getNamespaceMember(variableReference.identifier);
|
|
163
|
+
if (member) reportName(member.name, member.node);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
121
166
|
}
|
|
122
167
|
},
|
|
123
168
|
};
|
|
@@ -63,11 +63,9 @@ const getMethodAliasInfo = (node) => {
|
|
|
63
63
|
}
|
|
64
64
|
if (!callExpr || callExpr.type !== 'CallExpression') return null;
|
|
65
65
|
|
|
66
|
-
// Must have zero arguments
|
|
67
66
|
if (callExpr.arguments.length !== 0) return null;
|
|
68
67
|
|
|
69
68
|
const callee = callExpr.callee;
|
|
70
|
-
// callee must be: this.something.method
|
|
71
69
|
if (callee.type !== 'MemberExpression') return null;
|
|
72
70
|
if (callee.computed) return null;
|
|
73
71
|
if (callee.property.type !== 'Identifier') return null;
|
|
@@ -115,14 +113,11 @@ const noMemberAlias = {
|
|
|
115
113
|
const value = node.value;
|
|
116
114
|
if (!value) return;
|
|
117
115
|
|
|
118
|
-
// Must be: this.something.property
|
|
119
116
|
if (value.type !== 'MemberExpression') return;
|
|
120
|
-
// Outer property must be a plain identifier (not computed)
|
|
121
117
|
if (value.computed) return;
|
|
122
118
|
if (value.property.type !== 'Identifier') return;
|
|
123
119
|
|
|
124
120
|
const obj = value.object;
|
|
125
|
-
// Object must itself be: this.something
|
|
126
121
|
if (obj.type !== 'MemberExpression') return;
|
|
127
122
|
if (obj.computed) return;
|
|
128
123
|
if (obj.object.type !== 'ThisExpression') return;
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
* signalHostElementDimensions()
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
+
const { isGlobalReference } = require('./internals/import-resolution');
|
|
35
|
+
|
|
34
36
|
/**
|
|
35
37
|
* Maps Observer constructor names to their @ethlete/core signal equivalents.
|
|
36
38
|
* @type {Record<string, { alternatives: string[], from: string } | null>}
|
|
@@ -51,6 +53,8 @@ const OBSERVER_ALTERNATIVES = {
|
|
|
51
53
|
PerformanceObserver: null,
|
|
52
54
|
};
|
|
53
55
|
|
|
56
|
+
const GLOBAL_OBJECTS = new Set(['window', 'globalThis', 'self']);
|
|
57
|
+
|
|
54
58
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
55
59
|
const noNativeObservers = {
|
|
56
60
|
meta: {
|
|
@@ -68,35 +72,96 @@ const noNativeObservers = {
|
|
|
68
72
|
schema: [],
|
|
69
73
|
},
|
|
70
74
|
create(context) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
const { sourceCode } = context;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {any} node
|
|
79
|
+
* @param {Set<any>} seen
|
|
80
|
+
* @returns {string | null}
|
|
81
|
+
*/
|
|
82
|
+
const resolveObserverName = (node, seen = new Set()) => {
|
|
83
|
+
if (!node || seen.has(node)) return null;
|
|
84
|
+
seen.add(node);
|
|
85
|
+
|
|
86
|
+
if (node.type === 'TSNonNullExpression' || node.type === 'TSAsExpression') {
|
|
87
|
+
return resolveObserverName(node.expression, seen);
|
|
88
|
+
}
|
|
74
89
|
|
|
75
|
-
|
|
90
|
+
if (node.type === 'MemberExpression') {
|
|
91
|
+
if (node.computed || node.property.type !== 'Identifier') return null;
|
|
92
|
+
if (!Object.hasOwn(OBSERVER_ALTERNATIVES, node.property.name)) return null;
|
|
76
93
|
|
|
94
|
+
return node.object.type === 'Identifier' &&
|
|
95
|
+
GLOBAL_OBJECTS.has(node.object.name) &&
|
|
96
|
+
isGlobalReference(sourceCode, node.object)
|
|
97
|
+
? node.property.name
|
|
98
|
+
: null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (node.type !== 'Identifier') return null;
|
|
102
|
+
|
|
103
|
+
if (isGlobalReference(sourceCode, node)) {
|
|
77
104
|
// hasOwn, not `in`: `new constructor()` / `new toString()` would otherwise match an
|
|
78
105
|
// inherited Object.prototype key and read a function where an entry is expected
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
context.report({
|
|
95
|
-
node,
|
|
96
|
-
messageId: 'avoidObserver',
|
|
97
|
-
data: { observer: name },
|
|
98
|
-
});
|
|
106
|
+
return Object.hasOwn(OBSERVER_ALTERNATIVES, node.name) ? node.name : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @type {import('eslint').Scope.Scope | null} */
|
|
110
|
+
let scope = sourceCode.getScope(node);
|
|
111
|
+
|
|
112
|
+
while (scope) {
|
|
113
|
+
const variable = scope.set.get(node.name);
|
|
114
|
+
|
|
115
|
+
if (variable) {
|
|
116
|
+
const definition = /** @type {any} */ (variable.defs[0]);
|
|
117
|
+
|
|
118
|
+
return definition?.type === 'Variable' && definition.parent.kind === 'const'
|
|
119
|
+
? resolveObserverName(definition.node.init, seen)
|
|
120
|
+
: null;
|
|
99
121
|
}
|
|
122
|
+
|
|
123
|
+
scope = scope.upper;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return null;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {any} node
|
|
131
|
+
* @param {any} callee
|
|
132
|
+
*/
|
|
133
|
+
const check = (node, callee) => {
|
|
134
|
+
const name = resolveObserverName(callee);
|
|
135
|
+
|
|
136
|
+
if (!name) return;
|
|
137
|
+
|
|
138
|
+
const info = OBSERVER_ALTERNATIVES[name];
|
|
139
|
+
|
|
140
|
+
if (info) {
|
|
141
|
+
context.report({
|
|
142
|
+
node,
|
|
143
|
+
messageId: 'useSignalUtil',
|
|
144
|
+
data: {
|
|
145
|
+
observer: name,
|
|
146
|
+
alternatives: info.alternatives.join(' or '),
|
|
147
|
+
from: info.from,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
context.report({
|
|
152
|
+
node,
|
|
153
|
+
messageId: 'avoidObserver',
|
|
154
|
+
data: { observer: name },
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
NewExpression(node) {
|
|
161
|
+
check(node, node.callee);
|
|
162
|
+
},
|
|
163
|
+
'ClassDeclaration, ClassExpression'(node) {
|
|
164
|
+
if (node.superClass) check(node.superClass, node.superClass);
|
|
100
165
|
},
|
|
101
166
|
};
|
|
102
167
|
},
|
|
@@ -19,6 +19,7 @@ const noPipeLogic = {
|
|
|
19
19
|
docs: {
|
|
20
20
|
description:
|
|
21
21
|
'Pipes must not contain logic in the transform method. Extract to a utility function and assign it: `transform = myUtil;`',
|
|
22
|
+
recommended: true,
|
|
22
23
|
},
|
|
23
24
|
messages: {
|
|
24
25
|
noLogicInTransform:
|
|
@@ -28,7 +29,6 @@ const noPipeLogic = {
|
|
|
28
29
|
},
|
|
29
30
|
create(context) {
|
|
30
31
|
return {
|
|
31
|
-
// Case 1: transform() { ... } — method with a body
|
|
32
32
|
MethodDefinition(node) {
|
|
33
33
|
const key = /** @type {any} */ (node).key;
|
|
34
34
|
if (key.type !== 'Identifier' || key.name !== 'transform') return;
|
|
@@ -45,7 +45,6 @@ const noPipeLogic = {
|
|
|
45
45
|
context.report({ node, messageId: 'noLogicInTransform' });
|
|
46
46
|
},
|
|
47
47
|
|
|
48
|
-
// Case 2: transform = () => { ... } or transform = function() { ... }
|
|
49
48
|
PropertyDefinition(node) {
|
|
50
49
|
const key = /** @type {any} */ (node).key;
|
|
51
50
|
if (key.type !== 'Identifier' || key.name !== 'transform') return;
|
|
@@ -77,7 +77,6 @@ const noReadonlySignal = {
|
|
|
77
77
|
create(context) {
|
|
78
78
|
return {
|
|
79
79
|
PropertyDefinition(node) {
|
|
80
|
-
// Only flag when readonly is set
|
|
81
80
|
if (!node.readonly) return;
|
|
82
81
|
|
|
83
82
|
const init = node.value;
|
|
@@ -86,12 +85,9 @@ const noReadonlySignal = {
|
|
|
86
85
|
const { callee } = init;
|
|
87
86
|
let apiName = null;
|
|
88
87
|
|
|
89
|
-
// Simple call: signal(), input(), computed(), inject(), etc.
|
|
90
88
|
if (callee.type === 'Identifier' && (REACTIVE_APIS.has(callee.name) || callee.name.startsWith('inject'))) {
|
|
91
89
|
apiName = callee.name;
|
|
92
|
-
}
|
|
93
|
-
// Member call: input.required(), outputFromObservable.something(), etc.
|
|
94
|
-
else if (
|
|
90
|
+
} else if (
|
|
95
91
|
callee.type === 'MemberExpression' &&
|
|
96
92
|
callee.object.type === 'Identifier' &&
|
|
97
93
|
REACTIVE_APIS.has(callee.object.name)
|
|
@@ -28,6 +28,7 @@ const getAttachedInternalComment = (sourceCode, node) => {
|
|
|
28
28
|
const getMemberName = (node) => {
|
|
29
29
|
if (node.key?.type === 'Identifier') return node.key.name;
|
|
30
30
|
if (node.key?.type === 'Literal' && typeof node.key.value === 'string') return node.key.value;
|
|
31
|
+
if (node.key?.type === 'PrivateIdentifier') return `#${node.key.name}`;
|
|
31
32
|
|
|
32
33
|
return 'member';
|
|
33
34
|
};
|
|
@@ -87,7 +88,9 @@ const noRedundantInternal = {
|
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
const isPrivateIdentifier = node.key?.type === 'PrivateIdentifier';
|
|
92
|
+
|
|
93
|
+
if (node.accessibility !== 'private' && !isPrivateIdentifier) {
|
|
91
94
|
return;
|
|
92
95
|
}
|
|
93
96
|
|
|
@@ -95,7 +98,7 @@ const noRedundantInternal = {
|
|
|
95
98
|
node: internalComment,
|
|
96
99
|
messageId: 'redundantInternal',
|
|
97
100
|
data: {
|
|
98
|
-
accessibility:
|
|
101
|
+
accessibility: 'private',
|
|
99
102
|
kind: getMemberKind(node),
|
|
100
103
|
name: getMemberName(node),
|
|
101
104
|
},
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
const { getAngularDecoratorName, isImportedAs } = require('./internals/import-resolution');
|
|
5
5
|
|
|
6
|
-
const { getMetadataEntryRemovalRange } = require('./internals/angular-metadata-fix');
|
|
6
|
+
const { getMetadataEntryRemovalRange, getPropertyName } = require('./internals/angular-metadata-fix');
|
|
7
|
+
const { getAngularMajor } = require('./internals/angular-version');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Disallow explicit `changeDetection: ChangeDetectionStrategy.OnPush` on
|
|
@@ -24,53 +25,6 @@ const { getMetadataEntryRemovalRange } = require('./internals/angular-metadata-f
|
|
|
24
25
|
|
|
25
26
|
const REQUIRED_MAJOR = 22;
|
|
26
27
|
|
|
27
|
-
/** @type {number | null | undefined} */
|
|
28
|
-
let cachedAngularMajor;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Detect the workspace's installed Angular major version. Cached across files.
|
|
32
|
-
*
|
|
33
|
-
* @returns {number | null} major version, or null when it cannot be determined
|
|
34
|
-
*/
|
|
35
|
-
const detectAngularMajor = () => {
|
|
36
|
-
if (cachedAngularMajor !== undefined) return cachedAngularMajor;
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
const pkg = require('@angular/core/package.json');
|
|
40
|
-
const major = Number.parseInt(String(pkg.version).split('.')[0], 10);
|
|
41
|
-
cachedAngularMajor = Number.isNaN(major) ? null : major;
|
|
42
|
-
} catch {
|
|
43
|
-
cachedAngularMajor = null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return cachedAngularMajor;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Resolve the Angular major version to gate on. An explicit
|
|
51
|
-
* `settings.ethlete.angularMajor` wins over auto-detection — handy for
|
|
52
|
-
* pinning behaviour and for tests.
|
|
53
|
-
*
|
|
54
|
-
* @param {import('eslint').Rule.RuleContext} context
|
|
55
|
-
* @returns {number | null}
|
|
56
|
-
*/
|
|
57
|
-
const getAngularMajor = (context) => {
|
|
58
|
-
const settings = /** @type {any} */ (context.settings);
|
|
59
|
-
const override = settings?.ethlete?.angularMajor;
|
|
60
|
-
if (typeof override === 'number') return override;
|
|
61
|
-
|
|
62
|
-
return detectAngularMajor();
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* @param {import('estree').Property['key']} key
|
|
67
|
-
*/
|
|
68
|
-
const getPropertyName = (key) => {
|
|
69
|
-
if (key.type === 'Identifier') return key.name;
|
|
70
|
-
if (key.type === 'Literal' && typeof key.value === 'string') return key.value;
|
|
71
|
-
return null;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
28
|
/**
|
|
75
29
|
* @param {import('eslint').SourceCode} sourceCode
|
|
76
30
|
* @param {any} value
|
|
@@ -127,6 +81,7 @@ const noRedundantOnPushChangeDetection = {
|
|
|
127
81
|
docs: {
|
|
128
82
|
description:
|
|
129
83
|
'Disallow redundant `changeDetection: ChangeDetectionStrategy.OnPush`. OnPush is the default since Angular 22.',
|
|
84
|
+
recommended: true,
|
|
130
85
|
},
|
|
131
86
|
fixable: 'code',
|
|
132
87
|
schema: [],
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
const { ANGULAR_CORE, getImportedName } = require('./internals/import-resolution');
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Disallows calling .subscribe() inside Angular's effect() or
|
|
7
|
+
* Disallows calling .subscribe() inside Angular's effect(), computed(), afterRenderEffect() or linkedSignal() callbacks.
|
|
8
8
|
*
|
|
9
9
|
* This creates unmanaged subscriptions and causes memory leaks because neither
|
|
10
10
|
* effect() nor computed() provides a lifecycle-tied cleanup mechanism for subscriptions.
|
|
@@ -24,6 +24,8 @@ const { ANGULAR_CORE, getImportedName } = require('./internals/import-resolution
|
|
|
24
24
|
* .subscribe();
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
const REACTIVE_CONTEXTS = new Set(['effect', 'computed', 'afterRenderEffect', 'linkedSignal']);
|
|
28
|
+
|
|
27
29
|
/**
|
|
28
30
|
* @param {any} node
|
|
29
31
|
*/
|
|
@@ -34,7 +36,8 @@ const noRxjsInEffect = {
|
|
|
34
36
|
meta: {
|
|
35
37
|
type: 'problem',
|
|
36
38
|
docs: {
|
|
37
|
-
description:
|
|
39
|
+
description:
|
|
40
|
+
'Disallow .subscribe() inside effect(), computed(), afterRenderEffect() or linkedSignal() callbacks.',
|
|
38
41
|
recommended: true,
|
|
39
42
|
},
|
|
40
43
|
messages: {
|
|
@@ -45,8 +48,7 @@ const noRxjsInEffect = {
|
|
|
45
48
|
},
|
|
46
49
|
create(context) {
|
|
47
50
|
/**
|
|
48
|
-
* Returns the name of the wrapping reactive context
|
|
49
|
-
* the given node is inside one, or null otherwise.
|
|
51
|
+
* Returns the name of the wrapping reactive context if the given node is inside one, or null otherwise.
|
|
50
52
|
* @param {import('eslint').Rule.Node} node
|
|
51
53
|
* @returns {string | null}
|
|
52
54
|
*/
|
|
@@ -59,7 +61,7 @@ const noRxjsInEffect = {
|
|
|
59
61
|
}
|
|
60
62
|
if (crossedFunctionBoundary && current.type === 'CallExpression') {
|
|
61
63
|
const calleeName = getImportedName(context.sourceCode, current.callee, ANGULAR_CORE);
|
|
62
|
-
if (calleeName
|
|
64
|
+
if (calleeName && REACTIVE_CONTEXTS.has(calleeName)) return calleeName;
|
|
63
65
|
}
|
|
64
66
|
current = current.parent;
|
|
65
67
|
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Disallows SCREAMING_CASE variable names inside function bodies
|
|
5
|
+
* Disallows SCREAMING_CASE variable names inside function bodies, and SCREAMING_CASE names
|
|
6
|
+
* for function-valued variables at any scope.
|
|
6
7
|
*
|
|
7
8
|
* At module level, SCREAMING_CASE is fine for true constants. Inside functions,
|
|
8
9
|
* all local variables must be camelCase — there is no meaningful distinction
|
|
@@ -56,7 +57,7 @@ const noScreamingCaseLocal = {
|
|
|
56
57
|
type: 'suggestion',
|
|
57
58
|
docs: {
|
|
58
59
|
description:
|
|
59
|
-
'Disallow SCREAMING_CASE variable names inside function bodies
|
|
60
|
+
'Disallow SCREAMING_CASE variable names inside function bodies, and SCREAMING_CASE names for function-valued variables at any scope.',
|
|
60
61
|
recommended: true,
|
|
61
62
|
},
|
|
62
63
|
messages: {
|
|
@@ -69,15 +70,12 @@ const noScreamingCaseLocal = {
|
|
|
69
70
|
create(context) {
|
|
70
71
|
return {
|
|
71
72
|
VariableDeclarator(node) {
|
|
72
|
-
// Only flag simple identifier bindings (not destructuring)
|
|
73
73
|
if (node.id.type !== 'Identifier') return;
|
|
74
74
|
|
|
75
75
|
const { name } = node.id;
|
|
76
76
|
|
|
77
|
-
// Must match the SCREAMING_CASE pattern
|
|
78
77
|
if (!SCREAMING_CASE_RE.test(name)) return;
|
|
79
78
|
|
|
80
|
-
// Only flag inside function bodies
|
|
81
79
|
if (!isInsideFunction(node)) return;
|
|
82
80
|
if (node.init?.type === 'ArrowFunctionExpression' || node.init?.type === 'FunctionExpression') return;
|
|
83
81
|
|
|
@@ -20,6 +20,7 @@ const rule = {
|
|
|
20
20
|
type: 'suggestion',
|
|
21
21
|
docs: {
|
|
22
22
|
description: 'Require subscribe() to be called with an empty body. Move side effects into tap() inside the pipe.',
|
|
23
|
+
recommended: true,
|
|
23
24
|
},
|
|
24
25
|
schema: [],
|
|
25
26
|
messages: {
|
|
@@ -44,6 +45,26 @@ const rule = {
|
|
|
44
45
|
return false;
|
|
45
46
|
};
|
|
46
47
|
|
|
48
|
+
/** @param {any} value */
|
|
49
|
+
const isHandlerReference = (value) => {
|
|
50
|
+
if (value.type === 'CallExpression') {
|
|
51
|
+
return (
|
|
52
|
+
value.callee.type === 'MemberExpression' &&
|
|
53
|
+
!value.callee.computed &&
|
|
54
|
+
value.callee.property.type === 'Identifier' &&
|
|
55
|
+
value.callee.property.name === 'bind'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
value.type === 'MemberExpression' &&
|
|
61
|
+
value.object.type === 'ThisExpression' &&
|
|
62
|
+
!value.computed &&
|
|
63
|
+
value.property.type === 'Identifier' &&
|
|
64
|
+
!value.property.name.endsWith('$')
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
47
68
|
return {
|
|
48
69
|
CallExpression(node) {
|
|
49
70
|
const callee = /** @type {any} */ (node.callee);
|
|
@@ -75,12 +96,20 @@ const rule = {
|
|
|
75
96
|
continue;
|
|
76
97
|
}
|
|
77
98
|
|
|
99
|
+
if (isHandlerReference(arg)) {
|
|
100
|
+
context.report({ node, messageId: 'noSubscribeBody' });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
78
104
|
// Observer object: .subscribe({ next: (res) => ..., error: ..., complete: ... })
|
|
79
105
|
if (arg.type === 'ObjectExpression') {
|
|
80
106
|
for (const prop of arg.properties) {
|
|
81
107
|
if (prop.type !== 'Property') continue;
|
|
82
108
|
const val = /** @type {any} */ (prop.value);
|
|
83
|
-
if (
|
|
109
|
+
if (
|
|
110
|
+
((val.type === 'ArrowFunctionExpression' || val.type === 'FunctionExpression') && hasBody(val)) ||
|
|
111
|
+
isHandlerReference(val)
|
|
112
|
+
) {
|
|
84
113
|
context.report({ node, messageId: 'noSubscribeBody' });
|
|
85
114
|
return;
|
|
86
115
|
}
|
|
@@ -112,6 +112,7 @@ const noTemplateLiteralBeforeInlineTemplate = {
|
|
|
112
112
|
docs: {
|
|
113
113
|
description:
|
|
114
114
|
'Disallow a substituted template literal above an inline component template, which silently disables Angular language service completions for the rest of the file.',
|
|
115
|
+
recommended: true,
|
|
115
116
|
},
|
|
116
117
|
schema: [],
|
|
117
118
|
messages: {
|
|
@@ -134,6 +135,11 @@ const noTemplateLiteralBeforeInlineTemplate = {
|
|
|
134
135
|
if (node.expressions.length > 0) interpolatedLiteralStarts.push(node.range[0]);
|
|
135
136
|
},
|
|
136
137
|
|
|
138
|
+
/** @param {any} node */
|
|
139
|
+
TSTemplateLiteralType(node) {
|
|
140
|
+
if (node.types.length > 0) interpolatedLiteralStarts.push(node.range[0]);
|
|
141
|
+
},
|
|
142
|
+
|
|
137
143
|
/** @param {import('eslint').Rule.Node} node */
|
|
138
144
|
Decorator(node) {
|
|
139
145
|
const decoratorName = getAngularDecoratorName(context.sourceCode, node);
|
|
@@ -108,6 +108,23 @@ const noTrivialReturnType = {
|
|
|
108
108
|
return names;
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Whether an identifier is a property name (`a.name`, `{ name: 1 }`) rather than a reference.
|
|
113
|
+
* @param {any} identifier
|
|
114
|
+
*/
|
|
115
|
+
const isNonReference = (identifier) => {
|
|
116
|
+
const parent = identifier.parent;
|
|
117
|
+
|
|
118
|
+
if (!parent || parent.computed) return false;
|
|
119
|
+
if (parent.type === 'MemberExpression') return parent.property === identifier;
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
(parent.type === 'Property' || parent.type === 'PropertyDefinition' || parent.type === 'MethodDefinition') &&
|
|
123
|
+
parent.key === identifier &&
|
|
124
|
+
!parent.shorthand
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
111
128
|
/**
|
|
112
129
|
* Whether the function body references the function itself (recursion) — there the
|
|
113
130
|
* annotation is required, since TypeScript cannot infer a self-dependent return type.
|
|
@@ -135,7 +152,7 @@ const noTrivialReturnType = {
|
|
|
135
152
|
if (typeof current.type !== 'string') continue;
|
|
136
153
|
|
|
137
154
|
for (const { kind, name } of names) {
|
|
138
|
-
if (kind === 'name' && current.type === 'Identifier' && current.name === name) {
|
|
155
|
+
if (kind === 'name' && current.type === 'Identifier' && current.name === name && !isNonReference(current)) {
|
|
139
156
|
return true;
|
|
140
157
|
}
|
|
141
158
|
|
|
@@ -139,7 +139,6 @@ const noUnusedClassMember = {
|
|
|
139
139
|
const registerMember = (node) => {
|
|
140
140
|
const frame = classStack[classStack.length - 1];
|
|
141
141
|
if (!frame) return;
|
|
142
|
-
// Only register members that belong directly to this class body
|
|
143
142
|
if (node.parent !== frame.classBody) return;
|
|
144
143
|
|
|
145
144
|
if (node.static) return;
|
|
@@ -189,6 +188,7 @@ const noUnusedClassMember = {
|
|
|
189
188
|
|
|
190
189
|
PropertyDefinition: registerMember,
|
|
191
190
|
MethodDefinition: registerMember,
|
|
191
|
+
AccessorProperty: registerMember,
|
|
192
192
|
|
|
193
193
|
MemberExpression(node) {
|
|
194
194
|
if (node.object?.type !== 'ThisExpression') return;
|