@ethlete/eslint-plugin 0.1.0-next.0 → 0.1.0-next.10

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 (54) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/package.json +6 -5
  3. package/src/configs/recommended.js +414 -0
  4. package/src/index.js +138 -0
  5. package/src/rules/angular-decorator-property-order.js +205 -0
  6. package/src/rules/class-constant-property.js +365 -0
  7. package/src/rules/class-member-order.js +456 -0
  8. package/src/rules/enforce-routing-view-naming.js +84 -0
  9. package/src/rules/guard-return-newline.js +81 -0
  10. package/src/rules/inject-member-accessibility.js +93 -0
  11. package/src/rules/internals/angular-member-visibility.js +233 -0
  12. package/src/rules/internals/implemented-contract-members.js +294 -0
  13. package/src/rules/no-angular-router-api.js +158 -0
  14. package/src/rules/no-angular-seo-services.js +82 -0
  15. package/src/rules/no-direct-dom-manipulation.js +160 -0
  16. package/src/rules/no-document-cookie.js +55 -0
  17. package/src/rules/no-dom-query.js +81 -0
  18. package/src/rules/no-empty-angular-metadata-arrays.js +104 -0
  19. package/src/rules/no-empty-newlines-between-imports.js +45 -0
  20. package/src/rules/no-inject-chain.js +44 -0
  21. package/src/rules/no-leading-underscore-class-member.js +120 -0
  22. package/src/rules/no-legacy-angular-decorators.js +405 -0
  23. package/src/rules/no-locale-id.js +57 -0
  24. package/src/rules/no-member-alias.js +151 -0
  25. package/src/rules/no-native-observers.js +102 -0
  26. package/src/rules/no-pipe-logic.js +76 -0
  27. package/src/rules/no-public-property.js +85 -0
  28. package/src/rules/no-readonly-signal.js +119 -0
  29. package/src/rules/no-redundant-internal.js +94 -0
  30. package/src/rules/no-redundant-on-push-change-detection.js +237 -0
  31. package/src/rules/no-rxjs-in-effect.js +88 -0
  32. package/src/rules/no-screaming-case-local.js +113 -0
  33. package/src/rules/no-standalone-flag.js +95 -0
  34. package/src/rules/no-subscribe-in-pipe.js +89 -0
  35. package/src/rules/no-subscribe-with-body.js +95 -0
  36. package/src/rules/no-trivial-return-type.js +104 -0
  37. package/src/rules/no-trivial-wrapper-method.js +120 -0
  38. package/src/rules/no-type-only-import.js +79 -0
  39. package/src/rules/no-typed-injected-element-ref.js +88 -0
  40. package/src/rules/no-unused-class-member.js +209 -0
  41. package/src/rules/no-window-location.js +109 -0
  42. package/src/rules/prefer-clone-equal.js +112 -0
  43. package/src/rules/prefer-concise-angular-host-directives.js +225 -0
  44. package/src/rules/prefer-concise-angular-style-metadata.js +108 -0
  45. package/src/rules/prefer-element-dimensions.js +128 -0
  46. package/src/rules/prefer-linked-signal.js +124 -0
  47. package/src/rules/prefer-match-media.js +101 -0
  48. package/src/rules/prefer-rxjs-timer.js +82 -0
  49. package/src/rules/prefer-scroll-state.js +158 -0
  50. package/src/rules/prefer-viewport-size.js +75 -0
  51. package/src/rules/require-dollar-suffix.js +123 -0
  52. package/src/rules/require-on-push-change-detection.js +320 -0
  53. package/src/rules/require-view-encapsulation-none.js +264 -0
  54. package/src/rules/template-member-accessibility.js +204 -0
