@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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const ANGULAR_CORE = '@angular/core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
8
|
+
* @param {any} identifier
|
|
9
|
+
*/
|
|
10
|
+
const findVariable = (sourceCode, identifier) => {
|
|
11
|
+
/** @type {import('eslint').Scope.Scope | null} */
|
|
12
|
+
let scope = sourceCode.getScope(identifier);
|
|
13
|
+
|
|
14
|
+
while (scope) {
|
|
15
|
+
const variable = scope.set.get(identifier.name);
|
|
16
|
+
if (variable) return variable;
|
|
17
|
+
scope = scope.upper;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {any} specifier
|
|
25
|
+
*/
|
|
26
|
+
const getSpecifierImportedName = (specifier) => {
|
|
27
|
+
if (specifier.type === 'ImportDefaultSpecifier') return 'default';
|
|
28
|
+
if (specifier.type === 'ImportNamespaceSpecifier') return '*';
|
|
29
|
+
|
|
30
|
+
return specifier.imported.name ?? specifier.imported.value;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* What an identifier is bound to: `{ source, name }` for an import (`name` is the exported
|
|
35
|
+
* name, `'*'` for a namespace), `{ source: null, name }` for an undeclared global, and `null`
|
|
36
|
+
* for a local binding.
|
|
37
|
+
*
|
|
38
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
39
|
+
* @param {any} identifier
|
|
40
|
+
* @returns {{ source: string | null; name: string } | null}
|
|
41
|
+
*/
|
|
42
|
+
const resolveIdentifier = (sourceCode, identifier) => {
|
|
43
|
+
const variable = findVariable(sourceCode, identifier);
|
|
44
|
+
if (!variable || variable.defs.length === 0) return { source: null, name: identifier.name };
|
|
45
|
+
|
|
46
|
+
const definition = /** @type {any} */ (variable.defs[0]);
|
|
47
|
+
if (definition.type !== 'ImportBinding') return null;
|
|
48
|
+
|
|
49
|
+
return { source: definition.parent.source.value, name: getSpecifierImportedName(definition.node) };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The exported name `node` refers to when it is imported from one of `sources`, directly, under
|
|
54
|
+
* an alias, or as `namespace.name`. An undeclared identifier falls back to its own name, so code
|
|
55
|
+
* without an import statement still matches; any other binding yields `null`.
|
|
56
|
+
*
|
|
57
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
58
|
+
* @param {any} node
|
|
59
|
+
* @param {string | readonly string[]} sources
|
|
60
|
+
* @returns {string | null}
|
|
61
|
+
*/
|
|
62
|
+
const getImportedName = (sourceCode, node, sources) => {
|
|
63
|
+
const sourceList = typeof sources === 'string' ? [sources] : sources;
|
|
64
|
+
|
|
65
|
+
if (node?.type === 'Identifier') {
|
|
66
|
+
const resolved = resolveIdentifier(sourceCode, node);
|
|
67
|
+
if (!resolved) return null;
|
|
68
|
+
if (resolved.source === null) return resolved.name;
|
|
69
|
+
|
|
70
|
+
return sourceList.includes(resolved.source) && resolved.name !== '*' ? resolved.name : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
node?.type === 'MemberExpression' &&
|
|
75
|
+
!node.computed &&
|
|
76
|
+
node.object.type === 'Identifier' &&
|
|
77
|
+
node.property.type === 'Identifier'
|
|
78
|
+
) {
|
|
79
|
+
const resolved = resolveIdentifier(sourceCode, node.object);
|
|
80
|
+
if (!resolved || resolved.name !== '*' || resolved.source === null) return null;
|
|
81
|
+
|
|
82
|
+
return sourceList.includes(resolved.source) ? node.property.name : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
90
|
+
* @param {any} node
|
|
91
|
+
* @param {string} name
|
|
92
|
+
* @param {string | readonly string[]} [sources]
|
|
93
|
+
*/
|
|
94
|
+
const isImportedAs = (sourceCode, node, name, sources = ANGULAR_CORE) =>
|
|
95
|
+
getImportedName(sourceCode, node, sources) === name;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The `@angular/core` name of a decorator such as `@Component(...)`, `@Cmp(...)` or
|
|
99
|
+
* `@ng.Component(...)`.
|
|
100
|
+
*
|
|
101
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
102
|
+
* @param {any} decorator
|
|
103
|
+
*/
|
|
104
|
+
const getAngularDecoratorName = (sourceCode, decorator) => {
|
|
105
|
+
if (decorator?.type !== 'Decorator') return null;
|
|
106
|
+
|
|
107
|
+
const expression = decorator.expression;
|
|
108
|
+
const target = expression.type === 'CallExpression' ? expression.callee : expression;
|
|
109
|
+
|
|
110
|
+
return getImportedName(sourceCode, target, ANGULAR_CORE);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The local name of a value import `{ importedName as local } from source`, or `null` when the
|
|
115
|
+
* file does not import it as a value.
|
|
116
|
+
*
|
|
117
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
118
|
+
* @param {string} source
|
|
119
|
+
* @param {string} importedName
|
|
120
|
+
*/
|
|
121
|
+
const getImportLocalName = (sourceCode, source, importedName) => {
|
|
122
|
+
for (const node of sourceCode.ast.body) {
|
|
123
|
+
if (node.type !== 'ImportDeclaration' || node.source.value !== source) continue;
|
|
124
|
+
if (/** @type {any} */ (node).importKind === 'type') continue;
|
|
125
|
+
|
|
126
|
+
for (const specifier of node.specifiers) {
|
|
127
|
+
if (specifier.type !== 'ImportSpecifier' || /** @type {any} */ (specifier).importKind === 'type') continue;
|
|
128
|
+
if (getSpecifierImportedName(specifier) === importedName) return specifier.local.name;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Whether `identifier` refers to a global such as `document` or `setTimeout`, rather than a local,
|
|
137
|
+
* parameter or import of the same name.
|
|
138
|
+
*
|
|
139
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
140
|
+
* @param {any} identifier
|
|
141
|
+
*/
|
|
142
|
+
const isGlobalReference = (sourceCode, identifier) =>
|
|
143
|
+
identifier?.type === 'Identifier' && resolveIdentifier(sourceCode, identifier)?.source === null;
|
|
144
|
+
|
|
145
|
+
const MODULE_REFERENCE_SELECTOR =
|
|
146
|
+
'ImportDeclaration, ExportNamedDeclaration[source], ExportAllDeclaration, ImportExpression';
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The module an import, re-export or dynamic `import('…')` references, with the names it takes by
|
|
150
|
+
* name. `export *`, a namespace or default import, and a dynamic import have no named entries.
|
|
151
|
+
*
|
|
152
|
+
* @param {any} node a node matched by `MODULE_REFERENCE_SELECTOR`
|
|
153
|
+
* @returns {{ source: string; named: { name: string; node: any }[] } | null}
|
|
154
|
+
*/
|
|
155
|
+
const getModuleReference = (node) => {
|
|
156
|
+
const sourceNode = node.source;
|
|
157
|
+
let source = null;
|
|
158
|
+
if (sourceNode?.type === 'Literal' && typeof sourceNode.value === 'string') source = sourceNode.value;
|
|
159
|
+
if (sourceNode?.type === 'TemplateLiteral' && sourceNode.expressions.length === 0) {
|
|
160
|
+
source = sourceNode.quasis[0].value.cooked;
|
|
161
|
+
}
|
|
162
|
+
if (source === null) return null;
|
|
163
|
+
|
|
164
|
+
/** @type {{ name: string; node: any }[]} */
|
|
165
|
+
const named = [];
|
|
166
|
+
if (node.type === 'ImportDeclaration') {
|
|
167
|
+
for (const specifier of node.specifiers) {
|
|
168
|
+
if (specifier.type !== 'ImportSpecifier') continue;
|
|
169
|
+
named.push({ name: getSpecifierImportedName(specifier), node: specifier });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (node.type === 'ExportNamedDeclaration') {
|
|
173
|
+
for (const specifier of node.specifiers) {
|
|
174
|
+
named.push({ name: specifier.local.name ?? specifier.local.value, node: specifier });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { source, named };
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
ANGULAR_CORE,
|
|
183
|
+
MODULE_REFERENCE_SELECTOR,
|
|
184
|
+
getAngularDecoratorName,
|
|
185
|
+
getImportedName,
|
|
186
|
+
getImportLocalName,
|
|
187
|
+
getModuleReference,
|
|
188
|
+
isGlobalReference,
|
|
189
|
+
isImportedAs,
|
|
190
|
+
resolveIdentifier,
|
|
191
|
+
};
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
* injectQueryParamChanges(), injectPathParamChanges()
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
+
const { MODULE_REFERENCE_SELECTOR, getModuleReference, isImportedAs } = require('./internals/import-resolution');
|
|
39
|
+
|
|
40
|
+
const ANGULAR_ROUTER = '@angular/router';
|
|
41
|
+
|
|
38
42
|
/**
|
|
39
43
|
* Router properties that expose route state and must not be read directly.
|
|
40
44
|
* @type {Map<string, string>}
|
|
@@ -66,6 +70,17 @@ const noAngularRouterApi = {
|
|
|
66
70
|
schema: [],
|
|
67
71
|
},
|
|
68
72
|
create(context) {
|
|
73
|
+
const sourceCode = context.sourceCode;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {any} call
|
|
77
|
+
* @param {string} token
|
|
78
|
+
*/
|
|
79
|
+
const isInjectOf = (call, token) =>
|
|
80
|
+
call?.type === 'CallExpression' &&
|
|
81
|
+
isImportedAs(sourceCode, call.callee, 'inject') &&
|
|
82
|
+
isImportedAs(sourceCode, call.arguments[0], token, ANGULAR_ROUTER);
|
|
83
|
+
|
|
69
84
|
/**
|
|
70
85
|
* Names of variables/properties assigned from inject(Router).
|
|
71
86
|
* We track identifiers whose initializer is inject(Router) so we can
|
|
@@ -78,28 +93,14 @@ const noAngularRouterApi = {
|
|
|
78
93
|
// ── Track inject(Router) bindings ──────────────────────────────────────
|
|
79
94
|
// const router = inject(Router);
|
|
80
95
|
VariableDeclarator(node) {
|
|
81
|
-
if (
|
|
82
|
-
node.init?.type === 'CallExpression' &&
|
|
83
|
-
node.init.callee.type === 'Identifier' &&
|
|
84
|
-
node.init.callee.name === 'inject' &&
|
|
85
|
-
node.init.arguments[0]?.type === 'Identifier' &&
|
|
86
|
-
node.init.arguments[0].name === 'Router' &&
|
|
87
|
-
node.id.type === 'Identifier'
|
|
88
|
-
) {
|
|
96
|
+
if (isInjectOf(node.init, 'Router') && node.id.type === 'Identifier') {
|
|
89
97
|
routerBindings.add(node.id.name);
|
|
90
98
|
}
|
|
91
99
|
},
|
|
92
100
|
|
|
93
101
|
// ── Track class property assignments: router = inject(Router) ──────────
|
|
94
102
|
PropertyDefinition(node) {
|
|
95
|
-
if (
|
|
96
|
-
node.value?.type === 'CallExpression' &&
|
|
97
|
-
node.value.callee.type === 'Identifier' &&
|
|
98
|
-
node.value.callee.name === 'inject' &&
|
|
99
|
-
node.value.arguments[0]?.type === 'Identifier' &&
|
|
100
|
-
node.value.arguments[0].name === 'Router' &&
|
|
101
|
-
node.key.type === 'Identifier'
|
|
102
|
-
) {
|
|
103
|
+
if (isInjectOf(node.value, 'Router') && node.key.type === 'Identifier') {
|
|
103
104
|
routerBindings.add(node.key.name);
|
|
104
105
|
}
|
|
105
106
|
},
|
|
@@ -130,24 +131,17 @@ const noAngularRouterApi = {
|
|
|
130
131
|
},
|
|
131
132
|
|
|
132
133
|
// ── ActivatedRoute import ───────────────────────────────────────────────
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
context.report({ node, messageId: 'noActivatedRoute' });
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
134
|
+
[MODULE_REFERENCE_SELECTOR](node) {
|
|
135
|
+
const reference = getModuleReference(node);
|
|
136
|
+
if (reference?.source !== ANGULAR_ROUTER) return;
|
|
137
|
+
if (reference.named.some((entry) => entry.name === 'ActivatedRoute')) {
|
|
138
|
+
context.report({ node, messageId: 'noActivatedRoute' });
|
|
141
139
|
}
|
|
142
140
|
},
|
|
143
141
|
|
|
144
142
|
// ── inject(ActivatedRoute) ──────────────────────────────────────────────
|
|
145
143
|
CallExpression(node) {
|
|
146
|
-
|
|
147
|
-
if (callee.type !== 'Identifier' || callee.name !== 'inject') return;
|
|
148
|
-
const arg = node.arguments[0];
|
|
149
|
-
if (arg?.type !== 'Identifier') return;
|
|
150
|
-
if (arg.name === 'ActivatedRoute') {
|
|
144
|
+
if (isInjectOf(node, 'ActivatedRoute')) {
|
|
151
145
|
context.report({ node, messageId: 'noActivatedRoute' });
|
|
152
146
|
}
|
|
153
147
|
},
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const {
|
|
5
|
+
MODULE_REFERENCE_SELECTOR,
|
|
6
|
+
getImportedName,
|
|
7
|
+
getModuleReference,
|
|
8
|
+
isImportedAs,
|
|
9
|
+
} = require('./internals/import-resolution');
|
|
10
|
+
|
|
4
11
|
/**
|
|
5
12
|
* Disallows injecting Angular's `Title` and `Meta` services from
|
|
6
13
|
* `@angular/platform-browser` and importing them from that module.
|
|
@@ -48,11 +55,11 @@ const rule = {
|
|
|
48
55
|
*/
|
|
49
56
|
const injectTokenName = (node) => {
|
|
50
57
|
if (node.type !== 'CallExpression') return null;
|
|
51
|
-
if (
|
|
58
|
+
if (!isImportedAs(context.sourceCode, node.callee, 'inject')) return null;
|
|
52
59
|
const firstArg = node.arguments[0];
|
|
53
60
|
if (!firstArg) return null;
|
|
54
|
-
|
|
55
|
-
return
|
|
61
|
+
|
|
62
|
+
return getImportedName(context.sourceCode, firstArg, '@angular/platform-browser');
|
|
56
63
|
};
|
|
57
64
|
|
|
58
65
|
return {
|
|
@@ -64,13 +71,11 @@ const rule = {
|
|
|
64
71
|
},
|
|
65
72
|
|
|
66
73
|
// Detect: import { Title, Meta } from '@angular/platform-browser'
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
74
|
+
[MODULE_REFERENCE_SELECTOR](node) {
|
|
75
|
+
const reference = getModuleReference(node);
|
|
76
|
+
if (reference?.source !== '@angular/platform-browser') return;
|
|
70
77
|
|
|
71
|
-
for (const specifier of
|
|
72
|
-
if (specifier.type !== 'ImportSpecifier') continue;
|
|
73
|
-
const imported = specifier.imported.name ?? specifier.imported.value;
|
|
78
|
+
for (const { name: imported, node: specifier } of reference.named) {
|
|
74
79
|
if (imported === 'Title') context.report({ node: specifier, messageId: 'noImportTitle' });
|
|
75
80
|
if (imported === 'Meta') context.report({ node: specifier, messageId: 'noImportMeta' });
|
|
76
81
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const { createRequire } = require('node:module');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const fs = require('node:fs');
|
|
7
|
+
const { MODULE_REFERENCE_SELECTOR, getModuleReference } = require('./internals/import-resolution');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Disallows importing from `@ethlete/cdk`, the maintenance-mode predecessor of `@ethlete/components`,
|
|
@@ -52,13 +53,6 @@ const loadMigrationMap = (mapPath, cwd) => {
|
|
|
52
53
|
return map;
|
|
53
54
|
};
|
|
54
55
|
|
|
55
|
-
/** @param {any} specifier */
|
|
56
|
-
const importedName = (specifier) => {
|
|
57
|
-
if (specifier.type !== 'ImportSpecifier') return null;
|
|
58
|
-
|
|
59
|
-
return specifier.imported.name ?? specifier.imported.value ?? null;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
56
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
63
57
|
const noCdkImport = {
|
|
64
58
|
meta: {
|
|
@@ -95,13 +89,13 @@ const noCdkImport = {
|
|
|
95
89
|
const entryFor = (name) => map?.[name] ?? null;
|
|
96
90
|
|
|
97
91
|
return {
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
const source =
|
|
92
|
+
[MODULE_REFERENCE_SELECTOR](node) {
|
|
93
|
+
const reference = getModuleReference(node);
|
|
94
|
+
const source = reference?.source;
|
|
101
95
|
|
|
102
|
-
if (
|
|
96
|
+
if (!reference || (source !== CDK_PACKAGE && !source?.startsWith(`${CDK_PACKAGE}/`))) return;
|
|
103
97
|
|
|
104
|
-
const named
|
|
98
|
+
const { named } = reference;
|
|
105
99
|
|
|
106
100
|
if (!named.length) {
|
|
107
101
|
context.report({ node, messageId: 'module', data: { cdk: CDK_PACKAGE, docs: migrationDocs } });
|
|
@@ -109,8 +103,7 @@ const noCdkImport = {
|
|
|
109
103
|
return;
|
|
110
104
|
}
|
|
111
105
|
|
|
112
|
-
for (const specifier of named) {
|
|
113
|
-
const name = /** @type {string} */ (importedName(specifier));
|
|
106
|
+
for (const { name, node: specifier } of named) {
|
|
114
107
|
const entry = entryFor(name);
|
|
115
108
|
|
|
116
109
|
if (!entry) {
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
* deleteCookie('name');
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
const { isGlobalReference } = require('./internals/import-resolution');
|
|
25
|
+
|
|
24
26
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
25
27
|
const noDocumentCookie = {
|
|
26
28
|
meta: {
|
|
@@ -42,6 +44,7 @@ const noDocumentCookie = {
|
|
|
42
44
|
if (
|
|
43
45
|
node.object.type === 'Identifier' &&
|
|
44
46
|
node.object.name === 'document' &&
|
|
47
|
+
isGlobalReference(context.sourceCode, node.object) &&
|
|
45
48
|
node.property.type === 'Identifier' &&
|
|
46
49
|
node.property.name === 'cookie'
|
|
47
50
|
) {
|
|
@@ -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 returning a cleanup function from an `effect()` callback.
|
|
6
8
|
*
|
|
@@ -115,7 +117,9 @@ const noEffectCleanupReturn = {
|
|
|
115
117
|
CallExpression(node) {
|
|
116
118
|
const { callee } = node;
|
|
117
119
|
|
|
118
|
-
|
|
120
|
+
const calleeName = getImportedName(context.sourceCode, callee, ANGULAR_CORE);
|
|
121
|
+
|
|
122
|
+
if (!calleeName || !EFFECT_CALLEES.has(calleeName)) {
|
|
119
123
|
return;
|
|
120
124
|
}
|
|
121
125
|
|
|
@@ -130,7 +134,7 @@ const noEffectCleanupReturn = {
|
|
|
130
134
|
context.report({
|
|
131
135
|
node: callback.body,
|
|
132
136
|
messageId: 'returnedCleanup',
|
|
133
|
-
data: { callee:
|
|
137
|
+
data: { callee: calleeName },
|
|
134
138
|
});
|
|
135
139
|
|
|
136
140
|
return;
|
|
@@ -163,7 +167,7 @@ const noEffectCleanupReturn = {
|
|
|
163
167
|
context.report({
|
|
164
168
|
node: statement.argument,
|
|
165
169
|
messageId: 'returnedCleanup',
|
|
166
|
-
data: { callee:
|
|
170
|
+
data: { callee: calleeName },
|
|
167
171
|
fix:
|
|
168
172
|
fixable && openParen
|
|
169
173
|
? (fixer) => [
|
|
@@ -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'} 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
|
const MESSAGE_IDS = {
|
|
34
20
|
hostDirectives: 'noEmptyHostDirectives',
|
|
35
21
|
imports: 'noEmptyImports',
|
|
@@ -55,7 +41,7 @@ const noEmptyAngularMetadataArrays = {
|
|
|
55
41
|
return {
|
|
56
42
|
/** @param {import('eslint').Rule.Node} node */
|
|
57
43
|
Decorator(node) {
|
|
58
|
-
const decoratorName =
|
|
44
|
+
const decoratorName = getAngularDecoratorName(context.sourceCode, node);
|
|
59
45
|
if (decoratorName !== 'Component' && decoratorName !== 'Directive') return;
|
|
60
46
|
|
|
61
47
|
const decorator = /** @type {any} */ (node);
|
|
@@ -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 noInjectChain = {
|
|
6
8
|
meta: {
|
|
@@ -19,7 +21,7 @@ const noInjectChain = {
|
|
|
19
21
|
return {
|
|
20
22
|
MemberExpression(node) {
|
|
21
23
|
const obj = node.object;
|
|
22
|
-
if (obj.type === 'CallExpression' &&
|
|
24
|
+
if (obj.type === 'CallExpression' && isImportedAs(context.sourceCode, obj.callee, 'inject')) {
|
|
23
25
|
// Allow inject(X).method() — immediately-invoked patterns like
|
|
24
26
|
// inject(DestroyRef).onDestroy(...) are intentional Angular idioms.
|
|
25
27
|
const parent = node.parent;
|
|
@@ -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) {
|