@ethlete/eslint-plugin 1.0.0-next.25 → 1.0.0-next.27
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 +35 -0
- package/README.md +4 -0
- package/migrations.json +1 -0
- package/package.json +2 -1
- package/src/configs/recommended.js +16 -18
- 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 +66 -8
- package/src/rules/enforce-routing-view-naming.js +47 -24
- 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-inject-chain.js +5 -1
- package/src/rules/no-leading-underscore-class-member.js +27 -2
- package/src/rules/no-legacy-angular-decorators.js +19 -16
- 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-in-pipe.js +5 -1
- 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-trivial-wrapper-method.js +39 -1
- package/src/rules/no-type-only-import.js +23 -19
- package/src/rules/no-unused-class-member.js +14 -6
- 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 +13 -20
|
@@ -12,7 +12,8 @@ const { getImportedName } = require('./internals/import-resolution');
|
|
|
12
12
|
* - `new Subject / BehaviorSubject / ReplaySubject / AsyncSubject / Observable()`
|
|
13
13
|
* - a call to a known RxJS creator: from, of, interval, timer, fromEvent,
|
|
14
14
|
* fromEventPattern, combineLatest, merge, concat, forkJoin, race, zip,
|
|
15
|
-
* defer, iif, throwError, using
|
|
15
|
+
* defer, iif, throwError, using
|
|
16
|
+
* - `toObservable(...)` or `.asObservable()`
|
|
16
17
|
*
|
|
17
18
|
* ❌ const myData = someObs.pipe(map(...));
|
|
18
19
|
* ❌ const clicks = fromEvent(el, 'click');
|
|
@@ -40,7 +41,6 @@ const RXJS_CREATORS = new Set([
|
|
|
40
41
|
'iif',
|
|
41
42
|
'throwError',
|
|
42
43
|
'using',
|
|
43
|
-
'partition',
|
|
44
44
|
]);
|
|
45
45
|
|
|
46
46
|
const RXJS_SUBJECT_CTORS = new Set(['Subject', 'BehaviorSubject', 'ReplaySubject', 'AsyncSubject', 'Observable']);
|
|
@@ -58,7 +58,7 @@ const isObservableInit = (sourceCode, node) => {
|
|
|
58
58
|
node.type === 'CallExpression' &&
|
|
59
59
|
node.callee.type === 'MemberExpression' &&
|
|
60
60
|
node.callee.property.type === 'Identifier' &&
|
|
61
|
-
node.callee.property.name === 'pipe'
|
|
61
|
+
(node.callee.property.name === 'pipe' || node.callee.property.name === 'asObservable')
|
|
62
62
|
) {
|
|
63
63
|
return true;
|
|
64
64
|
}
|
|
@@ -68,6 +68,13 @@ const isObservableInit = (sourceCode, node) => {
|
|
|
68
68
|
return true;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
if (
|
|
72
|
+
node.type === 'CallExpression' &&
|
|
73
|
+
getImportedName(sourceCode, node.callee, '@angular/core/rxjs-interop') === 'toObservable'
|
|
74
|
+
) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
// new Subject(), new BehaviorSubject(...), etc.
|
|
72
79
|
if (node.type === 'NewExpression' && RXJS_SUBJECT_CTORS.has(getImportedName(sourceCode, node.callee, 'rxjs') ?? '')) {
|
|
73
80
|
return true;
|
|
@@ -82,6 +89,7 @@ const rule = {
|
|
|
82
89
|
type: 'suggestion',
|
|
83
90
|
docs: {
|
|
84
91
|
description: 'Require observable variables and class properties to use the $ suffix.',
|
|
92
|
+
recommended: true,
|
|
85
93
|
},
|
|
86
94
|
schema: [],
|
|
87
95
|
messages: {
|
|
@@ -28,7 +28,9 @@ const handlesSubmit = (node) =>
|
|
|
28
28
|
|
|
29
29
|
/** @param {any} node */
|
|
30
30
|
const submitsNatively = (node) =>
|
|
31
|
-
hasBinding(node, 'action') ||
|
|
31
|
+
hasBinding(node, 'action') ||
|
|
32
|
+
hasBinding(node, 'ngNoForm') ||
|
|
33
|
+
attribute(node, 'method')?.value?.trim().toLowerCase() === 'dialog';
|
|
32
34
|
|
|
33
35
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
34
36
|
const requireFormSubmit = {
|
|
@@ -36,6 +38,7 @@ const requireFormSubmit = {
|
|
|
36
38
|
type: 'problem',
|
|
37
39
|
docs: {
|
|
38
40
|
description: 'Require a `<form>` to handle its own submission.',
|
|
41
|
+
recommended: true,
|
|
39
42
|
},
|
|
40
43
|
schema: [],
|
|
41
44
|
messages: {
|
|
@@ -1,266 +1,13 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
getImportLocalName,
|
|
8
|
-
isImportedAs,
|
|
9
|
-
resolveIdentifier,
|
|
10
|
-
} = require('./internals/import-resolution');
|
|
4
|
+
const { getAngularMajor } = require('./internals/angular-version');
|
|
5
|
+
const { buildRequiredEnumFix, getPropertyName } = require('./internals/angular-metadata-fix');
|
|
6
|
+
const { getAngularDecoratorName, isImportedAs } = require('./internals/import-resolution');
|
|
11
7
|
|
|
12
|
-
const COMPONENT_ORDER = ['selector', 'template', 'styleUrl', 'encapsulation', 'changeDetection'];
|
|
13
|
-
|
|
14
|
-
// OnPush became the default change detection strategy in Angular 22, so from
|
|
15
|
-
// v22 onward it must NOT be declared (see no-redundant-on-push-change-detection).
|
|
16
|
-
// This rule therefore only applies to older Angular, where OnPush is opt-in.
|
|
17
8
|
const MAX_MAJOR = 21;
|
|
18
9
|
|
|
19
|
-
|
|
20
|
-
let cachedAngularMajor;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Detect the workspace's installed Angular major version. Cached across files.
|
|
24
|
-
*
|
|
25
|
-
* @returns {number | null} major version, or null when it cannot be determined
|
|
26
|
-
*/
|
|
27
|
-
const detectAngularMajor = () => {
|
|
28
|
-
if (cachedAngularMajor !== undefined) return cachedAngularMajor;
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const pkg = require('@angular/core/package.json');
|
|
32
|
-
const major = Number.parseInt(String(pkg.version).split('.')[0], 10);
|
|
33
|
-
cachedAngularMajor = Number.isNaN(major) ? null : major;
|
|
34
|
-
} catch {
|
|
35
|
-
cachedAngularMajor = null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return cachedAngularMajor;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Resolve the Angular major version to gate on. An explicit
|
|
43
|
-
* `settings.ethlete.angularMajor` wins over auto-detection — handy for
|
|
44
|
-
* pinning behaviour and for tests.
|
|
45
|
-
*
|
|
46
|
-
* @param {import('eslint').Rule.RuleContext} context
|
|
47
|
-
* @returns {number | null}
|
|
48
|
-
*/
|
|
49
|
-
const getAngularMajor = (context) => {
|
|
50
|
-
const settings = /** @type {any} */ (context.settings);
|
|
51
|
-
const override = settings?.ethlete?.angularMajor;
|
|
52
|
-
if (typeof override === 'number') return override;
|
|
53
|
-
|
|
54
|
-
return detectAngularMajor();
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* @param {import('estree').Property['key']} key
|
|
59
|
-
*/
|
|
60
|
-
const getPropertyName = (key) => {
|
|
61
|
-
if (key.type === 'Identifier') return key.name;
|
|
62
|
-
if (key.type === 'Literal' && typeof key.value === 'string') return key.value;
|
|
63
|
-
return null;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* @param {string | null} propertyName
|
|
68
|
-
*/
|
|
69
|
-
const getOrderKey = (propertyName) => {
|
|
70
|
-
if (propertyName === 'template' || propertyName === 'templateUrl') return 'template';
|
|
71
|
-
if (propertyName === 'styleUrl' || propertyName === 'styleUrls') return 'styleUrl';
|
|
72
|
-
return propertyName;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
77
|
-
* @param {import('eslint').AST.Token | import('eslint').Rule.Node} node
|
|
78
|
-
*/
|
|
79
|
-
const getIndent = (sourceCode, node) => {
|
|
80
|
-
if (!node.loc) return '';
|
|
81
|
-
|
|
82
|
-
const line = sourceCode.lines[node.loc.start.line - 1] ?? '';
|
|
83
|
-
return line.slice(0, node.loc.start.column);
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
88
|
-
* @param {any} property
|
|
89
|
-
*/
|
|
90
|
-
const hasTrailingComma = (sourceCode, property) => {
|
|
91
|
-
const tokenAfter = sourceCode.getTokenAfter(property);
|
|
92
|
-
return Boolean(tokenAfter && tokenAfter.type === 'Punctuator' && tokenAfter.value === ',');
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
97
|
-
*/
|
|
98
|
-
const findAngularCoreImport = (sourceCode) => {
|
|
99
|
-
const imports = sourceCode.ast.body.filter(
|
|
100
|
-
(node) =>
|
|
101
|
-
node.type === 'ImportDeclaration' && node.source.type === 'Literal' && node.source.value === '@angular/core',
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
imports.find((node) =>
|
|
106
|
-
node.specifiers.some(
|
|
107
|
-
(specifier) =>
|
|
108
|
-
specifier.type === 'ImportSpecifier' &&
|
|
109
|
-
specifier.imported.type === 'Identifier' &&
|
|
110
|
-
specifier.imported.name === 'ChangeDetectionStrategy',
|
|
111
|
-
),
|
|
112
|
-
) ??
|
|
113
|
-
imports.find((node) => node.importKind !== 'type') ??
|
|
114
|
-
null
|
|
115
|
-
);
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
120
|
-
* @param {any} importNode
|
|
121
|
-
*/
|
|
122
|
-
const buildAngularCoreImportFix = (sourceCode, importNode) => {
|
|
123
|
-
const specifiers = importNode.specifiers ?? [];
|
|
124
|
-
const hasChangeDetectionStrategy = specifiers.some(
|
|
125
|
-
(specifier) =>
|
|
126
|
-
specifier.type === 'ImportSpecifier' &&
|
|
127
|
-
specifier.imported.type === 'Identifier' &&
|
|
128
|
-
specifier.imported.name === 'ChangeDetectionStrategy',
|
|
129
|
-
);
|
|
130
|
-
if (hasChangeDetectionStrategy) return null;
|
|
131
|
-
|
|
132
|
-
const defaultSpecifier = specifiers.find((specifier) => specifier.type === 'ImportDefaultSpecifier') ?? null;
|
|
133
|
-
const namespaceSpecifier = specifiers.find((specifier) => specifier.type === 'ImportNamespaceSpecifier') ?? null;
|
|
134
|
-
const namedSpecifiers = specifiers.filter((specifier) => specifier.type === 'ImportSpecifier');
|
|
135
|
-
|
|
136
|
-
if (namespaceSpecifier) {
|
|
137
|
-
return (fixer) => fixer.insertTextAfter(importNode, `\nimport { ChangeDetectionStrategy } from '@angular/core';`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const importParts = [];
|
|
141
|
-
if (defaultSpecifier) {
|
|
142
|
-
importParts.push(sourceCode.getText(defaultSpecifier));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const namedTexts = namedSpecifiers.map((specifier) => sourceCode.getText(specifier));
|
|
146
|
-
importParts.push(`{ ${[...namedTexts, 'ChangeDetectionStrategy'].join(', ')} }`);
|
|
147
|
-
|
|
148
|
-
return (fixer) => fixer.replaceText(importNode, `import ${importParts.join(', ')} from '@angular/core';`);
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
153
|
-
*/
|
|
154
|
-
const buildMissingAngularCoreImportFix = (sourceCode) => {
|
|
155
|
-
const importDeclarations = sourceCode.ast.body.filter((node) => node.type === 'ImportDeclaration');
|
|
156
|
-
const lastImport = importDeclarations[importDeclarations.length - 1] ?? null;
|
|
157
|
-
|
|
158
|
-
if (lastImport) {
|
|
159
|
-
return (fixer) => fixer.insertTextAfter(lastImport, `\nimport { ChangeDetectionStrategy } from '@angular/core';`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const firstNode = sourceCode.ast.body[0];
|
|
163
|
-
const lastComment = firstNode ? sourceCode.getCommentsBefore(firstNode).at(-1) : null;
|
|
164
|
-
|
|
165
|
-
return lastComment
|
|
166
|
-
? (fixer) => fixer.insertTextAfter(lastComment, `\nimport { ChangeDetectionStrategy } from '@angular/core';\n`)
|
|
167
|
-
: (fixer) =>
|
|
168
|
-
fixer.replaceTextRange(
|
|
169
|
-
[0, firstNode?.range[0] ?? 0],
|
|
170
|
-
`import { ChangeDetectionStrategy } from '@angular/core';\n\n`,
|
|
171
|
-
);
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* @param {any} metadata
|
|
176
|
-
*/
|
|
177
|
-
const getChangeDetectionInsertionTarget = (metadata) => {
|
|
178
|
-
if (metadata.properties.some((property) => property.type === 'SpreadElement')) return null;
|
|
179
|
-
|
|
180
|
-
const changeDetectionIndex = COMPONENT_ORDER.indexOf('changeDetection');
|
|
181
|
-
|
|
182
|
-
return (
|
|
183
|
-
metadata.properties.find((property) => {
|
|
184
|
-
if (property.type !== 'Property') return false;
|
|
185
|
-
|
|
186
|
-
const propertyName = getOrderKey(getPropertyName(property.key));
|
|
187
|
-
const propertyIndex = propertyName === null ? COMPONENT_ORDER.length : COMPONENT_ORDER.indexOf(propertyName);
|
|
188
|
-
return propertyIndex > changeDetectionIndex || propertyIndex === -1;
|
|
189
|
-
}) ?? null
|
|
190
|
-
);
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
195
|
-
* @param {any} metadata
|
|
196
|
-
*/
|
|
197
|
-
const buildMetadataFix = (sourceCode, metadata) => {
|
|
198
|
-
const changeDetectionText = `changeDetection: ${getImportLocalName(sourceCode, ANGULAR_CORE, 'ChangeDetectionStrategy') ?? 'ChangeDetectionStrategy'}.OnPush`;
|
|
199
|
-
const properties = metadata.properties;
|
|
200
|
-
const insertionTarget = getChangeDetectionInsertionTarget(metadata);
|
|
201
|
-
const isMultiline = Boolean(metadata.loc && metadata.loc.start.line !== metadata.loc.end.line);
|
|
202
|
-
|
|
203
|
-
if (properties.length === 0) {
|
|
204
|
-
return (fixer) => fixer.replaceText(metadata, `{ ${changeDetectionText} }`);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (insertionTarget) {
|
|
208
|
-
if (isMultiline) {
|
|
209
|
-
const propertyIndent = getIndent(sourceCode, insertionTarget);
|
|
210
|
-
return (fixer) => fixer.insertTextBefore(insertionTarget, `${changeDetectionText},\n${propertyIndent}`);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return (fixer) => fixer.insertTextBefore(insertionTarget, `${changeDetectionText}, `);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const lastProperty = properties[properties.length - 1];
|
|
217
|
-
const closingBrace = sourceCode.getLastToken(metadata);
|
|
218
|
-
if (!lastProperty || !closingBrace) return null;
|
|
219
|
-
|
|
220
|
-
if (isMultiline) {
|
|
221
|
-
const closingIndent = getIndent(sourceCode, closingBrace);
|
|
222
|
-
const propertyIndent = getIndent(sourceCode, lastProperty);
|
|
223
|
-
const separator = hasTrailingComma(sourceCode, lastProperty) ? '' : ',';
|
|
224
|
-
const rangeStart =
|
|
225
|
-
separator === ''
|
|
226
|
-
? (sourceCode.getTokenAfter(lastProperty)?.range[1] ?? lastProperty.range[1])
|
|
227
|
-
: lastProperty.range[1];
|
|
228
|
-
|
|
229
|
-
return (fixer) =>
|
|
230
|
-
fixer.replaceTextRange(
|
|
231
|
-
[rangeStart, closingBrace.range[0]],
|
|
232
|
-
`${separator}\n${propertyIndent}${changeDetectionText}\n${closingIndent}`,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const separator = hasTrailingComma(sourceCode, lastProperty) ? '' : ',';
|
|
237
|
-
return (fixer) =>
|
|
238
|
-
fixer.replaceTextRange([lastProperty.range[1], closingBrace.range[0]], `${separator} ${changeDetectionText} `);
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
243
|
-
* @param {any} metadata
|
|
244
|
-
* @param {any} valueNode
|
|
245
|
-
*/
|
|
246
|
-
const buildFix = (sourceCode, metadata, valueNode) => {
|
|
247
|
-
const valueFix = valueNode && ((fixer) => fixer.replaceText(valueNode.property, 'OnPush'));
|
|
248
|
-
if (valueNode && valueNode.object.type !== 'Identifier') return valueFix;
|
|
249
|
-
if (valueNode && resolveIdentifier(sourceCode, valueNode.object)?.source) return valueFix;
|
|
250
|
-
|
|
251
|
-
const fixes = [];
|
|
252
|
-
const angularCoreImport = findAngularCoreImport(sourceCode);
|
|
253
|
-
const importFix = angularCoreImport
|
|
254
|
-
? buildAngularCoreImportFix(sourceCode, angularCoreImport)
|
|
255
|
-
: buildMissingAngularCoreImportFix(sourceCode);
|
|
256
|
-
|
|
257
|
-
if (importFix) fixes.push(importFix);
|
|
258
|
-
|
|
259
|
-
const metadataFix = valueFix ?? buildMetadataFix(sourceCode, metadata);
|
|
260
|
-
if (metadataFix) fixes.push(metadataFix);
|
|
261
|
-
|
|
262
|
-
return (fixer) => fixes.map((applyFix) => applyFix(fixer));
|
|
263
|
-
};
|
|
10
|
+
const SPEC = { property: 'changeDetection', importName: 'ChangeDetectionStrategy', member: 'OnPush' };
|
|
264
11
|
|
|
265
12
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
266
13
|
const requireOnPushChangeDetection = {
|
|
@@ -268,6 +15,7 @@ const requireOnPushChangeDetection = {
|
|
|
268
15
|
type: 'suggestion',
|
|
269
16
|
docs: {
|
|
270
17
|
description: 'Require `changeDetection: ChangeDetectionStrategy.OnPush` in all @Component decorators.',
|
|
18
|
+
recommended: true,
|
|
271
19
|
},
|
|
272
20
|
fixable: 'code',
|
|
273
21
|
messages: {
|
|
@@ -305,7 +53,7 @@ const requireOnPushChangeDetection = {
|
|
|
305
53
|
context.report({
|
|
306
54
|
node: metadata,
|
|
307
55
|
messageId: 'missing',
|
|
308
|
-
fix:
|
|
56
|
+
fix: buildRequiredEnumFix(sourceCode, metadata, null, SPEC),
|
|
309
57
|
});
|
|
310
58
|
return;
|
|
311
59
|
}
|
|
@@ -321,7 +69,7 @@ const requireOnPushChangeDetection = {
|
|
|
321
69
|
node: value,
|
|
322
70
|
messageId: 'notOnPush',
|
|
323
71
|
data: { value: value.property.name },
|
|
324
|
-
fix:
|
|
72
|
+
fix: buildRequiredEnumFix(sourceCode, metadata, value, SPEC),
|
|
325
73
|
});
|
|
326
74
|
}
|
|
327
75
|
},
|
|
@@ -1,219 +1,10 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const {
|
|
5
|
-
|
|
6
|
-
getAngularDecoratorName,
|
|
7
|
-
getImportLocalName,
|
|
8
|
-
isImportedAs,
|
|
9
|
-
resolveIdentifier,
|
|
10
|
-
} = require('./internals/import-resolution');
|
|
4
|
+
const { buildRequiredEnumFix, getPropertyName } = require('./internals/angular-metadata-fix');
|
|
5
|
+
const { getAngularDecoratorName, isImportedAs } = require('./internals/import-resolution');
|
|
11
6
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @param {import('estree').Property['key']} key
|
|
16
|
-
*/
|
|
17
|
-
const getPropertyName = (key) => {
|
|
18
|
-
if (key.type === 'Identifier') return key.name;
|
|
19
|
-
if (key.type === 'Literal' && typeof key.value === 'string') return key.value;
|
|
20
|
-
return null;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @param {string | null} propertyName
|
|
25
|
-
*/
|
|
26
|
-
const getOrderKey = (propertyName) => {
|
|
27
|
-
if (propertyName === 'template' || propertyName === 'templateUrl') return 'template';
|
|
28
|
-
if (propertyName === 'styleUrl' || propertyName === 'styleUrls') return 'styleUrl';
|
|
29
|
-
return propertyName;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
34
|
-
* @param {import('eslint').AST.Token | import('eslint').Rule.Node} node
|
|
35
|
-
*/
|
|
36
|
-
const getIndent = (sourceCode, node) => {
|
|
37
|
-
if (!node.loc) return '';
|
|
38
|
-
|
|
39
|
-
const line = sourceCode.lines[node.loc.start.line - 1] ?? '';
|
|
40
|
-
return line.slice(0, node.loc.start.column);
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
45
|
-
* @param {any} property
|
|
46
|
-
*/
|
|
47
|
-
const hasTrailingComma = (sourceCode, property) => {
|
|
48
|
-
const tokenAfter = sourceCode.getTokenAfter(property);
|
|
49
|
-
return Boolean(tokenAfter && tokenAfter.type === 'Punctuator' && tokenAfter.value === ',');
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
54
|
-
*/
|
|
55
|
-
const findAngularCoreImport = (sourceCode) => {
|
|
56
|
-
const imports = sourceCode.ast.body.filter(
|
|
57
|
-
(node) =>
|
|
58
|
-
node.type === 'ImportDeclaration' && node.source.type === 'Literal' && node.source.value === '@angular/core',
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
imports.find((node) =>
|
|
63
|
-
node.specifiers.some(
|
|
64
|
-
(specifier) =>
|
|
65
|
-
specifier.type === 'ImportSpecifier' &&
|
|
66
|
-
specifier.imported.type === 'Identifier' &&
|
|
67
|
-
specifier.imported.name === 'ViewEncapsulation',
|
|
68
|
-
),
|
|
69
|
-
) ??
|
|
70
|
-
imports.find((node) => node.importKind !== 'type') ??
|
|
71
|
-
null
|
|
72
|
-
);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
77
|
-
* @param {any} importNode
|
|
78
|
-
*/
|
|
79
|
-
const buildAngularCoreImportFix = (sourceCode, importNode) => {
|
|
80
|
-
const specifiers = importNode.specifiers ?? [];
|
|
81
|
-
const hasViewEncapsulation = specifiers.some(
|
|
82
|
-
(specifier) =>
|
|
83
|
-
specifier.type === 'ImportSpecifier' &&
|
|
84
|
-
specifier.imported.type === 'Identifier' &&
|
|
85
|
-
specifier.imported.name === 'ViewEncapsulation',
|
|
86
|
-
);
|
|
87
|
-
if (hasViewEncapsulation) return null;
|
|
88
|
-
|
|
89
|
-
const defaultSpecifier = specifiers.find((specifier) => specifier.type === 'ImportDefaultSpecifier') ?? null;
|
|
90
|
-
const namespaceSpecifier = specifiers.find((specifier) => specifier.type === 'ImportNamespaceSpecifier') ?? null;
|
|
91
|
-
const namedSpecifiers = specifiers.filter((specifier) => specifier.type === 'ImportSpecifier');
|
|
92
|
-
|
|
93
|
-
if (namespaceSpecifier) {
|
|
94
|
-
return (fixer) => fixer.insertTextAfter(importNode, `\nimport { ViewEncapsulation } from '@angular/core';`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const importParts = [];
|
|
98
|
-
if (defaultSpecifier) {
|
|
99
|
-
importParts.push(sourceCode.getText(defaultSpecifier));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const namedTexts = namedSpecifiers.map((specifier) => sourceCode.getText(specifier));
|
|
103
|
-
importParts.push(`{ ${[...namedTexts, 'ViewEncapsulation'].join(', ')} }`);
|
|
104
|
-
|
|
105
|
-
return (fixer) => fixer.replaceText(importNode, `import ${importParts.join(', ')} from '@angular/core';`);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
110
|
-
*/
|
|
111
|
-
const buildMissingAngularCoreImportFix = (sourceCode) => {
|
|
112
|
-
const importDeclarations = sourceCode.ast.body.filter((node) => node.type === 'ImportDeclaration');
|
|
113
|
-
const lastImport = importDeclarations[importDeclarations.length - 1] ?? null;
|
|
114
|
-
|
|
115
|
-
if (lastImport) {
|
|
116
|
-
return (fixer) => fixer.insertTextAfter(lastImport, `\nimport { ViewEncapsulation } from '@angular/core';`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const firstNode = sourceCode.ast.body[0];
|
|
120
|
-
const lastComment = firstNode ? sourceCode.getCommentsBefore(firstNode).at(-1) : null;
|
|
121
|
-
|
|
122
|
-
return lastComment
|
|
123
|
-
? (fixer) => fixer.insertTextAfter(lastComment, `\nimport { ViewEncapsulation } from '@angular/core';\n`)
|
|
124
|
-
: (fixer) =>
|
|
125
|
-
fixer.replaceTextRange([0, firstNode?.range[0] ?? 0], `import { ViewEncapsulation } from '@angular/core';\n\n`);
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* @param {any} metadata
|
|
130
|
-
*/
|
|
131
|
-
const getEncapsulationInsertionTarget = (metadata) => {
|
|
132
|
-
if (metadata.properties.some((property) => property.type === 'SpreadElement')) return null;
|
|
133
|
-
|
|
134
|
-
const encapsulationIndex = COMPONENT_ORDER.indexOf('encapsulation');
|
|
135
|
-
|
|
136
|
-
return (
|
|
137
|
-
metadata.properties.find((property) => {
|
|
138
|
-
if (property.type !== 'Property') return false;
|
|
139
|
-
|
|
140
|
-
const propertyName = getOrderKey(getPropertyName(property.key));
|
|
141
|
-
const propertyIndex = propertyName === null ? COMPONENT_ORDER.length : COMPONENT_ORDER.indexOf(propertyName);
|
|
142
|
-
return propertyIndex > encapsulationIndex || propertyIndex === -1;
|
|
143
|
-
}) ?? null
|
|
144
|
-
);
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
149
|
-
* @param {any} metadata
|
|
150
|
-
*/
|
|
151
|
-
const buildMetadataFix = (sourceCode, metadata) => {
|
|
152
|
-
const encText = `encapsulation: ${getImportLocalName(sourceCode, ANGULAR_CORE, 'ViewEncapsulation') ?? 'ViewEncapsulation'}.None`;
|
|
153
|
-
const properties = metadata.properties;
|
|
154
|
-
const insertionTarget = getEncapsulationInsertionTarget(metadata);
|
|
155
|
-
const isMultiline = Boolean(metadata.loc && metadata.loc.start.line !== metadata.loc.end.line);
|
|
156
|
-
|
|
157
|
-
if (properties.length === 0) {
|
|
158
|
-
return (fixer) => fixer.replaceText(metadata, `{ ${encText} }`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (insertionTarget) {
|
|
162
|
-
if (isMultiline) {
|
|
163
|
-
const propertyIndent = getIndent(sourceCode, insertionTarget);
|
|
164
|
-
return (fixer) => fixer.insertTextBefore(insertionTarget, `${encText},\n${propertyIndent}`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return (fixer) => fixer.insertTextBefore(insertionTarget, `${encText}, `);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const lastProperty = properties[properties.length - 1];
|
|
171
|
-
const closingBrace = sourceCode.getLastToken(metadata);
|
|
172
|
-
if (!lastProperty || !closingBrace) return null;
|
|
173
|
-
|
|
174
|
-
if (isMultiline) {
|
|
175
|
-
const closingIndent = getIndent(sourceCode, closingBrace);
|
|
176
|
-
const propertyIndent = getIndent(sourceCode, lastProperty);
|
|
177
|
-
const separator = hasTrailingComma(sourceCode, lastProperty) ? '' : ',';
|
|
178
|
-
const rangeStart =
|
|
179
|
-
separator === ''
|
|
180
|
-
? (sourceCode.getTokenAfter(lastProperty)?.range[1] ?? lastProperty.range[1])
|
|
181
|
-
: lastProperty.range[1];
|
|
182
|
-
|
|
183
|
-
return (fixer) =>
|
|
184
|
-
fixer.replaceTextRange(
|
|
185
|
-
[rangeStart, closingBrace.range[0]],
|
|
186
|
-
`${separator}\n${propertyIndent}${encText}\n${closingIndent}`,
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const separator = hasTrailingComma(sourceCode, lastProperty) ? '' : ',';
|
|
191
|
-
return (fixer) => fixer.replaceTextRange([lastProperty.range[1], closingBrace.range[0]], `${separator} ${encText} `);
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
196
|
-
* @param {any} metadata
|
|
197
|
-
* @param {any} valueNode
|
|
198
|
-
*/
|
|
199
|
-
const buildFix = (sourceCode, metadata, valueNode) => {
|
|
200
|
-
const valueFix = valueNode && ((fixer) => fixer.replaceText(valueNode.property, 'None'));
|
|
201
|
-
if (valueNode && valueNode.object.type !== 'Identifier') return valueFix;
|
|
202
|
-
if (valueNode && resolveIdentifier(sourceCode, valueNode.object)?.source) return valueFix;
|
|
203
|
-
|
|
204
|
-
const fixes = [];
|
|
205
|
-
const angularCoreImport = findAngularCoreImport(sourceCode);
|
|
206
|
-
const importFix = angularCoreImport
|
|
207
|
-
? buildAngularCoreImportFix(sourceCode, angularCoreImport)
|
|
208
|
-
: buildMissingAngularCoreImportFix(sourceCode);
|
|
209
|
-
|
|
210
|
-
if (importFix) fixes.push(importFix);
|
|
211
|
-
|
|
212
|
-
const metadataFix = valueFix ?? buildMetadataFix(sourceCode, metadata);
|
|
213
|
-
if (metadataFix) fixes.push(metadataFix);
|
|
214
|
-
|
|
215
|
-
return (fixer) => fixes.map((applyFix) => applyFix(fixer));
|
|
216
|
-
};
|
|
7
|
+
const SPEC = { property: 'encapsulation', importName: 'ViewEncapsulation', member: 'None' };
|
|
217
8
|
|
|
218
9
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
219
10
|
const requireViewEncapsulationNone = {
|
|
@@ -221,6 +12,7 @@ const requireViewEncapsulationNone = {
|
|
|
221
12
|
type: 'suggestion',
|
|
222
13
|
docs: {
|
|
223
14
|
description: 'Require `encapsulation: ViewEncapsulation.None` in all @Component decorators.',
|
|
15
|
+
recommended: true,
|
|
224
16
|
},
|
|
225
17
|
fixable: 'code',
|
|
226
18
|
messages: {
|
|
@@ -251,7 +43,7 @@ const requireViewEncapsulationNone = {
|
|
|
251
43
|
context.report({
|
|
252
44
|
node: metadata,
|
|
253
45
|
messageId: 'missing',
|
|
254
|
-
fix:
|
|
46
|
+
fix: buildRequiredEnumFix(sourceCode, metadata, null, SPEC),
|
|
255
47
|
});
|
|
256
48
|
return;
|
|
257
49
|
}
|
|
@@ -267,7 +59,7 @@ const requireViewEncapsulationNone = {
|
|
|
267
59
|
node: value,
|
|
268
60
|
messageId: 'notNone',
|
|
269
61
|
data: { value: value.property.name },
|
|
270
|
-
fix:
|
|
62
|
+
fix: buildRequiredEnumFix(sourceCode, metadata, value, SPEC),
|
|
271
63
|
});
|
|
272
64
|
}
|
|
273
65
|
},
|
|
@@ -20,30 +20,22 @@ const isInjectCall = (sourceCode, node) => {
|
|
|
20
20
|
return isImportedAs(sourceCode, node.value.callee, 'inject');
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const nextToken = protectedToken ? sourceCode.getTokenAfter(protectedToken) : null;
|
|
30
|
-
|
|
31
|
-
if (!protectedToken || !nextToken) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return (fixer) => fixer.removeRange([protectedToken.range[0], nextToken.range[0]]);
|
|
36
|
-
};
|
|
23
|
+
const MEMBER_TYPES = new Set([
|
|
24
|
+
'PropertyDefinition',
|
|
25
|
+
'AccessorProperty',
|
|
26
|
+
'TSAbstractPropertyDefinition',
|
|
27
|
+
'TSAbstractAccessorProperty',
|
|
28
|
+
]);
|
|
37
29
|
|
|
38
30
|
/**
|
|
39
31
|
* @param {any} node
|
|
40
32
|
*/
|
|
41
33
|
const isSupportedMember = (node) => {
|
|
42
|
-
if (node.type
|
|
34
|
+
if (MEMBER_TYPES.has(node.type)) {
|
|
43
35
|
return true;
|
|
44
36
|
}
|
|
45
37
|
|
|
46
|
-
if (node.type === 'MethodDefinition') {
|
|
38
|
+
if (node.type === 'MethodDefinition' || node.type === 'TSAbstractMethodDefinition') {
|
|
47
39
|
return node.kind !== 'constructor';
|
|
48
40
|
}
|
|
49
41
|
|
|
@@ -68,7 +60,7 @@ const templateMemberAccessibility = {
|
|
|
68
60
|
shouldBeExplicit:
|
|
69
61
|
'Member `{{name}}` is referenced from a template or host binding. Use `public` when it is intentionally exposed outside the component; otherwise use `protected`.',
|
|
70
62
|
shouldNotBeProtected:
|
|
71
|
-
'Member `{{name}}` should not be protected because it is not referenced from a template or host binding.',
|
|
63
|
+
'Member `{{name}}` should not be protected because it is not referenced from a template or host binding. If a subclass reads it, mark this class `abstract`.',
|
|
72
64
|
},
|
|
73
65
|
},
|
|
74
66
|
create(context) {
|
|
@@ -134,12 +126,13 @@ const templateMemberAccessibility = {
|
|
|
134
126
|
}
|
|
135
127
|
|
|
136
128
|
if (node.accessibility !== 'protected') return;
|
|
129
|
+
if (node.type.startsWith('TSAbstract') || classNode.abstract) return;
|
|
137
130
|
|
|
138
131
|
context.report({
|
|
139
132
|
node,
|
|
140
133
|
messageId: 'shouldNotBeProtected',
|
|
141
134
|
data: { name: memberName },
|
|
142
|
-
fix:
|
|
135
|
+
fix: buildAccessibilityFix(node, sourceCode, 'public'),
|
|
143
136
|
});
|
|
144
137
|
|
|
145
138
|
return;
|
|
@@ -174,11 +167,11 @@ const templateMemberAccessibility = {
|
|
|
174
167
|
};
|
|
175
168
|
|
|
176
169
|
return {
|
|
177
|
-
PropertyDefinition(node) {
|
|
170
|
+
'PropertyDefinition, AccessorProperty, TSAbstractPropertyDefinition, TSAbstractAccessorProperty'(node) {
|
|
178
171
|
checkMember(node);
|
|
179
172
|
checkImplicitPublicMember(node);
|
|
180
173
|
},
|
|
181
|
-
MethodDefinition(node) {
|
|
174
|
+
'MethodDefinition, TSAbstractMethodDefinition'(node) {
|
|
182
175
|
checkMember(node);
|
|
183
176
|
checkImplicitPublicMember(node);
|
|
184
177
|
},
|