@ethlete/eslint-plugin 1.0.0-next.25 → 1.0.0-next.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/package.json +2 -1
- package/src/configs/recommended.js +7 -16
- package/src/rules/angular-decorator-property-order.js +24 -7
- package/src/rules/class-constant-property.js +1 -1
- package/src/rules/class-member-order.js +60 -8
- package/src/rules/enforce-routing-view-naming.js +34 -23
- package/src/rules/guard-return-newline.js +4 -7
- package/src/rules/internals/angular-member-visibility.js +24 -2
- package/src/rules/internals/angular-metadata-fix.js +231 -1
- package/src/rules/internals/angular-version.js +50 -0
- package/src/rules/internals/implemented-contract-members.js +26 -11
- package/src/rules/no-angular-router-api.js +86 -33
- package/src/rules/no-angular-seo-services.js +1 -0
- package/src/rules/no-async-await.js +5 -1
- package/src/rules/no-cdk-import.js +32 -7
- package/src/rules/no-direct-dom-manipulation.js +50 -28
- package/src/rules/no-document-cookie.js +51 -8
- package/src/rules/no-dom-query.js +5 -2
- package/src/rules/no-empty-angular-metadata-arrays.js +2 -0
- package/src/rules/no-empty-newlines-between-imports.js +4 -1
- package/src/rules/no-impure-top-level-provider.js +42 -7
- package/src/rules/no-leading-underscore-class-member.js +27 -2
- package/src/rules/no-legacy-angular-decorators.js +11 -6
- package/src/rules/no-legacy-prepare-without-injector.js +57 -12
- package/src/rules/no-legacy-query-import.js +61 -16
- package/src/rules/no-member-alias.js +0 -5
- package/src/rules/no-native-observers.js +89 -24
- package/src/rules/no-pipe-logic.js +1 -2
- package/src/rules/no-readonly-signal.js +1 -5
- package/src/rules/no-redundant-internal.js +5 -2
- package/src/rules/no-redundant-on-push-change-detection.js +3 -48
- package/src/rules/no-rxjs-in-effect.js +7 -5
- package/src/rules/no-screaming-case-local.js +3 -5
- package/src/rules/no-standalone-flag.js +1 -0
- package/src/rules/no-subscribe-with-body.js +30 -1
- package/src/rules/no-template-literal-before-inline-template.js +6 -0
- package/src/rules/no-trivial-return-type.js +18 -1
- package/src/rules/no-unused-class-member.js +1 -1
- package/src/rules/no-window-location.js +60 -22
- package/src/rules/prefer-clone-equal.js +61 -8
- package/src/rules/prefer-concise-angular-host-directives.js +39 -11
- package/src/rules/prefer-concise-angular-style-metadata.js +10 -2
- package/src/rules/prefer-element-dimensions.js +264 -11
- package/src/rules/prefer-match-media.js +17 -3
- package/src/rules/prefer-rxjs-timer.js +26 -4
- package/src/rules/prefer-scroll-state.js +0 -4
- package/src/rules/prefer-static-boolean-properties.js +1 -0
- package/src/rules/prefer-viewport-size.js +153 -16
- package/src/rules/require-dollar-suffix.js +11 -3
- package/src/rules/require-form-submit.js +4 -1
- package/src/rules/require-on-push-change-detection.js +7 -259
- package/src/rules/require-view-encapsulation-none.js +6 -214
- package/src/rules/template-member-accessibility.js +12 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0-next.26
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Drop the last module-scope pins from the components import floor (2,870 B gz) and add a `forbidImpureReads` option to `no-impure-top-level-provider`.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- The `class-member-order` fix keeps members without semicolons apart, so the reordered class still parses.
|
|
12
|
+
- `recommendedTemplate` no longer reports an empty `<a>` or `<button>` carrying `et-match-card`, `et-match-participant` or `aria-labelledby` under `@angular-eslint/template/elements-content`.
|
|
13
|
+
- Close rule gaps: `no-document-cookie`, `no-window-location`, `prefer-rxjs-timer`, `no-subscribe-with-body`, `prefer-match-media`, `prefer-clone-equal` and `no-dom-query` catch more escapes, and the `no-async-await` `play` exemption only applies in story files.
|
|
14
|
+
- The `prefer-concise-angular-host-directives` fix no longer lets a trailing line comment swallow the closing brace.
|
|
15
|
+
- Lint-scan fixes: observer, DOM, `accessor`/`abstract`/`static` member and on-push version detection gaps are closed, and importing `constructor` or `toString` from `@ethlete/cdk` or `@ethlete/query` is no longer misread as a mapped symbol.
|
|
16
|
+
- Several rule fixes: `guard-return-newline` no longer counts a comment as the blank line, fixers keep multi-line imports, `no-legacy-prepare-without-injector` accepts `untracked` and IIFEs, `no-cdk-import` re-reads a changed migration map, `router.snapshot` is no longer reported, and `no-rxjs-in-effect` covers `afterRenderEffect` and `linkedSignal`.
|
|
17
|
+
- `no-angular-router-api`, `prefer-element-dimensions`, `prefer-viewport-size` and the legacy query rules report fewer false positives and catch more escapes, and member-visibility rules see files changed during an editor session.
|
|
18
|
+
- Declare `@typescript-eslint/parser` as a peer dependency.
|
|
19
|
+
- Fix several rule gaps: decorator property order keeps trailing comments, `require-dollar-suffix` handles `toObservable` and `asObservable` but not `partition`, and legacy decorator, template literal type, recursion and routing view checks catch more cases.
|
|
20
|
+
|
|
3
21
|
## 1.0.0-next.25
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethlete/eslint-plugin",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.26",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"@angular/core": ">=21.0.0",
|
|
10
10
|
"@nx/devkit": ">=21.0.0",
|
|
11
11
|
"@typescript-eslint/eslint-plugin": ">=8.0.0",
|
|
12
|
+
"@typescript-eslint/parser": ">=8.0.0",
|
|
12
13
|
"eslint": ">=9.0.0",
|
|
13
14
|
"typescript": ">=5.9.0"
|
|
14
15
|
},
|
|
@@ -9,12 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
const recommendedTs = {
|
|
11
11
|
files: ['**/*.ts'],
|
|
12
|
-
plugins: {
|
|
13
|
-
// The plugin itself is injected by the caller (see index.js)
|
|
14
|
-
},
|
|
15
12
|
rules: {
|
|
16
|
-
// ── TypeScript ──────────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
13
|
// No interface — use type (owned by ethlete/consistent-type-definitions)
|
|
19
14
|
'@typescript-eslint/consistent-type-definitions': 'off',
|
|
20
15
|
|
|
@@ -36,8 +31,6 @@ const recommendedTs = {
|
|
|
36
31
|
},
|
|
37
32
|
],
|
|
38
33
|
|
|
39
|
-
// ── Naming & formatting ─────────────────────────────────────────────────
|
|
40
|
-
|
|
41
34
|
// No var
|
|
42
35
|
'no-var': 'error',
|
|
43
36
|
|
|
@@ -53,7 +46,7 @@ const recommendedTs = {
|
|
|
53
46
|
// Max two function parameters
|
|
54
47
|
'max-params': ['error', 2],
|
|
55
48
|
|
|
56
|
-
// No
|
|
49
|
+
// No # or trailing _ (leading _ only on variables, parameters, properties, methods); enforce camelCase / PascalCase / UPPER_CASE
|
|
57
50
|
'@typescript-eslint/naming-convention': [
|
|
58
51
|
'error',
|
|
59
52
|
{
|
|
@@ -103,8 +96,6 @@ const recommendedTs = {
|
|
|
103
96
|
},
|
|
104
97
|
],
|
|
105
98
|
|
|
106
|
-
// ── Banned syntax ───────────────────────────────────────────────────────
|
|
107
|
-
|
|
108
99
|
'no-restricted-syntax': [
|
|
109
100
|
'error',
|
|
110
101
|
// enum / const enum is owned by ethlete/no-enum, so a lib can keep it on while it works
|
|
@@ -188,8 +179,6 @@ const recommendedTs = {
|
|
|
188
179
|
},
|
|
189
180
|
],
|
|
190
181
|
|
|
191
|
-
// ── Native DOM globals ──────────────────────────────────────────────────
|
|
192
|
-
|
|
193
182
|
// No direct document/window access — use inject(DOCUMENT) / inject(WINDOW) instead
|
|
194
183
|
'no-restricted-globals': [
|
|
195
184
|
'error',
|
|
@@ -203,8 +192,6 @@ const recommendedTs = {
|
|
|
203
192
|
},
|
|
204
193
|
],
|
|
205
194
|
|
|
206
|
-
// ── Custom plugin rules ─────────────────────────────────────────────────
|
|
207
|
-
|
|
208
195
|
// No inject(X).member chaining — assign to a const first
|
|
209
196
|
'ethlete/no-inject-chain': 'error',
|
|
210
197
|
|
|
@@ -357,8 +344,6 @@ const recommendedTs = {
|
|
|
357
344
|
// Angular language service completions for the rest of the file
|
|
358
345
|
'ethlete/no-template-literal-before-inline-template': 'error',
|
|
359
346
|
|
|
360
|
-
// ── Angular components ──────────────────────────────────────────────────
|
|
361
|
-
|
|
362
347
|
// Require ChangeDetectionStrategy.OnPush on Angular <= 21, where it is opt-in
|
|
363
348
|
// (version-aware: inert on Angular 22+, where OnPush is the default).
|
|
364
349
|
'ethlete/require-on-push-change-detection': 'error',
|
|
@@ -429,6 +414,12 @@ const recommendedTemplate = {
|
|
|
429
414
|
'@angular-eslint/template/no-any': 'error',
|
|
430
415
|
// Prefer plain attribute over unnecessary property binding for static strings: etIcon="foo" not [etIcon]="'foo'"
|
|
431
416
|
'@angular-eslint/template/prefer-static-string-properties': 'error',
|
|
417
|
+
// Merged with the rule's own allowList (aria-label, title, innerText, ...), not replacing it.
|
|
418
|
+
// The et-* hosts render their own content and accessible name.
|
|
419
|
+
'@angular-eslint/template/elements-content': [
|
|
420
|
+
'error',
|
|
421
|
+
{ allowList: ['aria-labelledby', 'et-match-card', 'et-match-participant'] },
|
|
422
|
+
],
|
|
432
423
|
// Prefer plain attribute over unnecessary property binding for static booleans: isReadonly not [isReadonly]="true"
|
|
433
424
|
// (warn + suggestion-only: the rewrite is only safe when the input has a booleanAttribute transform,
|
|
434
425
|
// which a template rule cannot verify)
|
|
@@ -9,7 +9,7 @@ const { getAngularDecoratorName } = require('./internals/import-resolution');
|
|
|
9
9
|
/** @typedef {import('estree').Property & TNodeWithRange} TPropertyNode */
|
|
10
10
|
/** @typedef {import('estree').SpreadElement & TNodeWithRange} TSpreadElementNode */
|
|
11
11
|
/** @typedef {import('estree').ObjectExpression & TNodeWithRange & { properties: Array<TPropertyNode | TSpreadElementNode> }} TObjectExpressionNode */
|
|
12
|
-
/** @typedef {{ hasComma: boolean; originalIndex: number; orderIndex: number; property: TPropertyNode; segmentText: string }} TPropertyEntry */
|
|
12
|
+
/** @typedef {{ endsWithLineComment: boolean; hasComma: boolean; trailingText: string; originalIndex: number; orderIndex: number; property: TPropertyNode; segmentText: string }} TPropertyEntry */
|
|
13
13
|
|
|
14
14
|
/** @type {Record<TDecoratorName, TDecoratorConfig>} */
|
|
15
15
|
const DECORATOR_CONFIG = {
|
|
@@ -88,6 +88,7 @@ const angularDecoratorPropertyOrder = {
|
|
|
88
88
|
type: 'layout',
|
|
89
89
|
docs: {
|
|
90
90
|
description: 'Require a consistent property order in Angular @Component and @Directive metadata objects.',
|
|
91
|
+
recommended: true,
|
|
91
92
|
},
|
|
92
93
|
fixable: 'code',
|
|
93
94
|
messages: {
|
|
@@ -142,14 +143,25 @@ const angularDecoratorPropertyOrder = {
|
|
|
142
143
|
const segmentEnd = hasComma && tokenAfter ? tokenAfter.range[0] : propertyNode.range[1];
|
|
143
144
|
const segmentText = sourceCode.text.slice(segmentStart, segmentEnd);
|
|
144
145
|
|
|
146
|
+
let trailingText = '';
|
|
147
|
+
let endsWithLineComment = false;
|
|
148
|
+
|
|
145
149
|
if (hasComma && tokenAfter) {
|
|
146
|
-
|
|
150
|
+
const sameLineComments = sourceCode
|
|
151
|
+
.getCommentsAfter(tokenAfter)
|
|
152
|
+
.filter((comment) => comment.loc?.start.line === tokenAfter.loc.end.line);
|
|
153
|
+
const lastComment = sameLineComments[sameLineComments.length - 1];
|
|
154
|
+
segmentStart = lastComment?.range ? lastComment.range[1] : tokenAfter.range[1];
|
|
155
|
+
trailingText = sourceCode.text.slice(tokenAfter.range[1], segmentStart);
|
|
156
|
+
endsWithLineComment = lastComment?.type === 'Line';
|
|
147
157
|
} else {
|
|
148
158
|
segmentStart = propertyNode.range[1];
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
return {
|
|
162
|
+
endsWithLineComment,
|
|
152
163
|
hasComma,
|
|
164
|
+
trailingText,
|
|
153
165
|
originalIndex,
|
|
154
166
|
orderIndex,
|
|
155
167
|
property: propertyNode,
|
|
@@ -188,11 +200,16 @@ const angularDecoratorPropertyOrder = {
|
|
|
188
200
|
fix(fixer) {
|
|
189
201
|
const reorderedBody =
|
|
190
202
|
sortedEntries
|
|
191
|
-
.map(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
203
|
+
.map((entry, index) => {
|
|
204
|
+
const needsComma = index < sortedEntries.length - 1 || trailingComma || entry.trailingText !== '';
|
|
205
|
+
const previous = sortedEntries[index - 1];
|
|
206
|
+
const lead = previous?.endsWithLineComment && !entry.segmentText.startsWith('\n') ? '\n' : '';
|
|
207
|
+
|
|
208
|
+
return `${lead}${entry.segmentText}${needsComma ? ',' : ''}${entry.trailingText}`;
|
|
209
|
+
})
|
|
210
|
+
.join('') +
|
|
211
|
+
(sortedEntries.at(-1)?.endsWithLineComment && !suffix.startsWith('\n') ? '\n' : '') +
|
|
212
|
+
suffix;
|
|
196
213
|
|
|
197
214
|
return fixer.replaceTextRange([openingBrace.range[1], closingBrace.range[0]], reorderedBody);
|
|
198
215
|
},
|
|
@@ -63,7 +63,7 @@ const getCallRootName = (node) => {
|
|
|
63
63
|
return callee.name;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier') {
|
|
66
|
+
if (callee.type === 'MemberExpression' && !callee.computed && callee.object.type === 'Identifier') {
|
|
67
67
|
return callee.object.name;
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -30,6 +30,9 @@ const MEMBER_GROUP_INDEX = new Map(
|
|
|
30
30
|
MEMBER_GROUP_ORDER.map(/** @param {TMemberGroup} group @param {number} index */ (group, index) => [group, index]),
|
|
31
31
|
);
|
|
32
32
|
|
|
33
|
+
/** @param {any} node */
|
|
34
|
+
const isProperty = (node) => node.type === 'PropertyDefinition' || node.type === 'AccessorProperty';
|
|
35
|
+
|
|
33
36
|
/**
|
|
34
37
|
* @param {any} key
|
|
35
38
|
*/
|
|
@@ -95,7 +98,7 @@ const getAngularCallName = (sourceCode, value) => {
|
|
|
95
98
|
* @param {Set<string>} apiNames
|
|
96
99
|
*/
|
|
97
100
|
const isPropertyInitializedWith = (sourceCode, node, apiNames) => {
|
|
98
|
-
if (node
|
|
101
|
+
if (!isProperty(node)) return false;
|
|
99
102
|
|
|
100
103
|
const apiName = getAngularCallName(sourceCode, node.value);
|
|
101
104
|
return apiName !== null && apiNames.has(apiName);
|
|
@@ -108,7 +111,7 @@ const INJECT_HELPER_NAME_PATTERN = /^inject[A-Z]/;
|
|
|
108
111
|
* @param {any} node
|
|
109
112
|
*/
|
|
110
113
|
const isPropertyInitializedWithInject = (sourceCode, node) => {
|
|
111
|
-
if (node
|
|
114
|
+
if (!isProperty(node)) return false;
|
|
112
115
|
if (getAngularCallName(sourceCode, node.value) === 'inject') return true;
|
|
113
116
|
|
|
114
117
|
const rootName = getCallRootName(node.value);
|
|
@@ -123,7 +126,7 @@ const isPropertyInitializedWithInject = (sourceCode, node) => {
|
|
|
123
126
|
const getMemberGroup = (sourceCode, node) => {
|
|
124
127
|
if (node.static) return null;
|
|
125
128
|
|
|
126
|
-
if (node
|
|
129
|
+
if (isProperty(node)) {
|
|
127
130
|
if (isPropertyInitializedWithInject(sourceCode, node)) {
|
|
128
131
|
return 'inject';
|
|
129
132
|
}
|
|
@@ -194,7 +197,7 @@ const collectThisReferences = (node, references) => {
|
|
|
194
197
|
* @param {any} node
|
|
195
198
|
*/
|
|
196
199
|
const getInitializerDependencies = (node) => {
|
|
197
|
-
if (node
|
|
200
|
+
if (!isProperty(node) || !node.value) {
|
|
198
201
|
return new Set();
|
|
199
202
|
}
|
|
200
203
|
|
|
@@ -345,6 +348,42 @@ const getSegments = (sourceCode, classBodyNode) => {
|
|
|
345
348
|
};
|
|
346
349
|
};
|
|
347
350
|
|
|
351
|
+
const ASI_HAZARD_TOKENS = new Set(['[', '(', '*', '`', 'in', 'instanceof']);
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* @param {any} node
|
|
355
|
+
*/
|
|
356
|
+
const endsWithOwnBlock = (node) => node.type === 'StaticBlock' || (node.type === 'MethodDefinition' && node.value.body);
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
360
|
+
* @param {any} node
|
|
361
|
+
* @param {string} segmentText
|
|
362
|
+
* @param {any} nextNode
|
|
363
|
+
* @param {string} nextText
|
|
364
|
+
*/
|
|
365
|
+
const joinMovedSegment = (sourceCode, node, segmentText, nextNode, nextText) => {
|
|
366
|
+
const nextLeadingText = nextNode
|
|
367
|
+
? nextText.slice(0, nextText.length - (getMemberEnd(sourceCode, nextNode) - nextNode.range[0]))
|
|
368
|
+
: nextText;
|
|
369
|
+
const nextStartsOnNewLine = nextLeadingText.includes('\n');
|
|
370
|
+
const lastToken = sourceCode.getLastToken(node);
|
|
371
|
+
const needsSemicolon =
|
|
372
|
+
lastToken?.value !== ';' &&
|
|
373
|
+
!endsWithOwnBlock(node) &&
|
|
374
|
+
nextNode &&
|
|
375
|
+
(!nextStartsOnNewLine || ASI_HAZARD_TOKENS.has(sourceCode.getFirstToken(nextNode)?.value ?? ''));
|
|
376
|
+
|
|
377
|
+
const memberEnd = getMemberEnd(sourceCode, node);
|
|
378
|
+
const nodeEnd = segmentText.length - (memberEnd - node.range[1]);
|
|
379
|
+
const withSemicolon = needsSemicolon ? `${segmentText.slice(0, nodeEnd)};${segmentText.slice(nodeEnd)}` : segmentText;
|
|
380
|
+
const endsWithLineComment = sourceCode
|
|
381
|
+
.getCommentsAfter(node)
|
|
382
|
+
.some((comment) => comment.type === 'Line' && comment.range?.[1] === memberEnd);
|
|
383
|
+
|
|
384
|
+
return endsWithLineComment && !nextStartsOnNewLine ? `${withSemicolon}\n` : withSemicolon;
|
|
385
|
+
};
|
|
386
|
+
|
|
348
387
|
/**
|
|
349
388
|
* @param {import('eslint').SourceCode} sourceCode
|
|
350
389
|
* @param {any} classBodyNode
|
|
@@ -373,11 +412,24 @@ const buildClassOrderFix = (sourceCode, classBodyNode, members) => {
|
|
|
373
412
|
const rankedIndexes = new Set(members.map((member) => member.originalIndex));
|
|
374
413
|
let nextSortedMember = 0;
|
|
375
414
|
|
|
415
|
+
const order = segmentTexts.map((_, index) =>
|
|
416
|
+
rankedIndexes.has(index) ? sortedMembers[nextSortedMember++].originalIndex : index,
|
|
417
|
+
);
|
|
418
|
+
|
|
376
419
|
const reorderedBody =
|
|
377
|
-
|
|
378
|
-
.map((
|
|
379
|
-
|
|
380
|
-
|
|
420
|
+
order
|
|
421
|
+
.map((originalIndex, position) => {
|
|
422
|
+
const nextIndex = order[position + 1] ?? segmentTexts.length;
|
|
423
|
+
if (nextIndex === originalIndex + 1) return segmentTexts[originalIndex];
|
|
424
|
+
|
|
425
|
+
return joinMovedSegment(
|
|
426
|
+
sourceCode,
|
|
427
|
+
classBodyNode.body[originalIndex],
|
|
428
|
+
segmentTexts[originalIndex],
|
|
429
|
+
classBodyNode.body[nextIndex] ?? null,
|
|
430
|
+
segmentTexts[nextIndex] ?? suffix,
|
|
431
|
+
);
|
|
432
|
+
})
|
|
381
433
|
.join('') + suffix;
|
|
382
434
|
|
|
383
435
|
return (/** @type {import('eslint').Rule.RuleFixer} */ fixer) =>
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* loadComponent: () => import('./items-list-view/items-list-view.component').then((m) => m.ItemsListViewComponent)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
const VIEW_PATH = /-view(?![\w-])/;
|
|
14
|
+
|
|
13
15
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
14
16
|
const enforceRoutingViewNaming = {
|
|
15
17
|
meta: {
|
|
@@ -17,6 +19,7 @@ const enforceRoutingViewNaming = {
|
|
|
17
19
|
docs: {
|
|
18
20
|
description:
|
|
19
21
|
'Routing components must import from a path containing "-view" and use a class name ending in "ViewComponent".',
|
|
22
|
+
recommended: true,
|
|
20
23
|
},
|
|
21
24
|
messages: {
|
|
22
25
|
pathMustContainView: 'Routing import path must contain "-view". Got: "{{path}}".',
|
|
@@ -25,48 +28,56 @@ const enforceRoutingViewNaming = {
|
|
|
25
28
|
schema: [],
|
|
26
29
|
},
|
|
27
30
|
create(context) {
|
|
31
|
+
/** @param {any} importExpr */
|
|
32
|
+
const checkImportPath = (importExpr) => {
|
|
33
|
+
if (!importExpr || importExpr.type !== 'ImportExpression') return;
|
|
34
|
+
|
|
35
|
+
const source = importExpr.source;
|
|
36
|
+
if (source && source.type === 'Literal' && typeof source.value === 'string' && !VIEW_PATH.test(source.value)) {
|
|
37
|
+
context.report({
|
|
38
|
+
node: source,
|
|
39
|
+
messageId: 'pathMustContainView',
|
|
40
|
+
data: { path: source.value },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
28
45
|
return {
|
|
29
46
|
Property(node) {
|
|
30
47
|
const key = /** @type {any} */ (node).key;
|
|
31
48
|
if (key.type !== 'Identifier' || key.name !== 'loadComponent') return;
|
|
32
49
|
|
|
33
|
-
// Must be an arrow function: () => import(...).then(m => m.XxxViewComponent)
|
|
34
50
|
const arrow = /** @type {any} */ (node).value;
|
|
35
51
|
if (!arrow || arrow.type !== 'ArrowFunctionExpression') return;
|
|
36
52
|
|
|
37
53
|
const body = arrow.body;
|
|
38
|
-
if (!body
|
|
54
|
+
if (!body) return;
|
|
55
|
+
|
|
56
|
+
if (body.type === 'ImportExpression') {
|
|
57
|
+
checkImportPath(body);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (body.type !== 'CallExpression') return;
|
|
39
62
|
|
|
40
63
|
const callee = body.callee;
|
|
41
64
|
if (!callee || callee.type !== 'MemberExpression') return;
|
|
42
65
|
if (callee.property.type !== 'Identifier' || callee.property.name !== 'then') return;
|
|
43
66
|
|
|
44
|
-
|
|
45
|
-
const importExpr = callee.object;
|
|
46
|
-
if (importExpr && importExpr.type === 'ImportExpression') {
|
|
47
|
-
const source = importExpr.source;
|
|
48
|
-
if (source && source.type === 'Literal' && typeof source.value === 'string') {
|
|
49
|
-
if (!source.value.includes('-view')) {
|
|
50
|
-
context.report({
|
|
51
|
-
node: source,
|
|
52
|
-
messageId: 'pathMustContainView',
|
|
53
|
-
data: { path: source.value },
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Check the class name in .then(m => m.XxxViewComponent)
|
|
60
|
-
const args = body.arguments;
|
|
61
|
-
if (!args || args.length === 0) return;
|
|
67
|
+
checkImportPath(callee.object);
|
|
62
68
|
|
|
63
|
-
const callback =
|
|
69
|
+
const callback = body.arguments?.[0];
|
|
64
70
|
if (!callback || callback.type !== 'ArrowFunctionExpression') return;
|
|
65
71
|
|
|
66
72
|
const cbBody = callback.body;
|
|
67
|
-
|
|
73
|
+
const returned =
|
|
74
|
+
cbBody?.type === 'BlockStatement'
|
|
75
|
+
? cbBody.body.find(/** @param {any} statement */ (statement) => statement.type === 'ReturnStatement')
|
|
76
|
+
?.argument
|
|
77
|
+
: cbBody;
|
|
78
|
+
if (!returned || returned.type !== 'MemberExpression') return;
|
|
68
79
|
|
|
69
|
-
const prop =
|
|
80
|
+
const prop = returned.property;
|
|
70
81
|
if (!prop || prop.type !== 'Identifier') return;
|
|
71
82
|
|
|
72
83
|
if (!prop.name.endsWith('ViewComponent')) {
|
|
@@ -25,6 +25,7 @@ const rule = {
|
|
|
25
25
|
type: 'layout',
|
|
26
26
|
docs: {
|
|
27
27
|
description: 'Require an empty line before a return in a multi-statement if-block (guard clause).',
|
|
28
|
+
recommended: true,
|
|
28
29
|
},
|
|
29
30
|
fixable: 'whitespace',
|
|
30
31
|
schema: [],
|
|
@@ -37,18 +38,14 @@ const rule = {
|
|
|
37
38
|
ReturnStatement(node) {
|
|
38
39
|
const blockParent = node.parent;
|
|
39
40
|
|
|
40
|
-
// Must be directly inside a BlockStatement
|
|
41
41
|
if (!blockParent || blockParent.type !== 'BlockStatement') return;
|
|
42
42
|
|
|
43
|
-
// The block's parent must be an IfStatement's consequent
|
|
44
43
|
const ifStatement = blockParent.parent;
|
|
45
44
|
if (!ifStatement || ifStatement.type !== 'IfStatement') return;
|
|
46
45
|
if (ifStatement.consequent !== blockParent) return;
|
|
47
46
|
|
|
48
|
-
// Block must have more than one statement (multi-statement guard)
|
|
49
47
|
if (blockParent.body.length <= 1) return;
|
|
50
48
|
|
|
51
|
-
// The return must be the last statement in the block
|
|
52
49
|
if (blockParent.body[blockParent.body.length - 1] !== node) return;
|
|
53
50
|
|
|
54
51
|
const prevStatement = blockParent.body[blockParent.body.length - 2];
|
|
@@ -56,11 +53,11 @@ const rule = {
|
|
|
56
53
|
if (!node.loc || !prevStatement.loc) return;
|
|
57
54
|
|
|
58
55
|
const prevEndLine = prevStatement.loc.end.line;
|
|
59
|
-
const
|
|
56
|
+
const sourceCode = context.sourceCode;
|
|
60
57
|
|
|
61
|
-
|
|
58
|
+
const textBetween = sourceCode.text.slice(prevStatement.range[1], node.range[0]);
|
|
62
59
|
|
|
63
|
-
|
|
60
|
+
if (/\n[ \t]*\r?\n/.test(textBetween)) return;
|
|
64
61
|
|
|
65
62
|
context.report({
|
|
66
63
|
node,
|
|
@@ -92,6 +92,28 @@ const readFileIfExists = (filePath) => {
|
|
|
92
92
|
return null;
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
+
/** @type {WeakMap<import('eslint').SourceCode, Map<string, string | null>>} */
|
|
96
|
+
const TEMPLATE_TEXT_CACHE = new WeakMap();
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
100
|
+
* @param {string} templatePath
|
|
101
|
+
*/
|
|
102
|
+
const readTemplateText = (sourceCode, templatePath) => {
|
|
103
|
+
let templates = TEMPLATE_TEXT_CACHE.get(sourceCode);
|
|
104
|
+
|
|
105
|
+
if (!templates) {
|
|
106
|
+
templates = new Map();
|
|
107
|
+
TEMPLATE_TEXT_CACHE.set(sourceCode, templates);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!templates.has(templatePath)) {
|
|
111
|
+
templates.set(templatePath, readFileIfExists(templatePath));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return templates.get(templatePath) ?? null;
|
|
115
|
+
};
|
|
116
|
+
|
|
95
117
|
/**
|
|
96
118
|
* @param {import('eslint').Rule.RuleContext} context
|
|
97
119
|
*/
|
|
@@ -183,7 +205,7 @@ const isReferencedFromTemplateOrHostMetadata = (memberName, metadata, context) =
|
|
|
183
205
|
|
|
184
206
|
if (templateUrl && filename) {
|
|
185
207
|
const templatePath = path.resolve(path.dirname(filename), templateUrl);
|
|
186
|
-
const templateText =
|
|
208
|
+
const templateText = readTemplateText(context.sourceCode, templatePath);
|
|
187
209
|
if (templateReferencesMember(memberName, templateText)) {
|
|
188
210
|
return true;
|
|
189
211
|
}
|
|
@@ -209,7 +231,7 @@ const isReferencedFromTemplateOrHostMetadata = (memberName, metadata, context) =
|
|
|
209
231
|
* @param {any} node
|
|
210
232
|
*/
|
|
211
233
|
const getMemberName = (node) => {
|
|
212
|
-
if (node.key?.type === 'Identifier') return node.key.name;
|
|
234
|
+
if (node.key?.type === 'Identifier' && !node.computed) return node.key.name;
|
|
213
235
|
if (node.key?.type === 'Literal' && typeof node.key.value === 'string') return node.key.value;
|
|
214
236
|
|
|
215
237
|
return null;
|