@ethlete/eslint-plugin 1.0.0-next.24 → 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 +32 -0
- package/migrations.json +12 -0
- package/package.json +13 -2
- package/src/configs/recommended.js +37 -27
- package/src/generators/generators.json +10 -0
- package/src/generators/restore-module-augmentation-interfaces/migration.js +50 -0
- package/src/generators/restore-module-augmentation-interfaces/restore-module-augmentation-interfaces.js +103 -0
- package/src/generators/restore-module-augmentation-interfaces/schema.json +14 -0
- package/src/index.js +8 -2
- 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/consistent-type-definitions.js +60 -0
- 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-csp-unsafe.js +362 -0
- 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-linked-signal.js +37 -39
- 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,37 @@
|
|
|
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
|
+
|
|
21
|
+
## 1.0.0-next.25
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- The `@angular-eslint/*` output rules moved from `recommendedTs` into a new `recommendedAngularTs` config, which `recommended` includes, so NestJS and other non-Angular projects can use `recommendedTs` without the `@angular-eslint` plugin.
|
|
26
|
+
- `recommendedTs` now uses `ethlete/consistent-type-definitions` instead of `@typescript-eslint/consistent-type-definitions`, so `--fix` no longer turns an interface inside `declare module` or `declare global` (such as the theme-name registry the `@ethlete/core` generators emit) into a type alias that merges into nothing.
|
|
27
|
+
- New `no-csp-unsafe` rule (error in `recommendedTs` and `recommendedTemplate`) flags style attributes, un-nonced `<script>`/`<style>` elements, `eval` and string timers that a strict CSP blocks.
|
|
28
|
+
- `et update` turns the type aliases the old `consistent-type-definitions` fix left in `declare module` and `declare global` blocks back into interfaces, and renames the rule in `eslint-disable` directives.
|
|
29
|
+
- `prefer-linked-signal` now also reports an `effect()` that only calls `.set()` behind an `if`/`else`.
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- `ethlete/no-csp-unsafe` fixes a static template `style="…"` attribute. It rewrites `style="width: 40%; margin-top: 4px"` to `[style]="{ width: '40%', 'margin-top': '4px' }"`, which a strict CSP allows. A value with a quote, `url(`, `!important` or a comment stays a manual fix.
|
|
34
|
+
|
|
3
35
|
## 1.0.0-next.24
|
|
4
36
|
|
|
5
37
|
### Minor Changes
|
package/migrations.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"migrations": [
|
|
3
|
+
{
|
|
4
|
+
"name": "module-augmentation-interfaces",
|
|
5
|
+
"version": "1.0.0-next.25",
|
|
6
|
+
"kind": "auto",
|
|
7
|
+
"description": "Turn the type aliases the old consistent-type-definitions fix left in declare module / declare global blocks back into interfaces, and rename its eslint-disable directives to ethlete/consistent-type-definitions.",
|
|
8
|
+
"generator": "@ethlete/eslint-plugin:restore-module-augmentation-interfaces",
|
|
9
|
+
"docs": "/eslint/rules"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
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",
|
|
7
7
|
"peerDependencies": {
|
|
8
8
|
"@angular-eslint/eslint-plugin-template": ">=21.0.0",
|
|
9
9
|
"@angular/core": ">=21.0.0",
|
|
10
|
+
"@nx/devkit": ">=21.0.0",
|
|
10
11
|
"@typescript-eslint/eslint-plugin": ">=8.0.0",
|
|
12
|
+
"@typescript-eslint/parser": ">=8.0.0",
|
|
11
13
|
"eslint": ">=9.0.0",
|
|
12
14
|
"typescript": ">=5.9.0"
|
|
13
|
-
}
|
|
15
|
+
},
|
|
16
|
+
"peerDependenciesMeta": {
|
|
17
|
+
"@nx/devkit": {
|
|
18
|
+
"optional": true
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"ethlete": {
|
|
22
|
+
"migrations": "./migrations.json"
|
|
23
|
+
},
|
|
24
|
+
"generators": "./src/generators/generators.json"
|
|
14
25
|
}
|
|
@@ -9,14 +9,9 @@
|
|
|
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
|
-
//
|
|
17
|
-
|
|
18
|
-
// No interface — use type
|
|
19
|
-
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
|
|
13
|
+
// No interface — use type (owned by ethlete/consistent-type-definitions)
|
|
14
|
+
'@typescript-eslint/consistent-type-definitions': 'off',
|
|
20
15
|
|
|
21
16
|
// No any
|
|
22
17
|
'@typescript-eslint/no-explicit-any': 'error',
|
|
@@ -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
|
|
|
@@ -248,6 +235,9 @@ const recommendedTs = {
|
|
|
248
235
|
// No enum / const enum — use a const object with `as const` plus a derived union type
|
|
249
236
|
'ethlete/no-enum': 'error',
|
|
250
237
|
|
|
238
|
+
// No interface — use type; an interface augmenting a module or the global scope is exempt
|
|
239
|
+
'ethlete/consistent-type-definitions': 'error',
|
|
240
|
+
|
|
251
241
|
// No .subscribe() inside effect() or computed()
|
|
252
242
|
'ethlete/no-rxjs-in-effect': 'error',
|
|
253
243
|
|
|
@@ -311,6 +301,10 @@ const recommendedTs = {
|
|
|
311
301
|
// — use getCookie / setCookie / hasCookie / deleteCookie from '@ethlete/core' instead
|
|
312
302
|
'ethlete/no-document-cookie': 'error',
|
|
313
303
|
|
|
304
|
+
// No code a strict CSP (no 'unsafe-inline' / 'unsafe-eval') blocks: style attributes,
|
|
305
|
+
// un-nonced <script>/<style> elements, eval / new Function / string timers
|
|
306
|
+
'ethlete/no-csp-unsafe': 'error',
|
|
307
|
+
|
|
314
308
|
// Injected providers must be private by default, and protected only when
|
|
315
309
|
// referenced from an Angular template or host binding.
|
|
316
310
|
'ethlete/inject-member-accessibility': 'error',
|
|
@@ -350,16 +344,6 @@ const recommendedTs = {
|
|
|
350
344
|
// Angular language service completions for the rest of the file
|
|
351
345
|
'ethlete/no-template-literal-before-inline-template': 'error',
|
|
352
346
|
|
|
353
|
-
// ── Angular outputs ─────────────────────────────────────────────────────
|
|
354
|
-
|
|
355
|
-
// No on-prefixed outputs (onSelectDate → selectDate)
|
|
356
|
-
'@angular-eslint/no-output-on-prefix': 'error',
|
|
357
|
-
|
|
358
|
-
// No outputs named after native DOM events (change, click, etc.)
|
|
359
|
-
'@angular-eslint/no-output-native': 'error',
|
|
360
|
-
|
|
361
|
-
// ── Angular components ──────────────────────────────────────────────────
|
|
362
|
-
|
|
363
347
|
// Require ChangeDetectionStrategy.OnPush on Angular <= 21, where it is opt-in
|
|
364
348
|
// (version-aware: inert on Angular 22+, where OnPush is the default).
|
|
365
349
|
'ethlete/require-on-push-change-detection': 'error',
|
|
@@ -402,6 +386,23 @@ const recommendedTs = {
|
|
|
402
386
|
},
|
|
403
387
|
};
|
|
404
388
|
|
|
389
|
+
/**
|
|
390
|
+
* `@angular-eslint/*` rules for TypeScript files in Angular projects; the base config must register
|
|
391
|
+
* the `@angular-eslint` plugin.
|
|
392
|
+
*
|
|
393
|
+
* @type {import('eslint').Linter.Config}
|
|
394
|
+
*/
|
|
395
|
+
const recommendedAngularTs = {
|
|
396
|
+
files: ['**/*.ts'],
|
|
397
|
+
rules: {
|
|
398
|
+
// No on-prefixed outputs (onSelectDate → selectDate)
|
|
399
|
+
'@angular-eslint/no-output-on-prefix': 'error',
|
|
400
|
+
|
|
401
|
+
// No outputs named after native DOM events (change, click, etc.)
|
|
402
|
+
'@angular-eslint/no-output-native': 'error',
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
|
|
405
406
|
/**
|
|
406
407
|
* Flat config entries for Angular HTML template files.
|
|
407
408
|
* @type {import('eslint').Linter.Config}
|
|
@@ -413,12 +414,20 @@ const recommendedTemplate = {
|
|
|
413
414
|
'@angular-eslint/template/no-any': 'error',
|
|
414
415
|
// Prefer plain attribute over unnecessary property binding for static strings: etIcon="foo" not [etIcon]="'foo'"
|
|
415
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
|
+
],
|
|
416
423
|
// Prefer plain attribute over unnecessary property binding for static booleans: isReadonly not [isReadonly]="true"
|
|
417
424
|
// (warn + suggestion-only: the rewrite is only safe when the input has a booleanAttribute transform,
|
|
418
425
|
// which a template rule cannot verify)
|
|
419
426
|
'ethlete/prefer-static-boolean-properties': 'warn',
|
|
420
427
|
// A form must handle its own submission
|
|
421
428
|
'ethlete/require-form-submit': 'error',
|
|
429
|
+
// No static style attribute or [attr.style] binding - a strict CSP blocks both
|
|
430
|
+
'ethlete/no-csp-unsafe': 'error',
|
|
422
431
|
},
|
|
423
432
|
};
|
|
424
433
|
|
|
@@ -432,6 +441,7 @@ const recommendedSpec = {
|
|
|
432
441
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
433
442
|
'no-restricted-globals': 'off',
|
|
434
443
|
'ethlete/no-async-await': 'off',
|
|
444
|
+
'ethlete/no-csp-unsafe': 'off',
|
|
435
445
|
'ethlete/no-direct-dom-manipulation': 'off',
|
|
436
446
|
'ethlete/no-document-cookie': 'off',
|
|
437
447
|
'ethlete/no-dom-query': 'off',
|
|
@@ -444,4 +454,4 @@ const recommendedSpec = {
|
|
|
444
454
|
},
|
|
445
455
|
};
|
|
446
456
|
|
|
447
|
-
module.exports = { recommendedTs, recommendedTemplate, recommendedSpec };
|
|
457
|
+
module.exports = { recommendedTs, recommendedAngularTs, recommendedTemplate, recommendedSpec };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/nx-generators",
|
|
3
|
+
"generators": {
|
|
4
|
+
"restore-module-augmentation-interfaces": {
|
|
5
|
+
"implementation": "./restore-module-augmentation-interfaces/migration",
|
|
6
|
+
"schema": "./restore-module-augmentation-interfaces/schema.json",
|
|
7
|
+
"description": "Turn type aliases in declare module / declare global blocks back into interfaces, and rename consistent-type-definitions eslint directives"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { formatFiles, visitNotIgnoredFiles } = require('@nx/devkit');
|
|
5
|
+
const { restoreModuleAugmentationInterfaces } = require('./restore-module-augmentation-interfaces');
|
|
6
|
+
|
|
7
|
+
const SOURCE_FILE = /\.(?:[cm]?ts|tsx|[cm]?js|jsx)$/;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {import('@nx/devkit').Tree} tree
|
|
11
|
+
* @param {{ skipFormat?: boolean }} schema
|
|
12
|
+
*/
|
|
13
|
+
const migrateModuleAugmentationInterfaces = async (tree, schema) => {
|
|
14
|
+
console.log('\n🔄 Restoring module augmentation interfaces...');
|
|
15
|
+
|
|
16
|
+
let filesChanged = 0;
|
|
17
|
+
let reviewCount = 0;
|
|
18
|
+
|
|
19
|
+
visitNotIgnoredFiles(tree, '', (filePath) => {
|
|
20
|
+
if (!SOURCE_FILE.test(filePath)) return;
|
|
21
|
+
|
|
22
|
+
const before = tree.read(filePath, 'utf-8');
|
|
23
|
+
|
|
24
|
+
if (!before) return;
|
|
25
|
+
|
|
26
|
+
const result = restoreModuleAugmentationInterfaces(filePath, before);
|
|
27
|
+
|
|
28
|
+
for (const entry of result.review) {
|
|
29
|
+
reviewCount++;
|
|
30
|
+
console.warn(
|
|
31
|
+
` ⚠ ${filePath}:${entry.line}: type ${entry.name} in a module augmentation is not an object literal, so it was left alone. An augmentation only merges as an interface.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!result.changed) return;
|
|
36
|
+
|
|
37
|
+
tree.write(filePath, result.content);
|
|
38
|
+
filesChanged++;
|
|
39
|
+
console.log(` ✓ ${filePath}`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
console.log(filesChanged === 0 ? ' Nothing to migrate.\n' : `\n✅ Migrated ${filesChanged} file(s).\n`);
|
|
43
|
+
|
|
44
|
+
if (reviewCount > 0) console.warn(` Review ${reviewCount} type alias(es) in module augmentations manually.\n`);
|
|
45
|
+
|
|
46
|
+
if (!schema.skipFormat) await formatFiles(tree);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
module.exports = migrateModuleAugmentationInterfaces;
|
|
50
|
+
module.exports.default = migrateModuleAugmentationInterfaces;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const ts = require('typescript');
|
|
5
|
+
|
|
6
|
+
const OLD_RULE = '@typescript-eslint/consistent-type-definitions';
|
|
7
|
+
const NEW_RULE = 'ethlete/consistent-type-definitions';
|
|
8
|
+
|
|
9
|
+
const LINE_DIRECTIVE = /\/\/[ \t]*eslint-(?:disable|enable)(?:-next-line|-line)?\b[^\n]*/g;
|
|
10
|
+
const BLOCK_DIRECTIVE = /\/\*[ \t]*eslint-(?:disable|enable)(?:-next-line|-line)?\b(?:(?!\*\/)[\s\S])*\*\//g;
|
|
11
|
+
|
|
12
|
+
const AUGMENTATION = /\bdeclare\s+(?:module|global)\b/;
|
|
13
|
+
|
|
14
|
+
const renameInDirective = (/** @type {string} */ directive) => directive.split(OLD_RULE).join(NEW_RULE);
|
|
15
|
+
|
|
16
|
+
/** Renames `@typescript-eslint/consistent-type-definitions` in every eslint-disable/enable directive. */
|
|
17
|
+
const renameDirectives = (/** @type {string} */ content) =>
|
|
18
|
+
content.replace(LINE_DIRECTIVE, renameInDirective).replace(BLOCK_DIRECTIVE, renameInDirective);
|
|
19
|
+
|
|
20
|
+
const isAugmentation = (/** @type {ts.Statement} */ statement) =>
|
|
21
|
+
ts.isModuleDeclaration(statement) &&
|
|
22
|
+
(ts.isStringLiteral(statement.name) || (statement.flags & ts.NodeFlags.GlobalAugmentation) !== 0) &&
|
|
23
|
+
statement.body !== undefined &&
|
|
24
|
+
ts.isModuleBlock(statement.body);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {readonly ts.Statement[]} statements
|
|
28
|
+
* @param {ts.TypeAliasDeclaration[]} found
|
|
29
|
+
*/
|
|
30
|
+
const collectAugmentationAliases = (statements, found) => {
|
|
31
|
+
for (const statement of statements) {
|
|
32
|
+
if (!isAugmentation(statement)) continue;
|
|
33
|
+
|
|
34
|
+
const body = /** @type {ts.ModuleBlock} */ (/** @type {ts.ModuleDeclaration} */ (statement).body);
|
|
35
|
+
|
|
36
|
+
for (const member of body.statements) {
|
|
37
|
+
if (ts.isTypeAliasDeclaration(member)) found.push(member);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
collectAugmentationAliases(body.statements, found);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return found;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Turns every object-literal `type X = { … }` directly inside a `declare module '…'` or
|
|
48
|
+
* `declare global` block back into `interface X { … }`, and renames
|
|
49
|
+
* `@typescript-eslint/consistent-type-definitions` in eslint directives to
|
|
50
|
+
* `ethlete/consistent-type-definitions`. Aliases of any other shape are returned in `review`.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} filePath
|
|
53
|
+
* @param {string} content
|
|
54
|
+
* @returns {{ content: string; changed: boolean; review: { name: string; line: number }[] }}
|
|
55
|
+
*/
|
|
56
|
+
const restoreModuleAugmentationInterfaces = (filePath, content) => {
|
|
57
|
+
if (!AUGMENTATION.test(content)) {
|
|
58
|
+
const renamed = renameDirectives(content);
|
|
59
|
+
|
|
60
|
+
return { content: renamed, changed: renamed !== content, review: [] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
64
|
+
const aliases = collectAugmentationAliases(sourceFile.statements, []);
|
|
65
|
+
|
|
66
|
+
/** @type {{ name: string; line: number }[]} */
|
|
67
|
+
const review = [];
|
|
68
|
+
/** @type {{ start: number; end: number; text: string }[]} */
|
|
69
|
+
const edits = [];
|
|
70
|
+
|
|
71
|
+
for (const alias of aliases) {
|
|
72
|
+
const name = alias.name.text;
|
|
73
|
+
|
|
74
|
+
if (!ts.isTypeLiteralNode(alias.type)) {
|
|
75
|
+
review.push({ name, line: sourceFile.getLineAndCharacterOfPosition(alias.getStart(sourceFile)).line + 1 });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const modifiers = (alias.modifiers ?? []).map((modifier) => `${modifier.getText(sourceFile)} `).join('');
|
|
80
|
+
const typeParameters = content
|
|
81
|
+
.slice(alias.name.end, alias.type.pos)
|
|
82
|
+
.replace(/\s*=\s*$/, '')
|
|
83
|
+
.trim();
|
|
84
|
+
|
|
85
|
+
edits.push({
|
|
86
|
+
start: alias.modifiers?.[0]?.getStart(sourceFile) ?? alias.getStart(sourceFile),
|
|
87
|
+
end: alias.end,
|
|
88
|
+
text: `${modifiers}interface ${name}${typeParameters} ${alias.type.getText(sourceFile)}`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let next = content;
|
|
93
|
+
|
|
94
|
+
for (const edit of edits.sort((left, right) => right.start - left.start)) {
|
|
95
|
+
next = next.slice(0, edit.start) + edit.text + next.slice(edit.end);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
next = renameDirectives(next);
|
|
99
|
+
|
|
100
|
+
return { content: next, changed: next !== content, review };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
module.exports = { restoreModuleAugmentationInterfaces };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "restore-module-augmentation-interfaces",
|
|
4
|
+
"title": "Restore module augmentation interfaces",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"skipFormat": {
|
|
8
|
+
"type": "boolean",
|
|
9
|
+
"description": "Skip formatting files after migration",
|
|
10
|
+
"default": false
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"required": []
|
|
14
|
+
}
|
package/src/index.js
CHANGED
|
@@ -30,6 +30,7 @@ const noLegacyAngularDecorators = require('./rules/no-legacy-angular-decorators'
|
|
|
30
30
|
const noAngularSeoServices = require('./rules/no-angular-seo-services');
|
|
31
31
|
const preferCloneEqual = require('./rules/prefer-clone-equal');
|
|
32
32
|
const noDocumentCookie = require('./rules/no-document-cookie');
|
|
33
|
+
const noCspUnsafe = require('./rules/no-csp-unsafe');
|
|
33
34
|
const injectMemberAccessibility = require('./rules/inject-member-accessibility');
|
|
34
35
|
const noRedundantInternal = require('./rules/no-redundant-internal');
|
|
35
36
|
const templateMemberAccessibility = require('./rules/template-member-accessibility');
|
|
@@ -60,8 +61,9 @@ const noCdkImport = require('./rules/no-cdk-import');
|
|
|
60
61
|
const noLegacyQueryImport = require('./rules/no-legacy-query-import');
|
|
61
62
|
const noAsyncAwait = require('./rules/no-async-await');
|
|
62
63
|
const noEnum = require('./rules/no-enum');
|
|
64
|
+
const consistentTypeDefinitions = require('./rules/consistent-type-definitions');
|
|
63
65
|
const takeUntilDestroyedLast = require('./rules/take-until-destroyed-last');
|
|
64
|
-
const { recommendedTs, recommendedTemplate, recommendedSpec } = require('./configs/recommended');
|
|
66
|
+
const { recommendedTs, recommendedAngularTs, recommendedTemplate, recommendedSpec } = require('./configs/recommended');
|
|
65
67
|
const { version } = require('../package.json');
|
|
66
68
|
|
|
67
69
|
/** @type {import('eslint').ESLint.Plugin} */
|
|
@@ -102,6 +104,7 @@ const plugin = {
|
|
|
102
104
|
'no-angular-seo-services': noAngularSeoServices,
|
|
103
105
|
'prefer-clone-equal': preferCloneEqual,
|
|
104
106
|
'no-document-cookie': noDocumentCookie,
|
|
107
|
+
'no-csp-unsafe': noCspUnsafe,
|
|
105
108
|
'inject-member-accessibility': injectMemberAccessibility,
|
|
106
109
|
'no-redundant-internal': noRedundantInternal,
|
|
107
110
|
'template-member-accessibility': templateMemberAccessibility,
|
|
@@ -131,6 +134,7 @@ const plugin = {
|
|
|
131
134
|
'no-legacy-query-import': noLegacyQueryImport,
|
|
132
135
|
'no-async-await': noAsyncAwait,
|
|
133
136
|
'no-enum': noEnum,
|
|
137
|
+
'consistent-type-definitions': consistentTypeDefinitions,
|
|
134
138
|
},
|
|
135
139
|
};
|
|
136
140
|
|
|
@@ -150,6 +154,8 @@ const recommendedTemplateWithPlugin = {
|
|
|
150
154
|
const configs = {
|
|
151
155
|
/** TypeScript rules (merged into a files: ['**\/*.ts'] block) */
|
|
152
156
|
recommendedTs: recommendedTsWithPlugin,
|
|
157
|
+
/** `@angular-eslint/*` rules for TypeScript files (files: ['**\/*.ts']) */
|
|
158
|
+
recommendedAngularTs,
|
|
153
159
|
/** Angular template rules (merged into a files: ['**\/*.html'] block) */
|
|
154
160
|
recommendedTemplate: recommendedTemplateWithPlugin,
|
|
155
161
|
/** Relaxed rules for spec files (files: ['**\/*.spec.ts']) */
|
|
@@ -158,7 +164,7 @@ const configs = {
|
|
|
158
164
|
* Both together as a flat array — the most common usage:
|
|
159
165
|
* export default [...baseConfig, ...ethlete.configs.recommended]
|
|
160
166
|
*/
|
|
161
|
-
recommended: [recommendedTsWithPlugin, recommendedTemplateWithPlugin, recommendedSpec],
|
|
167
|
+
recommended: [recommendedTsWithPlugin, recommendedAngularTs, recommendedTemplateWithPlugin, recommendedSpec],
|
|
162
168
|
};
|
|
163
169
|
|
|
164
170
|
const ethletePlugin = { ...plugin, configs };
|
|
@@ -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) =>
|