@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/migrations.json +12 -0
  3. package/package.json +13 -2
  4. package/src/configs/recommended.js +37 -27
  5. package/src/generators/generators.json +10 -0
  6. package/src/generators/restore-module-augmentation-interfaces/migration.js +50 -0
  7. package/src/generators/restore-module-augmentation-interfaces/restore-module-augmentation-interfaces.js +103 -0
  8. package/src/generators/restore-module-augmentation-interfaces/schema.json +14 -0
  9. package/src/index.js +8 -2
  10. package/src/rules/angular-decorator-property-order.js +24 -7
  11. package/src/rules/class-constant-property.js +1 -1
  12. package/src/rules/class-member-order.js +60 -8
  13. package/src/rules/consistent-type-definitions.js +60 -0
  14. package/src/rules/enforce-routing-view-naming.js +34 -23
  15. package/src/rules/guard-return-newline.js +4 -7
  16. package/src/rules/internals/angular-member-visibility.js +24 -2
  17. package/src/rules/internals/angular-metadata-fix.js +231 -1
  18. package/src/rules/internals/angular-version.js +50 -0
  19. package/src/rules/internals/implemented-contract-members.js +26 -11
  20. package/src/rules/no-angular-router-api.js +86 -33
  21. package/src/rules/no-angular-seo-services.js +1 -0
  22. package/src/rules/no-async-await.js +5 -1
  23. package/src/rules/no-cdk-import.js +32 -7
  24. package/src/rules/no-csp-unsafe.js +362 -0
  25. package/src/rules/no-direct-dom-manipulation.js +50 -28
  26. package/src/rules/no-document-cookie.js +51 -8
  27. package/src/rules/no-dom-query.js +5 -2
  28. package/src/rules/no-empty-angular-metadata-arrays.js +2 -0
  29. package/src/rules/no-empty-newlines-between-imports.js +4 -1
  30. package/src/rules/no-impure-top-level-provider.js +42 -7
  31. package/src/rules/no-leading-underscore-class-member.js +27 -2
  32. package/src/rules/no-legacy-angular-decorators.js +11 -6
  33. package/src/rules/no-legacy-prepare-without-injector.js +57 -12
  34. package/src/rules/no-legacy-query-import.js +61 -16
  35. package/src/rules/no-member-alias.js +0 -5
  36. package/src/rules/no-native-observers.js +89 -24
  37. package/src/rules/no-pipe-logic.js +1 -2
  38. package/src/rules/no-readonly-signal.js +1 -5
  39. package/src/rules/no-redundant-internal.js +5 -2
  40. package/src/rules/no-redundant-on-push-change-detection.js +3 -48
  41. package/src/rules/no-rxjs-in-effect.js +7 -5
  42. package/src/rules/no-screaming-case-local.js +3 -5
  43. package/src/rules/no-standalone-flag.js +1 -0
  44. package/src/rules/no-subscribe-with-body.js +30 -1
  45. package/src/rules/no-template-literal-before-inline-template.js +6 -0
  46. package/src/rules/no-trivial-return-type.js +18 -1
  47. package/src/rules/no-unused-class-member.js +1 -1
  48. package/src/rules/no-window-location.js +60 -22
  49. package/src/rules/prefer-clone-equal.js +61 -8
  50. package/src/rules/prefer-concise-angular-host-directives.js +39 -11
  51. package/src/rules/prefer-concise-angular-style-metadata.js +10 -2
  52. package/src/rules/prefer-element-dimensions.js +264 -11
  53. package/src/rules/prefer-linked-signal.js +37 -39
  54. package/src/rules/prefer-match-media.js +17 -3
  55. package/src/rules/prefer-rxjs-timer.js +26 -4
  56. package/src/rules/prefer-scroll-state.js +0 -4
  57. package/src/rules/prefer-static-boolean-properties.js +1 -0
  58. package/src/rules/prefer-viewport-size.js +153 -16
  59. package/src/rules/require-dollar-suffix.js +11 -3
  60. package/src/rules/require-form-submit.js +4 -1
  61. package/src/rules/require-on-push-change-detection.js +7 -259
  62. package/src/rules/require-view-encapsulation-none.js +6 -214
  63. 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
@@ -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.24",
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
- // ── TypeScript ──────────────────────────────────────────────────────────
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 _ or # prefixes; enforce camelCase / PascalCase / UPPER_CASE
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
- segmentStart = tokenAfter.range[1];
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
- (entry, index) =>
193
- `${entry.segmentText}${index < sortedEntries.length - 1 || trailingComma ? ',' : ''}`,
194
- )
195
- .join('') + suffix;
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.type !== 'PropertyDefinition') return false;
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.type !== 'PropertyDefinition') return false;
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.type === 'PropertyDefinition') {
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.type !== 'PropertyDefinition' || !node.value) {
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
- segmentTexts
378
- .map((segmentText, index) =>
379
- rankedIndexes.has(index) ? segmentTexts[sortedMembers[nextSortedMember++].originalIndex] : segmentText,
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) =>