@@ -0,0 +1,151 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ /**
5
+ * Disallows class members that exist solely as aliases for a nested property or
6
+ * zero-arg method of another class member accessed via `this`.
7
+ *
8
+ * These aliases add an extra layer of indirection without any benefit. Instead,
9
+ * the source member should be widened in accessibility so it can be referenced
10
+ * directly at the call/template site.
11
+ *
12
+ * BAD — property alias:
13
+ * private stackDirective = inject(NotificationStackDirective);
14
+ * protected displayRefs = this.stackDirective.displayRefs; // just an alias
15
+ *
16
+ * BAD — zero-arg method alias:
17
+ * context = inject(SomeContextToken);
18
+ * retry() { this.context.retry(); }
19
+ *
20
+ * GOOD:
21
+ * protected stackDirective = inject(NotificationStackDirective);
22
+ * // use stackDirective.displayRefs directly in the template
23
+ *
24
+ * context = inject(SomeContextToken);
25
+ * // callers use directive.context.retry() directly
26
+ *
27
+ * Property rule fires when the entire initializer is a single `this.a.b`
28
+ * expression — it will not flag deeper chains, computed accesses, or any
29
+ * expression that does more than a plain property read.
30
+ *
31
+ * Method rule fires when a zero-param method body contains exactly one
32
+ * statement that calls `this.a.b()` with zero arguments AND the method name
33
+ * matches the target method name exactly. When the names differ the method is
34
+ * an intentional API rename (e.g. retry → playerResource.reload) and is not
35
+ * flagged.
36
+ */
37
+
38
+ /**
39
+ * Checks whether a method node is a pure zero-arg forwarding alias of the form:
40
+ * method() { this.source.method(); }
41
+ * method() { return this.source.method(); }
42
+ *
43
+ * @param {any} node MethodDefinition
44
+ * @returns {{ sourceName: string, propName: string } | null}
45
+ */
46
+ const getMethodAliasInfo = (node) => {
47
+ if (node.kind === 'constructor' || node.kind === 'get' || node.kind === 'set') return null;
48
+
49
+ const fn = node.value;
50
+ if (!fn || fn.params.length !== 0) return null;
51
+
52
+ const body = fn.body;
53
+ if (!body || body.type !== 'BlockStatement' || body.body.length !== 1) return null;
54
+
55
+ const stmt = body.body[0];
56
+ let callExpr = null;
57
+ if (stmt.type === 'ExpressionStatement') {
58
+ callExpr = stmt.expression;
59
+ } else if (stmt.type === 'ReturnStatement' && stmt.argument) {
60
+ callExpr = stmt.argument;
61
+ }
62
+ if (!callExpr || callExpr.type !== 'CallExpression') return null;
63
+
64
+ // Must have zero arguments
65
+ if (callExpr.arguments.length !== 0) return null;
66
+
67
+ const callee = callExpr.callee;
68
+ // callee must be: this.something.method
69
+ if (callee.type !== 'MemberExpression') return null;
70
+ if (callee.computed) return null;
71
+ if (callee.property.type !== 'Identifier') return null;
72
+
73
+ const obj = callee.object;
74
+ if (obj.type !== 'MemberExpression') return null;
75
+ if (obj.computed) return null;
76
+ if (obj.object.type !== 'ThisExpression') return null;
77
+ if (obj.property.type !== 'Identifier') return null;
78
+
79
+ return { sourceName: obj.property.name, propName: callee.property.name };
80
+ };
81
+
82
+ /** @type {import('eslint').Rule.RuleModule} */
83
+ const noMemberAlias = {
84
+ meta: {
85
+ type: 'suggestion',
86
+ docs: {
87
+ description:
88
+ 'Disallow class members that are pure aliases for a nested property or zero-arg method of another member accessed via `this`.',
89
+ recommended: true,
90
+ },
91
+ messages: {
92
+ noAlias:
93
+ '`{{alias}}` is a pure alias for `this.{{source}}.{{prop}}`. ' +
94
+ 'Remove the alias and widen the accessibility of `{{source}}` so it can be used directly.',
95
+ },
96
+ schema: [],
97
+ },
98
+ create(context) {
99
+ return {
100
+ PropertyDefinition(node) {
101
+ const value = node.value;
102
+ if (!value) return;
103
+
104
+ // Must be: this.something.property
105
+ if (value.type !== 'MemberExpression') return;
106
+ // Outer property must be a plain identifier (not computed)
107
+ if (value.computed) return;
108
+ if (value.property.type !== 'Identifier') return;
109
+
110
+ const obj = value.object;
111
+ // Object must itself be: this.something
112
+ if (obj.type !== 'MemberExpression') return;
113
+ if (obj.computed) return;
114
+ if (obj.object.type !== 'ThisExpression') return;
115
+ if (obj.property.type !== 'Identifier') return;
116
+
117
+ const aliasName = node.key.type === 'Identifier' ? node.key.name : null;
118
+ if (!aliasName) return;
119
+
120
+ const sourceName = obj.property.name;
121
+ const propName = value.property.name;
122
+
123
+ context.report({
124
+ node,
125
+ messageId: 'noAlias',
126
+ data: { alias: aliasName, source: sourceName, prop: propName },
127
+ });
128
+ },
129
+
130
+ MethodDefinition(node) {
131
+ const aliasName = node.key.type === 'Identifier' ? node.key.name : null;
132
+ if (!aliasName) return;
133
+
134
+ const info = getMethodAliasInfo(node);
135
+ if (!info) return;
136
+
137
+ // Only flag when names match — different names indicate an intentional
138
+ // API rename, which is not a pure alias.
139
+ if (aliasName !== info.propName) return;
140
+
141
+ context.report({
142
+ node,
143
+ messageId: 'noAlias',
144
+ data: { alias: aliasName, source: info.sourceName, prop: info.propName },
145
+ });
146
+ },
147
+ };
148
+ },
149
+ };
150
+
151
+ module.exports = noMemberAlias;
@@ -0,0 +1,102 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ /**
5
+ * Disallows raw browser Observer APIs (IntersectionObserver, MutationObserver,
6
+ * ResizeObserver, PerformanceObserver). Use the signal-based utilities from
7
+ * @ethlete/core instead.
8
+ *
9
+ * The signal utilities:
10
+ * - Integrate automatically with Angular's reactivity model
11
+ * - Tear down the observer when the component is destroyed (no memory leaks)
12
+ * - Are composable with other signals and computed()
13
+ * - Require no manual disconnect() calls
14
+ *
15
+ * BAD:
16
+ * new IntersectionObserver(cb, options)
17
+ * new MutationObserver(cb)
18
+ * new ResizeObserver(cb)
19
+ *
20
+ * GOOD:
21
+ * // IntersectionObserver → from @ethlete/core
22
+ * signalElementIntersection(elementRef, options)
23
+ * signalHostElementIntersection(options) // shortcut inside a directive/component
24
+ *
25
+ * // MutationObserver → from @ethlete/core
26
+ * signalElementMutations(elementRef, options)
27
+ * signalHostElementMutations(options)
28
+ *
29
+ * // ResizeObserver → from @ethlete/core
30
+ * signalElementDimensions(elementRef)
31
+ * signalHostElementDimensions()
32
+ */
33
+
34
+ /**
35
+ * Maps Observer constructor names to their @ethlete/core signal equivalents.
36
+ * @type {Record<string, { alternatives: string[], from: string } | null>}
37
+ */
38
+ const OBSERVER_ALTERNATIVES = {
39
+ IntersectionObserver: {
40
+ alternatives: ['signalElementIntersection', 'signalHostElementIntersection'],
41
+ from: '@ethlete/core',
42
+ },
43
+ MutationObserver: {
44
+ alternatives: ['signalElementMutations', 'signalHostElementMutations'],
45
+ from: '@ethlete/core',
46
+ },
47
+ ResizeObserver: {
48
+ alternatives: ['signalElementDimensions', 'signalHostElementDimensions'],
49
+ from: '@ethlete/core',
50
+ },
51
+ PerformanceObserver: null,
52
+ };
53
+
54
+ /** @type {import('eslint').Rule.RuleModule} */
55
+ const noNativeObservers = {
56
+ meta: {
57
+ type: 'suggestion',
58
+ docs: {
59
+ description: "Disallow raw browser Observer APIs. Use signal-based utilities from '@ethlete/core' instead.",
60
+ recommended: true,
61
+ },
62
+ messages: {
63
+ useSignalUtil:
64
+ "Use '{{alternatives}}' from '{{from}}' instead of 'new {{observer}}()'. The signal utility auto-cleans up when the component is destroyed.",
65
+ avoidObserver:
66
+ "Avoid 'new {{observer}}()'. Prefer a reactive signal utility from '@ethlete/core' where possible, or ensure you call disconnect() in inject(DestroyRef).onDestroy().",
67
+ },
68
+ schema: [],
69
+ },
70
+ create(context) {
71
+ return {
72
+ NewExpression(node) {
73
+ if (node.callee.type !== 'Identifier') return;
74
+
75
+ const name = node.callee.name;
76
+ if (!(name in OBSERVER_ALTERNATIVES)) return;
77
+
78
+ const info = OBSERVER_ALTERNATIVES[name];
79
+
80
+ if (info) {
81
+ context.report({
82
+ node,
83
+ messageId: 'useSignalUtil',
84
+ data: {
85
+ observer: name,
86
+ alternatives: info.alternatives.join(' or '),
87
+ from: info.from,
88
+ },
89
+ });
90
+ } else {
91
+ context.report({
92
+ node,
93
+ messageId: 'avoidObserver',
94
+ data: { observer: name },
95
+ });
96
+ }
97
+ },
98
+ };
99
+ },
100
+ };
101
+
102
+ module.exports = noNativeObservers;
@@ -0,0 +1,76 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ /**
5
+ * Checks whether a class node has a @Pipe decorator.
6
+ * @param {import('eslint').Rule.Node} classNode
7
+ */
8
+ const hasPipeDecorator = (classNode) => {
9
+ const decorators = /** @type {any} */ (classNode).decorators ?? [];
10
+ return decorators.some((/** @type {any} */ dec) => {
11
+ const expr = dec.expression;
12
+ if (expr.type === 'CallExpression') {
13
+ return expr.callee.type === 'Identifier' && expr.callee.name === 'Pipe';
14
+ }
15
+ if (expr.type === 'Identifier') {
16
+ return expr.name === 'Pipe';
17
+ }
18
+ return false;
19
+ });
20
+ };
21
+
22
+ /** @type {import('eslint').Rule.RuleModule} */
23
+ const noPipeLogic = {
24
+ meta: {
25
+ type: 'suggestion',
26
+ docs: {
27
+ description:
28
+ 'Pipes must not contain logic in the transform method. Extract to a utility function and assign it: `transform = myUtil;`',
29
+ },
30
+ messages: {
31
+ noLogicInTransform:
32
+ 'Pipes must not contain logic in transform. Extract to a utility function and assign it: `transform = myUtil;`',
33
+ },
34
+ schema: [],
35
+ },
36
+ create(context) {
37
+ return {
38
+ // Case 1: transform() { ... } — method with a body
39
+ MethodDefinition(node) {
40
+ const key = /** @type {any} */ (node).key;
41
+ if (key.type !== 'Identifier' || key.name !== 'transform') return;
42
+
43
+ const classBody = node.parent;
44
+ if (!classBody || classBody.type !== 'ClassBody') return;
45
+
46
+ const classNode = classBody.parent;
47
+ if (!classNode || (classNode.type !== 'ClassDeclaration' && classNode.type !== 'ClassExpression')) return;
48
+
49
+ if (!hasPipeDecorator(classNode)) return;
50
+
51
+ context.report({ node, messageId: 'noLogicInTransform' });
52
+ },
53
+
54
+ // Case 2: transform = () => { ... } or transform = function() { ... }
55
+ PropertyDefinition(node) {
56
+ const key = /** @type {any} */ (node).key;
57
+ if (key.type !== 'Identifier' || key.name !== 'transform') return;
58
+
59
+ const classBody = node.parent;
60
+ if (!classBody || classBody.type !== 'ClassBody') return;
61
+
62
+ const classNode = classBody.parent;
63
+ if (!classNode || (classNode.type !== 'ClassDeclaration' && classNode.type !== 'ClassExpression')) return;
64
+
65
+ if (!hasPipeDecorator(classNode)) return;
66
+
67
+ const value = /** @type {any} */ (node).value;
68
+ if (value && (value.type === 'ArrowFunctionExpression' || value.type === 'FunctionExpression')) {
69
+ context.report({ node, messageId: 'noLogicInTransform' });
70
+ }
71
+ },
72
+ };
73
+ },
74
+ };
75
+
76
+ module.exports = noPipeLogic;
@@ -0,0 +1,85 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ const { getMemberName, isReferencedFromTemplateOrHost } = require('./internals/angular-member-visibility');
5
+
6
+ /**
7
+ * @param {any} node
8
+ */
9
+ const isInjectCall = (node) => {
10
+ if (!node || node.type !== 'CallExpression') return false;
11
+ return node.callee.type === 'Identifier' && node.callee.name === 'inject';
12
+ };
13
+
14
+ /** @type {import('eslint').Rule.RuleModule} */
15
+ const noPublicProperty = {
16
+ meta: {
17
+ type: 'problem',
18
+ docs: {
19
+ description:
20
+ 'Disallow explicit public on class properties, except for injected members that intentionally expose an external API.',
21
+ recommended: true,
22
+ },
23
+ fixable: 'code',
24
+ schema: [],
25
+ messages: {
26
+ noPublicProperty:
27
+ 'No public keyword on class properties. The only exception is an injected member that intentionally exposes an external API.',
28
+ shouldBeProtected:
29
+ 'Member `{{name}}` should be protected instead of public because it is referenced from a template or host binding.',
30
+ },
31
+ },
32
+ create(context) {
33
+ /**
34
+ * @param {any} node
35
+ */
36
+ const removePublicFix = (node) => {
37
+ const publicToken = context.sourceCode.getFirstToken(node, (token) => token.value === 'public');
38
+ const nextToken = publicToken ? context.sourceCode.getTokenAfter(publicToken) : null;
39
+ if (!publicToken || !nextToken) return null;
40
+
41
+ return (fixer) => fixer.removeRange([publicToken.range[0], nextToken.range[0]]);
42
+ };
43
+
44
+ /**
45
+ * @param {any} node
46
+ */
47
+ const replacePublicWithProtectedFix = (node) => {
48
+ const publicToken = context.sourceCode.getFirstToken(node, (token) => token.value === 'public');
49
+ if (!publicToken) return null;
50
+
51
+ return (fixer) => fixer.replaceText(publicToken, 'protected');
52
+ };
53
+
54
+ return {
55
+ PropertyDefinition(node) {
56
+ if (node.accessibility !== 'public') return;
57
+ if (isInjectCall(node.value)) return;
58
+
59
+ const classNode = node.parent && node.parent.type === 'ClassBody' ? node.parent.parent : null;
60
+ const memberName = getMemberName(node);
61
+ const isTemplateVisible =
62
+ classNode && memberName ? isReferencedFromTemplateOrHost(classNode, memberName, context) : false;
63
+
64
+ if (isTemplateVisible) {
65
+ context.report({
66
+ node,
67
+ messageId: 'shouldBeProtected',
68
+ data: { name: memberName },
69
+ fix: replacePublicWithProtectedFix(node),
70
+ });
71
+
72
+ return;
73
+ }
74
+
75
+ context.report({
76
+ node,
77
+ messageId: 'noPublicProperty',
78
+ fix: removePublicFix(node),
79
+ });
80
+ },
81
+ };
82
+ },
83
+ };
84
+
85
+ module.exports = noPublicProperty;
@@ -0,0 +1,119 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ /**
5
+ * Disallows `readonly` on class properties initialized with reactive Angular/RxJS APIs.
6
+ *
7
+ * The `readonly` modifier on a signal, input, computed, or inject() call is misleading —
8
+ * the "readonly" constraint applies to the reference (you can't reassign the property),
9
+ * but the underlying value is mutable. It also prevents common patterns like
10
+ * resetting or swapping the signal.
11
+ *
12
+ * BAD:
13
+ * readonly mySignal = signal(false);
14
+ * readonly myInput = input(false);
15
+ * readonly myComputed = computed(() => this.x() + 1);
16
+ * readonly myService = inject(MyService);
17
+ *
18
+ * GOOD (constants that truly never change):
19
+ * readonly MAX_ITEMS = 100;
20
+ * readonly PLATFORM = inject(PLATFORM_ID); // ← still a violation; assign without readonly
21
+ *
22
+ * GOOD (actual usage):
23
+ * mySignal = signal(false);
24
+ * myInput = input(false);
25
+ * myComputed = computed(() => this.x() + 1);
26
+ * private myService = inject(MyService);
27
+ */
28
+
29
+ /**
30
+ * Names of Angular/RxJS reactive APIs that should never be marked `readonly`.
31
+ */
32
+ const REACTIVE_APIS = new Set([
33
+ // Angular signals
34
+ 'signal',
35
+ 'computed',
36
+ 'linkedSignal',
37
+ // Angular models / inputs / outputs
38
+ 'input',
39
+ 'model',
40
+ 'output',
41
+ 'outputFromObservable',
42
+ // Angular view queries
43
+ 'viewChild',
44
+ 'viewChildren',
45
+ 'contentChild',
46
+ 'contentChildren',
47
+ // Angular DI
48
+ 'inject',
49
+ // Angular lifecycle
50
+ 'effect',
51
+ 'afterNextRender',
52
+ 'afterRender',
53
+ // RxJS interop
54
+ 'toSignal',
55
+ 'toObservable',
56
+ ]);
57
+
58
+ /** @type {import('eslint').Rule.RuleModule} */
59
+ const noReadonlySignal = {
60
+ meta: {
61
+ type: 'suggestion',
62
+ fixable: 'code',
63
+ docs: {
64
+ description: 'Disallow `readonly` on class properties initialized with reactive Angular/RxJS APIs.',
65
+ recommended: true,
66
+ },
67
+ messages: {
68
+ noReadonlySignal:
69
+ "Remove `readonly` from '{{name}}' — reactive APIs return mutable references, and `readonly` gives a false sense of immutability.",
70
+ },
71
+ schema: [],
72
+ },
73
+ create(context) {
74
+ return {
75
+ PropertyDefinition(node) {
76
+ // Only flag when readonly is set
77
+ if (!node.readonly) return;
78
+
79
+ const init = node.value;
80
+ if (!init || init.type !== 'CallExpression') return;
81
+
82
+ const { callee } = init;
83
+ let apiName = null;
84
+
85
+ // Simple call: signal(), input(), computed(), inject(), etc.
86
+ if (callee.type === 'Identifier' && REACTIVE_APIS.has(callee.name)) {
87
+ apiName = callee.name;
88
+ }
89
+ // Member call: input.required(), outputFromObservable.something(), etc.
90
+ else if (
91
+ callee.type === 'MemberExpression' &&
92
+ callee.object.type === 'Identifier' &&
93
+ REACTIVE_APIS.has(callee.object.name)
94
+ ) {
95
+ apiName = `${callee.object.name}.${callee.property.type === 'Identifier' ? callee.property.name : '...'}`;
96
+ }
97
+
98
+ if (apiName) {
99
+ context.report({
100
+ node,
101
+ messageId: 'noReadonlySignal',
102
+ data: { name: apiName },
103
+ fix(fixer) {
104
+ // Find and remove the "readonly" token
105
+ const sourceCode = context.sourceCode;
106
+ const tokens = sourceCode.getTokens(node);
107
+ const readonlyToken = tokens.find((t) => t.value === 'readonly');
108
+ if (!readonlyToken) return null;
109
+ // Remove "readonly " (including the trailing space)
110
+ return fixer.removeRange([readonlyToken.range[0], readonlyToken.range[1] + 1]);
111
+ },
112
+ });
113
+ }
114
+ },
115
+ };
116
+ },
117
+ };
118
+
119
+ module.exports = noReadonlySignal;
@@ -0,0 +1,94 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ /**
5
+ * @param {import('eslint').SourceCode} sourceCode
6
+ * @param {import('eslint').Rule.Node} node
7
+ */
8
+ const getAttachedInternalComment = (sourceCode, node) => {
9
+ const comments = sourceCode.getCommentsBefore(node);
10
+ const attachedComment = comments[comments.length - 1];
11
+
12
+ if (!attachedComment || attachedComment.type !== 'Block') {
13
+ return null;
14
+ }
15
+
16
+ const textBetween = sourceCode.text.slice(attachedComment.range[1], node.range[0]);
17
+
18
+ if (textBetween.trim() !== '') {
19
+ return null;
20
+ }
21
+
22
+ return /@internal\b/u.test(attachedComment.value) ? attachedComment : null;
23
+ };
24
+
25
+ /**
26
+ * @param {any} node
27
+ */
28
+ const getMemberName = (node) => {
29
+ if (node.key?.type === 'Identifier') return node.key.name;
30
+ if (node.key?.type === 'Literal' && typeof node.key.value === 'string') return node.key.value;
31
+
32
+ return 'member';
33
+ };
34
+
35
+ /**
36
+ * @param {any} node
37
+ */
38
+ const getMemberKind = (node) => {
39
+ if (node.type === 'MethodDefinition') return 'method';
40
+ return 'property';
41
+ };
42
+
43
+ /** @type {import('eslint').Rule.RuleModule} */
44
+ const noRedundantInternal = {
45
+ meta: {
46
+ type: 'suggestion',
47
+ docs: {
48
+ description: 'Disallow redundant @internal usage on class members that are already hidden by accessibility.',
49
+ recommended: true,
50
+ },
51
+ fixable: 'code',
52
+ schema: [],
53
+ messages: {
54
+ redundantInternal:
55
+ '`@internal` is redundant on {{kind}} `{{name}}` because {{accessibility}} members are already hidden from consumers.',
56
+ },
57
+ },
58
+ create(context) {
59
+ const sourceCode = context.sourceCode;
60
+
61
+ /**
62
+ * @param {any} node
63
+ */
64
+ const checkMember = (node) => {
65
+ const internalComment = getAttachedInternalComment(sourceCode, node);
66
+
67
+ if (!internalComment) {
68
+ return;
69
+ }
70
+
71
+ if (node.accessibility !== 'private' && node.accessibility !== 'protected') {
72
+ return;
73
+ }
74
+
75
+ context.report({
76
+ node: internalComment,
77
+ messageId: 'redundantInternal',
78
+ data: {
79
+ accessibility: node.accessibility,
80
+ kind: getMemberKind(node),
81
+ name: getMemberName(node),
82
+ },
83
+ fix: (fixer) => fixer.removeRange([internalComment.range[0], node.range[0]]),
84
+ });
85
+ };
86
+
87
+ return {
88
+ PropertyDefinition: checkMember,
89
+ MethodDefinition: checkMember,
90
+ };
91
+ },
92
+ };
93
+
94
+ module.exports = noRedundantInternal;