@ethlete/eslint-plugin 1.0.0-next.22 → 1.0.0-next.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/package.json +4 -5
- package/src/configs/recommended.js +23 -10
- package/src/index.js +6 -1
- package/src/rules/angular-decorator-property-order.js +20 -20
- package/src/rules/class-constant-property.js +35 -14
- package/src/rules/class-member-order.js +77 -46
- package/src/rules/guard-return-newline.js +5 -4
- package/src/rules/inject-member-accessibility.js +22 -24
- package/src/rules/internals/angular-io.js +31 -14
- package/src/rules/internals/angular-member-visibility.js +16 -12
- package/src/rules/internals/angular-metadata-fix.js +22 -0
- package/src/rules/internals/import-resolution.js +191 -0
- package/src/rules/internals/member-accessibility-fix.js +26 -0
- package/src/rules/no-angular-router-api.js +23 -29
- package/src/rules/no-angular-seo-services.js +21 -16
- package/src/rules/no-cdk-import.js +7 -14
- package/src/rules/no-direct-dom-manipulation.js +17 -0
- package/src/rules/no-document-cookie.js +3 -0
- package/src/rules/no-effect-cleanup-return.js +15 -9
- package/src/rules/no-empty-angular-metadata-arrays.js +6 -36
- package/src/rules/no-enum.js +115 -0
- package/src/rules/no-inject-chain.js +3 -1
- package/src/rules/no-leading-underscore-class-member.js +33 -3
- package/src/rules/no-legacy-angular-decorators.js +17 -21
- package/src/rules/no-legacy-prepare-without-injector.js +42 -26
- package/src/rules/no-legacy-query-import.js +7 -16
- package/src/rules/no-locale-id.js +6 -18
- package/src/rules/no-member-alias.js +21 -0
- package/src/rules/no-native-html-input-name.js +1 -1
- package/src/rules/no-pipe-logic.js +11 -17
- package/src/rules/no-readonly-signal.js +13 -5
- package/src/rules/no-redundant-internal.js +21 -2
- package/src/rules/no-redundant-on-push-change-detection.js +32 -56
- package/src/rules/no-rxjs-in-effect.js +12 -8
- package/src/rules/no-screaming-case-local.js +1 -0
- package/src/rules/no-standalone-flag.js +7 -36
- package/src/rules/no-subscribe-in-pipe.js +3 -2
- package/src/rules/no-template-literal-before-inline-template.js +3 -14
- package/src/rules/no-trivial-return-type.js +2 -0
- package/src/rules/no-trivial-wrapper-method.js +13 -16
- package/src/rules/no-type-only-import.js +7 -2
- package/src/rules/no-typed-injected-element-ref.js +23 -19
- package/src/rules/no-unused-class-member.js +34 -14
- package/src/rules/prefer-clone-equal.js +8 -6
- package/src/rules/prefer-concise-angular-host-directives.js +23 -29
- package/src/rules/prefer-concise-angular-style-metadata.js +8 -16
- package/src/rules/prefer-element-dimensions.js +9 -10
- package/src/rules/prefer-linked-signal.js +7 -4
- package/src/rules/prefer-match-media.js +12 -16
- package/src/rules/prefer-present-tense-output.js +1 -1
- package/src/rules/prefer-rxjs-timer.js +3 -1
- package/src/rules/prefer-scroll-state.js +3 -67
- package/src/rules/prefer-static-boolean-properties.js +30 -0
- package/src/rules/prefer-viewport-size.js +1 -7
- package/src/rules/require-dollar-suffix.js +7 -4
- package/src/rules/require-form-submit.js +2 -28
- package/src/rules/require-on-push-change-detection.js +48 -36
- package/src/rules/require-view-encapsulation-none.js +48 -34
- package/src/rules/take-until-destroyed-last.js +64 -0
- package/src/rules/template-member-accessibility.js +12 -27
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0-next.24
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add `take-until-destroyed-last` to the recommended config: `takeUntilDestroyed()` must be the last operator in a `.pipe()`, since an operator after it can keep the subscription alive.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- `class-member-order` now fixes classes with `static` members or index signatures and keeps a trailing comment with its member, and `prefer-concise-angular-host-directives` no longer drops or misplaces comments.
|
|
12
|
+
- Angular and RxJS rules now resolve `Component`, `inject`, `input`, `effect` and friends through their imports, `document` and `setTimeout` match only the globals, and import bans also report re-exports and dynamic imports.
|
|
13
|
+
- Several fixers no longer emit broken code (a dangling `other._value`, a duplicate `styleUrl`, a dropped `host` spread), `no-trivial-return-type` checks function declarations, `no-screaming-case-local` reports a function once, and `standalone` sorts after `selector`.
|
|
14
|
+
|
|
15
|
+
## 1.0.0-next.23
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- [#3069](https://github.com/ethlete-io/ethdk/pull/3069) [`a9113cb`](https://github.com/ethlete-io/ethdk/commit/a9113cb6ce63942ffce0e26ca7df7f0556a14838) Thanks [@github-actions](https://github.com/apps/github-actions)! - New rule `ethlete/no-enum`. It bans `enum` and `const enum`, and auto-fixes a string enum into a
|
|
20
|
+
const object plus a derived union type.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- [#3069](https://github.com/ethlete-io/ethdk/pull/3069) [`a9113cb`](https://github.com/ethlete-io/ethdk/commit/a9113cb6ce63942ffce0e26ca7df7f0556a14838) Thanks [@github-actions](https://github.com/apps/github-actions)! - Harden unsafe fixers, reduce contract and test false positives, and correct replacement guidance across the recommended rules.
|
|
25
|
+
|
|
3
26
|
## 1.0.0-next.22
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethlete/eslint-plugin",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.24",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"peerDependencies": {
|
|
8
8
|
"@angular-eslint/eslint-plugin-template": ">=21.0.0",
|
|
9
|
-
"@angular/core": "
|
|
9
|
+
"@angular/core": ">=21.0.0",
|
|
10
10
|
"@typescript-eslint/eslint-plugin": ">=8.0.0",
|
|
11
11
|
"eslint": ">=9.0.0",
|
|
12
|
-
"
|
|
13
|
-
"typescript": "6.0.3"
|
|
12
|
+
"typescript": ">=5.9.0"
|
|
14
13
|
}
|
|
15
|
-
}
|
|
14
|
+
}
|
|
@@ -107,11 +107,8 @@ const recommendedTs = {
|
|
|
107
107
|
|
|
108
108
|
'no-restricted-syntax': [
|
|
109
109
|
'error',
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
selector: 'TSEnumDeclaration',
|
|
113
|
-
message: 'No enums. Use a const object + derived union type instead.',
|
|
114
|
-
},
|
|
110
|
+
// enum / const enum is owned by ethlete/no-enum, so a lib can keep it on while it works
|
|
111
|
+
// through the rest of this rule's backlog.
|
|
115
112
|
// No standalone function declarations / expressions — use arrow functions
|
|
116
113
|
{
|
|
117
114
|
selector: 'FunctionDeclaration',
|
|
@@ -242,9 +239,15 @@ const recommendedTs = {
|
|
|
242
239
|
// No .subscribe() inside a .pipe() callback
|
|
243
240
|
'ethlete/no-subscribe-in-pipe': 'error',
|
|
244
241
|
|
|
242
|
+
// takeUntilDestroyed() is the last operator in a .pipe()
|
|
243
|
+
'ethlete/take-until-destroyed-last': 'error',
|
|
244
|
+
|
|
245
245
|
// No async/await — asynchronous work is modelled as cold Observables
|
|
246
246
|
'ethlete/no-async-await': 'error',
|
|
247
247
|
|
|
248
|
+
// No enum / const enum — use a const object with `as const` plus a derived union type
|
|
249
|
+
'ethlete/no-enum': 'error',
|
|
250
|
+
|
|
248
251
|
// No .subscribe() inside effect() or computed()
|
|
249
252
|
'ethlete/no-rxjs-in-effect': 'error',
|
|
250
253
|
|
|
@@ -272,7 +275,7 @@ const recommendedTs = {
|
|
|
272
275
|
// No raw IntersectionObserver / MutationObserver / ResizeObserver — use @ethlete/core signal utils
|
|
273
276
|
'ethlete/no-native-observers': 'error',
|
|
274
277
|
|
|
275
|
-
// Prefer injectViewportSize() over window.innerWidth / innerHeight
|
|
278
|
+
// Prefer injectViewportSize() over window.innerWidth / innerHeight
|
|
276
279
|
'ethlete/prefer-viewport-size': 'warn',
|
|
277
280
|
|
|
278
281
|
// Prefer injectMediaQueryIsMatched() / injectBreakpointIsMatched() etc. over .matchMedia()
|
|
@@ -297,7 +300,7 @@ const recommendedTs = {
|
|
|
297
300
|
'ethlete/no-legacy-angular-decorators': 'error',
|
|
298
301
|
|
|
299
302
|
// No Angular Title/Meta services — use @ethlete/core SEO utilities instead
|
|
300
|
-
// (
|
|
303
|
+
// (applyHeadTitleBinding, applyMetaBinding, applyLinkBinding, etc.)
|
|
301
304
|
'ethlete/no-angular-seo-services': 'error',
|
|
302
305
|
|
|
303
306
|
// No JSON.parse(JSON.stringify()), structuredClone, or lodash cloneDeep / isEqual
|
|
@@ -414,20 +417,30 @@ const recommendedTemplate = {
|
|
|
414
417
|
// (warn + suggestion-only: the rewrite is only safe when the input has a booleanAttribute transform,
|
|
415
418
|
// which a template rule cannot verify)
|
|
416
419
|
'ethlete/prefer-static-boolean-properties': 'warn',
|
|
417
|
-
// A form must handle its own submission
|
|
420
|
+
// A form must handle its own submission
|
|
418
421
|
'ethlete/require-form-submit': 'error',
|
|
419
422
|
},
|
|
420
423
|
};
|
|
421
424
|
|
|
422
425
|
/**
|
|
423
|
-
* Relaxed rules for test/spec files.
|
|
424
|
-
* Non-null assertions are common and intentional in tests.
|
|
426
|
+
* Relaxed rules for test/spec files, including common DOM assertions and async test orchestration.
|
|
425
427
|
* @type {import('eslint').Linter.Config}
|
|
426
428
|
*/
|
|
427
429
|
const recommendedSpec = {
|
|
428
430
|
files: ['**/*.spec.ts'],
|
|
429
431
|
rules: {
|
|
430
432
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
433
|
+
'no-restricted-globals': 'off',
|
|
434
|
+
'ethlete/no-async-await': 'off',
|
|
435
|
+
'ethlete/no-direct-dom-manipulation': 'off',
|
|
436
|
+
'ethlete/no-document-cookie': 'off',
|
|
437
|
+
'ethlete/no-dom-query': 'off',
|
|
438
|
+
'ethlete/no-native-observers': 'off',
|
|
439
|
+
'ethlete/no-window-location': 'off',
|
|
440
|
+
'ethlete/prefer-element-dimensions': 'off',
|
|
441
|
+
'ethlete/prefer-match-media': 'off',
|
|
442
|
+
'ethlete/prefer-scroll-state': 'off',
|
|
443
|
+
'ethlete/prefer-viewport-size': 'off',
|
|
431
444
|
},
|
|
432
445
|
};
|
|
433
446
|
|
package/src/index.js
CHANGED
|
@@ -59,19 +59,23 @@ const requireFormSubmit = require('./rules/require-form-submit');
|
|
|
59
59
|
const noCdkImport = require('./rules/no-cdk-import');
|
|
60
60
|
const noLegacyQueryImport = require('./rules/no-legacy-query-import');
|
|
61
61
|
const noAsyncAwait = require('./rules/no-async-await');
|
|
62
|
+
const noEnum = require('./rules/no-enum');
|
|
63
|
+
const takeUntilDestroyedLast = require('./rules/take-until-destroyed-last');
|
|
62
64
|
const { recommendedTs, recommendedTemplate, recommendedSpec } = require('./configs/recommended');
|
|
65
|
+
const { version } = require('../package.json');
|
|
63
66
|
|
|
64
67
|
/** @type {import('eslint').ESLint.Plugin} */
|
|
65
68
|
const plugin = {
|
|
66
69
|
meta: {
|
|
67
70
|
name: '@ethlete/eslint-plugin',
|
|
68
|
-
version
|
|
71
|
+
version,
|
|
69
72
|
},
|
|
70
73
|
rules: {
|
|
71
74
|
'no-inject-chain': noInjectChain,
|
|
72
75
|
'no-legacy-prepare-without-injector': noLegacyPrepareWithoutInjector,
|
|
73
76
|
'no-trivial-return-type': noTrivialReturnType,
|
|
74
77
|
'no-subscribe-in-pipe': noSubscribeInPipe,
|
|
78
|
+
'take-until-destroyed-last': takeUntilDestroyedLast,
|
|
75
79
|
'no-rxjs-in-effect': noRxjsInEffect,
|
|
76
80
|
'no-effect-cleanup-return': noEffectCleanupReturn,
|
|
77
81
|
'no-readonly-signal': noReadonlySignal,
|
|
@@ -126,6 +130,7 @@ const plugin = {
|
|
|
126
130
|
'no-cdk-import': noCdkImport,
|
|
127
131
|
'no-legacy-query-import': noLegacyQueryImport,
|
|
128
132
|
'no-async-await': noAsyncAwait,
|
|
133
|
+
'no-enum': noEnum,
|
|
129
134
|
},
|
|
130
135
|
};
|
|
131
136
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { getAngularDecoratorName } = require('./internals/import-resolution');
|
|
5
|
+
|
|
4
6
|
/** @typedef {'Component' | 'Directive'} TDecoratorName */
|
|
5
7
|
/** @typedef {{ order: string[]; orderText: string }} TDecoratorConfig */
|
|
6
8
|
/** @typedef {{ range: [number, number] }} TNodeWithRange */
|
|
@@ -14,11 +16,13 @@ const DECORATOR_CONFIG = {
|
|
|
14
16
|
Component: {
|
|
15
17
|
order: [
|
|
16
18
|
'selector',
|
|
19
|
+
'standalone',
|
|
17
20
|
'template',
|
|
18
21
|
'styleUrl',
|
|
19
22
|
'encapsulation',
|
|
20
23
|
'changeDetection',
|
|
21
24
|
'imports',
|
|
25
|
+
'deferredImports',
|
|
22
26
|
'providers',
|
|
23
27
|
'viewProviders',
|
|
24
28
|
'animations',
|
|
@@ -29,16 +33,28 @@ const DECORATOR_CONFIG = {
|
|
|
29
33
|
'outputs',
|
|
30
34
|
'queries',
|
|
31
35
|
'preserveWhitespaces',
|
|
36
|
+
'interpolation',
|
|
32
37
|
'schemas',
|
|
33
38
|
'jit',
|
|
34
39
|
'exportAs',
|
|
35
40
|
],
|
|
36
41
|
orderText:
|
|
37
|
-
'selector, template/templateUrl, styleUrl/styleUrls, encapsulation, changeDetection, imports, providers, viewProviders, animations, hostDirectives, host, styles, inputs, outputs, queries, preserveWhitespaces, schemas, jit, exportAs',
|
|
42
|
+
'selector, standalone, template/templateUrl, styleUrl/styleUrls, encapsulation, changeDetection, imports, deferredImports, providers, viewProviders, animations, hostDirectives, host, styles, inputs, outputs, queries, preserveWhitespaces, interpolation, schemas, jit, exportAs',
|
|
38
43
|
},
|
|
39
44
|
Directive: {
|
|
40
|
-
order: [
|
|
41
|
-
|
|
45
|
+
order: [
|
|
46
|
+
'selector',
|
|
47
|
+
'standalone',
|
|
48
|
+
'exportAs',
|
|
49
|
+
'providers',
|
|
50
|
+
'inputs',
|
|
51
|
+
'outputs',
|
|
52
|
+
'queries',
|
|
53
|
+
'hostDirectives',
|
|
54
|
+
'host',
|
|
55
|
+
'jit',
|
|
56
|
+
],
|
|
57
|
+
orderText: 'selector, standalone, exportAs, providers, inputs, outputs, queries, hostDirectives, host, jit',
|
|
42
58
|
},
|
|
43
59
|
};
|
|
44
60
|
|
|
@@ -66,22 +82,6 @@ const getOrderKey = (decoratorName, propertyName) => {
|
|
|
66
82
|
return propertyName;
|
|
67
83
|
};
|
|
68
84
|
|
|
69
|
-
/**
|
|
70
|
-
* @param {import('eslint').Rule.Node} node
|
|
71
|
-
* @returns {TDecoratorName | null}
|
|
72
|
-
*/
|
|
73
|
-
const getDecoratorName = (node) => {
|
|
74
|
-
const decorator = /** @type {any} */ (node);
|
|
75
|
-
if (decorator.type !== 'Decorator') return null;
|
|
76
|
-
|
|
77
|
-
const expression = decorator.expression;
|
|
78
|
-
if (expression.type === 'CallExpression') {
|
|
79
|
-
return expression.callee.type === 'Identifier' ? expression.callee.name : null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return expression.type === 'Identifier' ? expression.name : null;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
85
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
86
86
|
const angularDecoratorPropertyOrder = {
|
|
87
87
|
meta: {
|
|
@@ -101,7 +101,7 @@ const angularDecoratorPropertyOrder = {
|
|
|
101
101
|
return {
|
|
102
102
|
/** @param {import('eslint').Rule.Node} node */
|
|
103
103
|
Decorator(node) {
|
|
104
|
-
const decoratorName =
|
|
104
|
+
const decoratorName = getAngularDecoratorName(context.sourceCode, node);
|
|
105
105
|
if (decoratorName !== 'Component' && decoratorName !== 'Directive') return;
|
|
106
106
|
|
|
107
107
|
const decorator = /** @type {any} */ (node);
|
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { getAngularDecoratorName } = require('./internals/import-resolution');
|
|
5
|
+
|
|
4
6
|
const SCREAMING_CASE_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
5
7
|
|
|
6
8
|
/**
|
|
9
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
7
10
|
* @param {any} classNode
|
|
8
11
|
*/
|
|
9
|
-
const hasPipeDecorator = (classNode) =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (expr.type === 'CallExpression') {
|
|
14
|
-
return expr.callee.type === 'Identifier' && expr.callee.name === 'Pipe';
|
|
15
|
-
}
|
|
16
|
-
if (expr.type === 'Identifier') {
|
|
17
|
-
return expr.name === 'Pipe';
|
|
18
|
-
}
|
|
19
|
-
return false;
|
|
20
|
-
});
|
|
21
|
-
};
|
|
12
|
+
const hasPipeDecorator = (sourceCode, classNode) =>
|
|
13
|
+
(classNode.decorators ?? []).some(
|
|
14
|
+
(/** @type {any} */ decorator) => getAngularDecoratorName(sourceCode, decorator) === 'Pipe',
|
|
15
|
+
);
|
|
22
16
|
|
|
23
17
|
const REACTIVE_APIS = new Set([
|
|
24
18
|
'signal',
|
|
@@ -249,6 +243,11 @@ const collectWrittenMembers = (node, writtenMembers) => {
|
|
|
249
243
|
|
|
250
244
|
if (node.type === 'AssignmentExpression') {
|
|
251
245
|
collectAssignedMemberNames(node.left, writtenMembers);
|
|
246
|
+
|
|
247
|
+
if (node.left.type === 'MemberExpression' && node.left.object.type === 'MemberExpression') {
|
|
248
|
+
const memberName = getWrittenMemberName(node.left.object);
|
|
249
|
+
if (memberName !== null) writtenMembers.add(memberName);
|
|
250
|
+
}
|
|
252
251
|
}
|
|
253
252
|
|
|
254
253
|
if (node.type === 'UpdateExpression') {
|
|
@@ -259,6 +258,20 @@ const collectWrittenMembers = (node, writtenMembers) => {
|
|
|
259
258
|
}
|
|
260
259
|
}
|
|
261
260
|
|
|
261
|
+
if (
|
|
262
|
+
node.type === 'CallExpression' &&
|
|
263
|
+
node.callee.type === 'MemberExpression' &&
|
|
264
|
+
node.callee.object.type === 'MemberExpression' &&
|
|
265
|
+
!node.callee.computed &&
|
|
266
|
+
node.callee.property.type === 'Identifier' &&
|
|
267
|
+
['add', 'clear', 'delete', 'pop', 'push', 'reverse', 'set', 'shift', 'sort', 'splice', 'unshift'].includes(
|
|
268
|
+
node.callee.property.name,
|
|
269
|
+
)
|
|
270
|
+
) {
|
|
271
|
+
const memberName = getWrittenMemberName(node.callee.object);
|
|
272
|
+
if (memberName !== null) writtenMembers.add(memberName);
|
|
273
|
+
}
|
|
274
|
+
|
|
262
275
|
for (const [key, value] of Object.entries(node)) {
|
|
263
276
|
if (key === 'parent' || key === 'range' || key === 'loc') continue;
|
|
264
277
|
if (!value || typeof value !== 'object') continue;
|
|
@@ -326,7 +339,15 @@ const classConstantProperty = {
|
|
|
326
339
|
continue;
|
|
327
340
|
}
|
|
328
341
|
|
|
329
|
-
if (
|
|
342
|
+
if (
|
|
343
|
+
!member.readonly &&
|
|
344
|
+
((member.value.type === 'Literal' && member.value.value === null) ||
|
|
345
|
+
(member.value.type === 'Identifier' && member.value.name === 'undefined'))
|
|
346
|
+
) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (memberName === 'transform' && hasPipeDecorator(context.sourceCode, node.parent)) {
|
|
330
351
|
continue;
|
|
331
352
|
}
|
|
332
353
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { ANGULAR_CORE, getImportedName } = require('./internals/import-resolution');
|
|
5
|
+
|
|
4
6
|
/** @typedef {'inject' | 'input' | 'output' | 'query' | 'property' | 'constructor' | 'method' | 'private-method'} TMemberGroup */
|
|
5
7
|
/** @typedef {{ dependencies: Set<string>; group: TMemberGroup; groupIndex: number; name: string; node: any; originalIndex: number }} TMemberEntry */
|
|
6
|
-
/** @typedef {TMemberEntry & { segmentText: string }} TSegmentEntry */
|
|
7
8
|
|
|
8
9
|
const INPUT_APIS = new Set(['input', 'model']);
|
|
9
10
|
const OUTPUT_APIS = new Set(['output', 'outputFromObservable']);
|
|
10
11
|
const QUERY_APIS = new Set(['viewChild', 'viewChildren', 'contentChild', 'contentChildren']);
|
|
12
|
+
const ANGULAR_API_SOURCES = [ANGULAR_CORE, '@angular/core/rxjs-interop'];
|
|
11
13
|
|
|
12
14
|
/** @type {TMemberGroup[]} */
|
|
13
15
|
const MEMBER_GROUP_ORDER = [
|
|
@@ -71,49 +73,70 @@ const getCallRootName = (value) => {
|
|
|
71
73
|
};
|
|
72
74
|
|
|
73
75
|
/**
|
|
76
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
77
|
+
* @param {any} value
|
|
78
|
+
*/
|
|
79
|
+
const getAngularCallName = (sourceCode, value) => {
|
|
80
|
+
if (!value || value.type !== 'CallExpression') return null;
|
|
81
|
+
|
|
82
|
+
const { callee } = value;
|
|
83
|
+
const isRequired =
|
|
84
|
+
callee.type === 'MemberExpression' &&
|
|
85
|
+
!callee.computed &&
|
|
86
|
+
callee.property.type === 'Identifier' &&
|
|
87
|
+
callee.property.name === 'required';
|
|
88
|
+
|
|
89
|
+
return getImportedName(sourceCode, isRequired ? callee.object : callee, ANGULAR_API_SOURCES);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
74
94
|
* @param {any} node
|
|
75
95
|
* @param {Set<string>} apiNames
|
|
76
96
|
*/
|
|
77
|
-
const isPropertyInitializedWith = (node, apiNames) => {
|
|
97
|
+
const isPropertyInitializedWith = (sourceCode, node, apiNames) => {
|
|
78
98
|
if (node.type !== 'PropertyDefinition') return false;
|
|
79
99
|
|
|
80
|
-
const apiName =
|
|
100
|
+
const apiName = getAngularCallName(sourceCode, node.value);
|
|
81
101
|
return apiName !== null && apiNames.has(apiName);
|
|
82
102
|
};
|
|
83
103
|
|
|
84
104
|
const INJECT_HELPER_NAME_PATTERN = /^inject[A-Z]/;
|
|
85
105
|
|
|
86
106
|
/**
|
|
107
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
87
108
|
* @param {any} node
|
|
88
109
|
*/
|
|
89
|
-
const isPropertyInitializedWithInject = (node) => {
|
|
110
|
+
const isPropertyInitializedWithInject = (sourceCode, node) => {
|
|
90
111
|
if (node.type !== 'PropertyDefinition') return false;
|
|
112
|
+
if (getAngularCallName(sourceCode, node.value) === 'inject') return true;
|
|
91
113
|
|
|
92
|
-
const
|
|
93
|
-
return
|
|
114
|
+
const rootName = getCallRootName(node.value);
|
|
115
|
+
return rootName !== null && INJECT_HELPER_NAME_PATTERN.test(rootName);
|
|
94
116
|
};
|
|
95
117
|
|
|
96
118
|
/**
|
|
119
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
97
120
|
* @param {any} node
|
|
98
121
|
* @returns {TMemberGroup | null}
|
|
99
122
|
*/
|
|
100
|
-
const getMemberGroup = (node) => {
|
|
123
|
+
const getMemberGroup = (sourceCode, node) => {
|
|
101
124
|
if (node.static) return null;
|
|
102
125
|
|
|
103
126
|
if (node.type === 'PropertyDefinition') {
|
|
104
|
-
if (isPropertyInitializedWithInject(node)) {
|
|
127
|
+
if (isPropertyInitializedWithInject(sourceCode, node)) {
|
|
105
128
|
return 'inject';
|
|
106
129
|
}
|
|
107
130
|
|
|
108
|
-
if (isPropertyInitializedWith(node, INPUT_APIS)) {
|
|
131
|
+
if (isPropertyInitializedWith(sourceCode, node, INPUT_APIS)) {
|
|
109
132
|
return 'input';
|
|
110
133
|
}
|
|
111
134
|
|
|
112
|
-
if (isPropertyInitializedWith(node, OUTPUT_APIS)) {
|
|
135
|
+
if (isPropertyInitializedWith(sourceCode, node, OUTPUT_APIS)) {
|
|
113
136
|
return 'output';
|
|
114
137
|
}
|
|
115
138
|
|
|
116
|
-
if (isPropertyInitializedWith(node, QUERY_APIS)) {
|
|
139
|
+
if (isPropertyInitializedWith(sourceCode, node, QUERY_APIS)) {
|
|
117
140
|
return 'query';
|
|
118
141
|
}
|
|
119
142
|
|
|
@@ -188,12 +211,13 @@ const getInitializerDependencies = (node) => {
|
|
|
188
211
|
};
|
|
189
212
|
|
|
190
213
|
/**
|
|
214
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
191
215
|
* @param {any} node
|
|
192
216
|
* @param {number} originalIndex
|
|
193
217
|
* @returns {TMemberEntry | null}
|
|
194
218
|
*/
|
|
195
|
-
const toMemberEntry = (node, originalIndex) => {
|
|
196
|
-
const group = getMemberGroup(node);
|
|
219
|
+
const toMemberEntry = (sourceCode, node, originalIndex) => {
|
|
220
|
+
const group = getMemberGroup(sourceCode, node);
|
|
197
221
|
const name = getMemberName(node);
|
|
198
222
|
|
|
199
223
|
if (group === null || name === null) {
|
|
@@ -273,12 +297,24 @@ const sortMembers = (members) => {
|
|
|
273
297
|
return sortedMembers;
|
|
274
298
|
};
|
|
275
299
|
|
|
300
|
+
/**
|
|
301
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
302
|
+
* @param {any} node
|
|
303
|
+
*/
|
|
304
|
+
const getMemberEnd = (sourceCode, node) => {
|
|
305
|
+
const trailingComments = sourceCode
|
|
306
|
+
.getCommentsAfter(node)
|
|
307
|
+
.filter((comment) => comment.loc?.start.line === node.loc.end.line);
|
|
308
|
+
const lastComment = trailingComments.at(-1);
|
|
309
|
+
|
|
310
|
+
return lastComment?.range ? lastComment.range[1] : node.range[1];
|
|
311
|
+
};
|
|
312
|
+
|
|
276
313
|
/**
|
|
277
314
|
* @param {import('eslint').SourceCode} sourceCode
|
|
278
315
|
* @param {any} classBodyNode
|
|
279
|
-
* @param {TMemberEntry[]} members
|
|
280
316
|
*/
|
|
281
|
-
const
|
|
317
|
+
const getSegments = (sourceCode, classBodyNode) => {
|
|
282
318
|
const openingBrace = sourceCode.getFirstToken(classBodyNode);
|
|
283
319
|
const closingBrace = sourceCode.getLastToken(classBodyNode);
|
|
284
320
|
|
|
@@ -288,22 +324,23 @@ const getSegmentEntries = (sourceCode, classBodyNode, members) => {
|
|
|
288
324
|
|
|
289
325
|
let segmentStart = openingBrace.range[1];
|
|
290
326
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
327
|
+
/** @type {string[]} */
|
|
328
|
+
const segmentTexts = classBodyNode.body.map(
|
|
329
|
+
/** @param {any} node */ (node) => {
|
|
330
|
+
const segmentEnd = getMemberEnd(sourceCode, node);
|
|
331
|
+
const segmentText = sourceCode.text.slice(segmentStart, segmentEnd);
|
|
332
|
+
segmentStart = segmentEnd;
|
|
294
333
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
};
|
|
299
|
-
});
|
|
334
|
+
return segmentText;
|
|
335
|
+
},
|
|
336
|
+
);
|
|
300
337
|
|
|
301
338
|
const suffix = sourceCode.text.slice(segmentStart, closingBrace.range[0]);
|
|
302
339
|
|
|
303
340
|
return {
|
|
304
341
|
closingBrace,
|
|
305
342
|
openingBrace,
|
|
306
|
-
|
|
343
|
+
segmentTexts,
|
|
307
344
|
suffix,
|
|
308
345
|
};
|
|
309
346
|
};
|
|
@@ -314,43 +351,37 @@ const getSegmentEntries = (sourceCode, classBodyNode, members) => {
|
|
|
314
351
|
* @param {TMemberEntry[]} members
|
|
315
352
|
*/
|
|
316
353
|
const buildClassOrderFix = (sourceCode, classBodyNode, members) => {
|
|
317
|
-
if (classBodyNode.body.length !== members.length) {
|
|
318
|
-
return null;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
354
|
const sortedMembers = sortMembers(members);
|
|
322
355
|
|
|
323
356
|
if (!sortedMembers) {
|
|
324
357
|
return null;
|
|
325
358
|
}
|
|
326
359
|
|
|
327
|
-
const
|
|
360
|
+
const isAlreadySorted = sortedMembers.every((member, index) => member === members[index]);
|
|
328
361
|
|
|
329
|
-
if (
|
|
362
|
+
if (isAlreadySorted) {
|
|
330
363
|
return null;
|
|
331
364
|
}
|
|
332
365
|
|
|
333
|
-
const
|
|
334
|
-
const segmentEntriesByOriginalIndex = new Map(
|
|
335
|
-
segmentEntries.map(
|
|
336
|
-
/** @param {TSegmentEntry} segmentEntry */ (segmentEntry) => [segmentEntry.originalIndex, segmentEntry],
|
|
337
|
-
),
|
|
338
|
-
);
|
|
339
|
-
const sortedSegmentEntries = sortedMembers.map((member) => segmentEntriesByOriginalIndex.get(member.originalIndex));
|
|
366
|
+
const segments = getSegments(sourceCode, classBodyNode);
|
|
340
367
|
|
|
341
|
-
if (
|
|
368
|
+
if (!segments) {
|
|
342
369
|
return null;
|
|
343
370
|
}
|
|
344
371
|
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return null;
|
|
349
|
-
}
|
|
372
|
+
const { closingBrace, openingBrace, segmentTexts, suffix } = segments;
|
|
373
|
+
const rankedIndexes = new Set(members.map((member) => member.originalIndex));
|
|
374
|
+
let nextSortedMember = 0;
|
|
350
375
|
|
|
351
|
-
const reorderedBody =
|
|
376
|
+
const reorderedBody =
|
|
377
|
+
segmentTexts
|
|
378
|
+
.map((segmentText, index) =>
|
|
379
|
+
rankedIndexes.has(index) ? segmentTexts[sortedMembers[nextSortedMember++].originalIndex] : segmentText,
|
|
380
|
+
)
|
|
381
|
+
.join('') + suffix;
|
|
352
382
|
|
|
353
|
-
return (
|
|
383
|
+
return (/** @type {import('eslint').Rule.RuleFixer} */ fixer) =>
|
|
384
|
+
fixer.replaceTextRange([openingBrace.range[1], closingBrace.range[0]], reorderedBody);
|
|
354
385
|
};
|
|
355
386
|
|
|
356
387
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
@@ -379,7 +410,7 @@ const classMemberOrder = {
|
|
|
379
410
|
const members = node.body
|
|
380
411
|
.map(
|
|
381
412
|
/** @param {any} member @param {number} originalIndex */ (member, originalIndex) =>
|
|
382
|
-
toMemberEntry(member, originalIndex),
|
|
413
|
+
toMemberEntry(sourceCode, member, originalIndex),
|
|
383
414
|
)
|
|
384
415
|
.filter(Boolean);
|
|
385
416
|
|
|
@@ -58,7 +58,6 @@ const rule = {
|
|
|
58
58
|
const prevEndLine = prevStatement.loc.end.line;
|
|
59
59
|
const returnStartLine = node.loc.start.line;
|
|
60
60
|
|
|
61
|
-
// Need at least one empty line (gap of 2+ lines) between prev statement and return
|
|
62
61
|
if (returnStartLine - prevEndLine >= 2) return;
|
|
63
62
|
|
|
64
63
|
const sourceCode = context.sourceCode;
|
|
@@ -67,10 +66,12 @@ const rule = {
|
|
|
67
66
|
node,
|
|
68
67
|
messageId: 'missingEmptyLine',
|
|
69
68
|
fix(fixer) {
|
|
70
|
-
const
|
|
71
|
-
|
|
69
|
+
const trailingComment = sourceCode
|
|
70
|
+
.getCommentsAfter(/** @type {import('eslint').Rule.Node} */ (prevStatement))
|
|
71
|
+
.find((comment) => comment.loc.start.line === prevEndLine && comment.range[1] < node.range[0]);
|
|
72
|
+
const insertionNode = trailingComment ?? prevStatement;
|
|
72
73
|
|
|
73
|
-
return fixer.insertTextAfter(
|
|
74
|
+
return fixer.insertTextAfter(insertionNode, '\n');
|
|
74
75
|
},
|
|
75
76
|
});
|
|
76
77
|
},
|