@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +4 -0
  3. package/migrations.json +1 -0
  4. package/package.json +2 -1
  5. package/src/configs/recommended.js +16 -18
  6. package/src/rules/angular-decorator-property-order.js +24 -7
  7. package/src/rules/class-constant-property.js +1 -1
  8. package/src/rules/class-member-order.js +66 -8
  9. package/src/rules/enforce-routing-view-naming.js +47 -24
  10. package/src/rules/guard-return-newline.js +4 -7
  11. package/src/rules/internals/angular-member-visibility.js +24 -2
  12. package/src/rules/internals/angular-metadata-fix.js +231 -1
  13. package/src/rules/internals/angular-version.js +50 -0
  14. package/src/rules/internals/implemented-contract-members.js +26 -11
  15. package/src/rules/no-angular-router-api.js +86 -33
  16. package/src/rules/no-angular-seo-services.js +1 -0
  17. package/src/rules/no-async-await.js +5 -1
  18. package/src/rules/no-cdk-import.js +32 -7
  19. package/src/rules/no-direct-dom-manipulation.js +50 -28
  20. package/src/rules/no-document-cookie.js +51 -8
  21. package/src/rules/no-dom-query.js +5 -2
  22. package/src/rules/no-empty-angular-metadata-arrays.js +2 -0
  23. package/src/rules/no-empty-newlines-between-imports.js +4 -1
  24. package/src/rules/no-impure-top-level-provider.js +42 -7
  25. package/src/rules/no-inject-chain.js +5 -1
  26. package/src/rules/no-leading-underscore-class-member.js +27 -2
  27. package/src/rules/no-legacy-angular-decorators.js +19 -16
  28. package/src/rules/no-legacy-prepare-without-injector.js +57 -12
  29. package/src/rules/no-legacy-query-import.js +61 -16
  30. package/src/rules/no-member-alias.js +0 -5
  31. package/src/rules/no-native-observers.js +89 -24
  32. package/src/rules/no-pipe-logic.js +1 -2
  33. package/src/rules/no-readonly-signal.js +1 -5
  34. package/src/rules/no-redundant-internal.js +5 -2
  35. package/src/rules/no-redundant-on-push-change-detection.js +3 -48
  36. package/src/rules/no-rxjs-in-effect.js +7 -5
  37. package/src/rules/no-screaming-case-local.js +3 -5
  38. package/src/rules/no-standalone-flag.js +1 -0
  39. package/src/rules/no-subscribe-in-pipe.js +5 -1
  40. package/src/rules/no-subscribe-with-body.js +30 -1
  41. package/src/rules/no-template-literal-before-inline-template.js +6 -0
  42. package/src/rules/no-trivial-return-type.js +18 -1
  43. package/src/rules/no-trivial-wrapper-method.js +39 -1
  44. package/src/rules/no-type-only-import.js +23 -19
  45. package/src/rules/no-unused-class-member.js +14 -6
  46. package/src/rules/no-window-location.js +60 -22
  47. package/src/rules/prefer-clone-equal.js +61 -8
  48. package/src/rules/prefer-concise-angular-host-directives.js +39 -11
  49. package/src/rules/prefer-concise-angular-style-metadata.js +10 -2
  50. package/src/rules/prefer-element-dimensions.js +264 -11
  51. package/src/rules/prefer-match-media.js +17 -3
  52. package/src/rules/prefer-rxjs-timer.js +26 -4
  53. package/src/rules/prefer-scroll-state.js +0 -4
  54. package/src/rules/prefer-static-boolean-properties.js +1 -0
  55. package/src/rules/prefer-viewport-size.js +153 -16
  56. package/src/rules/require-dollar-suffix.js +11 -3
  57. package/src/rules/require-form-submit.js +4 -1
  58. package/src/rules/require-on-push-change-detection.js +7 -259
  59. package/src/rules/require-view-encapsulation-none.js +6 -214
  60. 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, partition
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') || hasBinding(node, 'ngNoForm') || attribute(node, 'method')?.value === 'dialog';
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
- ANGULAR_CORE,
6
- getAngularDecoratorName,
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
- /** @type {number | null | undefined} */
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: buildFix(sourceCode, metadata, null),
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: buildFix(sourceCode, metadata, value),
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
- ANGULAR_CORE,
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 COMPONENT_ORDER = ['selector', 'template', 'styleUrl', 'encapsulation', 'changeDetection'];
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: buildFix(sourceCode, metadata, null),
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: buildFix(sourceCode, metadata, value),
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
- * @param {any} node
25
- * @param {import('eslint').SourceCode} sourceCode
26
- */
27
- const buildRemoveProtectedFix = (node, sourceCode) => {
28
- const protectedToken = sourceCode.getFirstToken(node, (token) => token.value === 'protected');
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 === 'PropertyDefinition') {
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: buildRemoveProtectedFix(node, sourceCode),
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
  },