@ethlete/eslint-plugin 1.0.0-next.21 → 1.0.0-next.23

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 (41) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +3 -4
  3. package/src/configs/recommended.js +20 -10
  4. package/src/index.js +4 -1
  5. package/src/rules/class-constant-property.js +27 -0
  6. package/src/rules/guard-return-newline.js +5 -4
  7. package/src/rules/inject-member-accessibility.js +17 -21
  8. package/src/rules/internals/angular-io.js +19 -1
  9. package/src/rules/internals/angular-member-visibility.js +5 -5
  10. package/src/rules/internals/angular-metadata-fix.js +22 -0
  11. package/src/rules/internals/member-accessibility-fix.js +26 -0
  12. package/src/rules/no-angular-seo-services.js +7 -7
  13. package/src/rules/no-direct-dom-manipulation.js +17 -0
  14. package/src/rules/no-effect-cleanup-return.js +8 -6
  15. package/src/rules/no-empty-angular-metadata-arrays.js +3 -19
  16. package/src/rules/no-enum.js +115 -0
  17. package/src/rules/no-leading-underscore-class-member.js +7 -0
  18. package/src/rules/no-legacy-angular-decorators.js +8 -0
  19. package/src/rules/no-legacy-prepare-without-injector.js +42 -26
  20. package/src/rules/no-legacy-query-import.js +1 -1
  21. package/src/rules/no-locale-id.js +2 -14
  22. package/src/rules/no-member-alias.js +17 -0
  23. package/src/rules/no-native-observers.js +4 -1
  24. package/src/rules/no-pipe-logic.js +1 -0
  25. package/src/rules/no-readonly-signal.js +13 -5
  26. package/src/rules/no-redundant-internal.js +21 -2
  27. package/src/rules/no-redundant-on-push-change-detection.js +28 -38
  28. package/src/rules/no-standalone-flag.js +4 -19
  29. package/src/rules/no-trivial-return-type.js +1 -0
  30. package/src/rules/no-trivial-wrapper-method.js +3 -0
  31. package/src/rules/no-type-only-import.js +7 -2
  32. package/src/rules/no-typed-injected-element-ref.js +16 -9
  33. package/src/rules/no-unused-class-member.js +28 -5
  34. package/src/rules/prefer-match-media.js +2 -2
  35. package/src/rules/prefer-scroll-state.js +3 -67
  36. package/src/rules/prefer-static-boolean-properties.js +30 -0
  37. package/src/rules/prefer-viewport-size.js +1 -7
  38. package/src/rules/require-form-submit.js +2 -28
  39. package/src/rules/require-on-push-change-detection.js +31 -11
  40. package/src/rules/require-view-encapsulation-none.js +31 -9
  41. package/src/rules/template-member-accessibility.js +4 -21
@@ -6,7 +6,7 @@
6
6
  * calling window.matchMedia() directly or using Angular CDK's BreakpointObserver.
7
7
  *
8
8
  * The utilities:
9
- * - Return reactive signals that update when the media query result changes
9
+ * - Offer both one-shot boolean reads and reactive signals
10
10
  * - Are SSR-safe (no window access during server rendering)
11
11
  * - Are automatically cleaned up when the component is destroyed
12
12
  *
@@ -47,7 +47,7 @@ const preferMatchMedia = {
47
47
  },
48
48
  messages: {
49
49
  preferMatchMedia:
50
- "Avoid 'window.matchMedia()' directly. Use 'injectMediaQueryIsMatched(query)' or a pre-built helper (injectCanHover, injectHasTouchInput, injectIsPortrait, injectBreakpointIsMatched, …) from '@ethlete/core' — they return reactive signals and are SSR-safe.",
50
+ "Avoid 'window.matchMedia()' directly. Use 'injectMediaQueryIsMatched(query)' for a one-shot SSR-safe boolean, or 'injectObserveMediaQuery(query)' when the result must update reactively.",
51
51
  noBreakpointObserver:
52
52
  "Do not use Angular CDK's 'BreakpointObserver' directly. Use 'injectBreakpointObserver()' from '@ethlete/core' instead, which wraps it with a reactive signal API and proper DI scoping.",
53
53
  },
@@ -3,23 +3,13 @@
3
3
 
4
4
  /**
5
5
  * Prefers signalElementScrollState / signalHostElementScrollState from @ethlete/core
6
- * over imperatively reading scroll position properties inside reactive contexts,
7
- * and disallows adding raw scroll event listeners in favour of the same signal utilities.
6
+ * instead of raw scroll event listeners.
8
7
  *
9
8
  * The signal utilities:
10
9
  * - Set up a scroll event listener that keeps a reactive signal in sync
11
10
  * - Clean up automatically when the component is destroyed
12
11
  * - Provide debouncing and direction tracking built in
13
12
  *
14
- * NOTE: The property-read check is a WARNING — imperative one-shot reads of
15
- * scrollTop/scrollLeft are fine in event handlers and animations; only flag
16
- * inside effect() / computed().
17
- *
18
- * BAD (reactive context):
19
- * effect(() => {
20
- * const top = this.el.nativeElement.scrollTop; // stale snapshot ❌
21
- * });
22
- *
23
13
  * BAD (scroll listeners — always wrong):
24
14
  * el.addEventListener('scroll', fn); // ❌
25
15
  * fromEvent(el, 'scroll').subscribe(fn); // ❌
@@ -31,53 +21,17 @@
31
21
  * // Or use <et-scrollable> / ScrollableComponent for list scrolling
32
22
  */
33
23
 
34
- /** Scroll position properties read from DOM elements. */
35
- const SCROLL_PROPS = new Set(['scrollTop', 'scrollLeft', 'scrollY', 'scrollX']);
36
-
37
- /**
38
- * Returns the name of the wrapping reactive context ('effect' | 'computed') if
39
- * the node is inside one, crossing at least one function boundary.
40
- * @param {import('eslint').Rule.Node} node
41
- * @returns {string | null}
42
- */
43
- const getReactiveContext = (node) => {
44
- let crossedFunctionBoundary = false;
45
- let current = node.parent;
46
-
47
- while (current) {
48
- if (current.type === 'ArrowFunctionExpression' || current.type === 'FunctionExpression') {
49
- crossedFunctionBoundary = true;
50
- }
51
-
52
- if (
53
- crossedFunctionBoundary &&
54
- current.type === 'CallExpression' &&
55
- current.callee.type === 'Identifier' &&
56
- (current.callee.name === 'effect' || current.callee.name === 'computed')
57
- ) {
58
- return current.callee.name;
59
- }
60
-
61
- current = current.parent;
62
- }
63
-
64
- return null;
65
- };
66
-
67
24
  /** @type {import('eslint').Rule.RuleModule} */
68
25
  const preferScrollState = {
69
26
  meta: {
70
27
  type: 'suggestion',
71
28
  docs: {
72
- description:
73
- "Prefer 'signalElementScrollState()' / 'signalHostElementScrollState()' from '@ethlete/core' over reading scroll position properties inside reactive contexts.",
29
+ description: "Prefer the scroll signal utilities from '@ethlete/core' over raw scroll event listeners.",
74
30
  recommended: true,
75
31
  },
76
32
  messages: {
77
- preferScrollState:
78
- "Avoid reading '{{prop}}' inside {{context}}() — it creates a stale snapshot. Use 'signalElementScrollState(elementRef)' or 'signalHostElementScrollState()' from '@ethlete/core' instead. For scroll direction, use 'signalElementLastScrollDirection()' / 'signalHostElementLastScrollDirection()'.",
79
33
  noScrollListener:
80
- "Do not add 'scroll' event listeners directly. Use 'signalElementScrollState(elementRef)' / 'signalHostElementScrollState()' for scroll position or state, 'signalElementLastScrollDirection(elementRef)' / 'signalHostElementLastScrollDirection()' for scroll direction, or the 'ScrollableComponent' (et-scrollable) from '@ethlete/cdk' for scrollable lists. All are imported from '@ethlete/core' or '@ethlete/cdk'.",
34
+ "Do not add 'scroll' event listeners directly. Use 'signalElementScrollState(elementRef)' / 'signalHostElementScrollState()' for scroll state, 'signalElementLastScrollDirection(elementRef)' / 'signalHostElementLastScrollDirection()' for direction, or 'ScrollableComponent' from '@ethlete/components'.",
81
35
  },
82
36
  schema: [],
83
37
  },
@@ -133,24 +87,6 @@ const preferScrollState = {
133
87
  context.report({ node, messageId: 'noScrollListener' });
134
88
  }
135
89
  },
136
-
137
- MemberExpression(node) {
138
- if (node.property.type !== 'Identifier') return;
139
- if (!SCROLL_PROPS.has(node.property.name)) return;
140
-
141
- // Only flag reads (not assignments like el.scrollTop = 0).
142
- const parent = node.parent;
143
- if (parent && parent.type === 'AssignmentExpression' && parent.left === node) return;
144
-
145
- const reactiveCtx = getReactiveContext(node);
146
- if (!reactiveCtx) return;
147
-
148
- context.report({
149
- node,
150
- messageId: 'preferScrollState',
151
- data: { prop: node.property.name, context: reactiveCtx },
152
- });
153
- },
154
90
  };
155
91
  },
156
92
  };
@@ -23,6 +23,33 @@
23
23
  * which covers the string-literal case (where the rewrite is always safe).
24
24
  */
25
25
 
26
+ const NATIVE_BOOLEAN_PROPERTIES = new Set([
27
+ 'allowFullscreen',
28
+ 'async',
29
+ 'autofocus',
30
+ 'autoplay',
31
+ 'checked',
32
+ 'controls',
33
+ 'default',
34
+ 'defer',
35
+ 'disabled',
36
+ 'formNoValidate',
37
+ 'hidden',
38
+ 'inert',
39
+ 'isMap',
40
+ 'loop',
41
+ 'multiple',
42
+ 'muted',
43
+ 'noModule',
44
+ 'noValidate',
45
+ 'open',
46
+ 'playsInline',
47
+ 'readOnly',
48
+ 'required',
49
+ 'reversed',
50
+ 'selected',
51
+ ]);
52
+
26
53
  /** @type {import('eslint').Rule.RuleModule} */
27
54
  const preferStaticBooleanProperties = {
28
55
  meta: {
@@ -55,6 +82,9 @@ const preferStaticBooleanProperties = {
55
82
  // structural directives (*foo has no keySpan details)
56
83
  const isBindingProperty = keySpan?.details && !keySpan.details.includes('@') && !keySpan.details.includes('.');
57
84
  if (!isBindingProperty) return;
85
+ if (name.startsWith('ng') || NATIVE_BOOLEAN_PROPERTIES.has(name)) {
86
+ return;
87
+ }
58
88
 
59
89
  // Only literal `true` / `false` expressions. `value.ast` is a
60
90
  // LiteralPrimitive for literals; the raw-source check guards against
@@ -21,13 +21,7 @@
21
21
  */
22
22
 
23
23
  /** Properties on window that report viewport dimensions. */
24
- const VIEWPORT_PROPERTIES = new Set([
25
- 'innerWidth',
26
- 'innerHeight',
27
- 'outerWidth',
28
- 'outerHeight',
29
- 'screen', // window.screen.width / window.screen.height
30
- ]);
24
+ const VIEWPORT_PROPERTIES = new Set(['innerWidth', 'innerHeight']);
31
25
 
32
26
  /** @type {import('eslint').Rule.RuleModule} */
33
27
  const preferViewportSize = {
@@ -2,17 +2,12 @@
2
2
  'use strict';
3
3
 
4
4
  /**
5
- * Checks the two ends of the same wire: that a `<form>` handles its own submission, and that a
6
- * submit control reaches a form at all.
5
+ * Checks that a `<form>` handles its own submission.
7
6
  *
8
7
  * <form (ngSubmit)="save()"> ✔
9
8
  * <form [etForm]="form"> ✔ signal forms - the directive submits the field tree
10
9
  * <form> ✘ pressing Enter in a field does nothing, or reloads the page
11
10
  *
12
- * <form …><button type="submit"> ✔
13
- * <button type="submit" form="edit"> ✔ associated by id, outside the form's subtree
14
- * <button type="submit"> ✘ submits nothing
15
- *
16
11
  * A form declaring native submission (`action`, `ngNoForm`, `method="dialog"`) is left alone - it is
17
12
  * handled by the platform rather than by a handler.
18
13
  */
@@ -35,23 +30,17 @@ const handlesSubmit = (node) =>
35
30
  const submitsNatively = (node) =>
36
31
  hasBinding(node, 'action') || hasBinding(node, 'ngNoForm') || attribute(node, 'method')?.value === 'dialog';
37
32
 
38
- /** @param {any} node */
39
- const isSubmitControl = (node) =>
40
- (node.name === 'button' || node.name === 'input') && attribute(node, 'type')?.value === 'submit';
41
-
42
33
  /** @type {import('eslint').Rule.RuleModule} */
43
34
  const requireFormSubmit = {
44
35
  meta: {
45
36
  type: 'problem',
46
37
  docs: {
47
- description: 'Require a `<form>` to handle its own submission, and a submit control to reach a form.',
38
+ description: 'Require a `<form>` to handle its own submission.',
48
39
  },
49
40
  schema: [],
50
41
  messages: {
51
42
  missingSubmitHandler:
52
43
  'This `<form>` handles no submission - pressing Enter in a field either does nothing or reloads the page. Bind `[etForm]` (signal forms), `(ngSubmit)` (reactive forms) or `(submit)`, or use a plain element if this is not a form.',
53
- submitOutsideForm:
54
- 'A `type="submit"` control outside a `<form>` submits nothing. Put it inside the form, or associate it with one by id: `form="the-form-id"`.',
55
44
  },
56
45
  },
57
46
  create(context) {
@@ -59,8 +48,6 @@ const requireFormSubmit = {
59
48
 
60
49
  if (!parserServices?.convertNodeSourceSpanToLoc) return {};
61
50
 
62
- let formDepth = 0;
63
-
64
51
  /** @param {any} node */
65
52
  const report = (node, messageId) =>
66
53
  context.report({
@@ -72,21 +59,8 @@ const requireFormSubmit = {
72
59
  /** @param {any} node */
73
60
  Element(node) {
74
61
  if (node.name === 'form') {
75
- formDepth++;
76
-
77
62
  if (!handlesSubmit(node) && !submitsNatively(node)) report(node, 'missingSubmitHandler');
78
-
79
- return;
80
63
  }
81
-
82
- if (formDepth > 0 || !isSubmitControl(node) || hasBinding(node, 'form')) return;
83
-
84
- report(node, 'submitOutsideForm');
85
- },
86
-
87
- /** @param {any} node */
88
- 'Element:exit'(node) {
89
- if (node.name === 'form') formDepth--;
90
64
  },
91
65
  };
92
66
  },
@@ -87,11 +87,25 @@ const hasTrailingComma = (sourceCode, property) => {
87
87
  /**
88
88
  * @param {import('eslint').SourceCode} sourceCode
89
89
  */
90
- const findAngularCoreImport = (sourceCode) =>
91
- sourceCode.ast.body.find(
90
+ const findAngularCoreImport = (sourceCode) => {
91
+ const imports = sourceCode.ast.body.filter(
92
92
  (node) =>
93
93
  node.type === 'ImportDeclaration' && node.source.type === 'Literal' && node.source.value === '@angular/core',
94
- ) ?? null;
94
+ );
95
+
96
+ return (
97
+ imports.find((node) =>
98
+ node.specifiers.some(
99
+ (specifier) =>
100
+ specifier.type === 'ImportSpecifier' &&
101
+ specifier.imported.type === 'Identifier' &&
102
+ specifier.imported.name === 'ChangeDetectionStrategy',
103
+ ),
104
+ ) ??
105
+ imports.find((node) => node.importKind !== 'type') ??
106
+ null
107
+ );
108
+ };
95
109
 
96
110
  /**
97
111
  * @param {import('eslint').SourceCode} sourceCode
@@ -133,11 +147,18 @@ const buildMissingAngularCoreImportFix = (sourceCode) => {
133
147
  const importDeclarations = sourceCode.ast.body.filter((node) => node.type === 'ImportDeclaration');
134
148
  const lastImport = importDeclarations[importDeclarations.length - 1] ?? null;
135
149
 
136
- return lastImport
137
- ? (fixer) => fixer.insertTextAfter(lastImport, `\nimport { ChangeDetectionStrategy } from '@angular/core';`)
150
+ if (lastImport) {
151
+ return (fixer) => fixer.insertTextAfter(lastImport, `\nimport { ChangeDetectionStrategy } from '@angular/core';`);
152
+ }
153
+
154
+ const firstNode = sourceCode.ast.body[0];
155
+ const lastComment = firstNode ? sourceCode.getCommentsBefore(firstNode).at(-1) : null;
156
+
157
+ return lastComment
158
+ ? (fixer) => fixer.insertTextAfter(lastComment, `\nimport { ChangeDetectionStrategy } from '@angular/core';\n`)
138
159
  : (fixer) =>
139
160
  fixer.replaceTextRange(
140
- [0, sourceCode.ast.range[0]],
161
+ [0, firstNode?.range[0] ?? 0],
141
162
  `import { ChangeDetectionStrategy } from '@angular/core';\n\n`,
142
163
  );
143
164
  };
@@ -146,6 +167,8 @@ const buildMissingAngularCoreImportFix = (sourceCode) => {
146
167
  * @param {any} metadata
147
168
  */
148
169
  const getChangeDetectionInsertionTarget = (metadata) => {
170
+ if (metadata.properties.some((property) => property.type === 'SpreadElement')) return null;
171
+
149
172
  const changeDetectionIndex = COMPONENT_ORDER.indexOf('changeDetection');
150
173
 
151
174
  return (
@@ -165,7 +188,7 @@ const getChangeDetectionInsertionTarget = (metadata) => {
165
188
  */
166
189
  const buildMetadataFix = (sourceCode, metadata) => {
167
190
  const changeDetectionText = 'changeDetection: ChangeDetectionStrategy.OnPush';
168
- const properties = metadata.properties.filter((property) => property.type === 'Property');
191
+ const properties = metadata.properties;
169
192
  const insertionTarget = getChangeDetectionInsertionTarget(metadata);
170
193
  const isMultiline = Boolean(metadata.loc && metadata.loc.start.line !== metadata.loc.end.line);
171
194
 
@@ -282,10 +305,7 @@ const requireOnPushChangeDetection = {
282
305
  if (!metadata || metadata.type !== 'ObjectExpression') return;
283
306
 
284
307
  const changeDetectionProp = metadata.properties.find(
285
- (property) =>
286
- property.type === 'Property' &&
287
- property.key.type === 'Identifier' &&
288
- property.key.name === 'changeDetection',
308
+ (property) => property.type === 'Property' && getPropertyName(property.key) === 'changeDetection',
289
309
  );
290
310
 
291
311
  if (!changeDetectionProp) {
@@ -44,11 +44,25 @@ const hasTrailingComma = (sourceCode, property) => {
44
44
  /**
45
45
  * @param {import('eslint').SourceCode} sourceCode
46
46
  */
47
- const findAngularCoreImport = (sourceCode) =>
48
- sourceCode.ast.body.find(
47
+ const findAngularCoreImport = (sourceCode) => {
48
+ const imports = sourceCode.ast.body.filter(
49
49
  (node) =>
50
50
  node.type === 'ImportDeclaration' && node.source.type === 'Literal' && node.source.value === '@angular/core',
51
- ) ?? null;
51
+ );
52
+
53
+ return (
54
+ imports.find((node) =>
55
+ node.specifiers.some(
56
+ (specifier) =>
57
+ specifier.type === 'ImportSpecifier' &&
58
+ specifier.imported.type === 'Identifier' &&
59
+ specifier.imported.name === 'ViewEncapsulation',
60
+ ),
61
+ ) ??
62
+ imports.find((node) => node.importKind !== 'type') ??
63
+ null
64
+ );
65
+ };
52
66
 
53
67
  /**
54
68
  * @param {import('eslint').SourceCode} sourceCode
@@ -90,16 +104,25 @@ const buildMissingAngularCoreImportFix = (sourceCode) => {
90
104
  const importDeclarations = sourceCode.ast.body.filter((node) => node.type === 'ImportDeclaration');
91
105
  const lastImport = importDeclarations[importDeclarations.length - 1] ?? null;
92
106
 
93
- return lastImport
94
- ? (fixer) => fixer.insertTextAfter(lastImport, `\nimport { ViewEncapsulation } from '@angular/core';`)
107
+ if (lastImport) {
108
+ return (fixer) => fixer.insertTextAfter(lastImport, `\nimport { ViewEncapsulation } from '@angular/core';`);
109
+ }
110
+
111
+ const firstNode = sourceCode.ast.body[0];
112
+ const lastComment = firstNode ? sourceCode.getCommentsBefore(firstNode).at(-1) : null;
113
+
114
+ return lastComment
115
+ ? (fixer) => fixer.insertTextAfter(lastComment, `\nimport { ViewEncapsulation } from '@angular/core';\n`)
95
116
  : (fixer) =>
96
- fixer.replaceTextRange([0, sourceCode.ast.range[0]], `import { ViewEncapsulation } from '@angular/core';\n\n`);
117
+ fixer.replaceTextRange([0, firstNode?.range[0] ?? 0], `import { ViewEncapsulation } from '@angular/core';\n\n`);
97
118
  };
98
119
 
99
120
  /**
100
121
  * @param {any} metadata
101
122
  */
102
123
  const getEncapsulationInsertionTarget = (metadata) => {
124
+ if (metadata.properties.some((property) => property.type === 'SpreadElement')) return null;
125
+
103
126
  const encapsulationIndex = COMPONENT_ORDER.indexOf('encapsulation');
104
127
 
105
128
  return (
@@ -119,7 +142,7 @@ const getEncapsulationInsertionTarget = (metadata) => {
119
142
  */
120
143
  const buildMetadataFix = (sourceCode, metadata) => {
121
144
  const encText = 'encapsulation: ViewEncapsulation.None';
122
- const properties = metadata.properties.filter((property) => property.type === 'Property');
145
+ const properties = metadata.properties;
123
146
  const insertionTarget = getEncapsulationInsertionTarget(metadata);
124
147
  const isMultiline = Boolean(metadata.loc && metadata.loc.start.line !== metadata.loc.end.line);
125
148
 
@@ -228,8 +251,7 @@ const requireViewEncapsulationNone = {
228
251
  if (!metadata || metadata.type !== 'ObjectExpression') return;
229
252
 
230
253
  const encProp = metadata.properties.find(
231
- (property) =>
232
- property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === 'encapsulation',
254
+ (property) => property.type === 'Property' && getPropertyName(property.key) === 'encapsulation',
233
255
  );
234
256
 
235
257
  if (!encProp) {
@@ -7,6 +7,7 @@ const {
7
7
  isReferencedFromTemplateOrHost,
8
8
  } = require('./internals/angular-member-visibility');
9
9
  const { getImplementedContractMemberNames } = require('./internals/implemented-contract-members');
10
+ const { buildAccessibilityFix } = require('./internals/member-accessibility-fix');
10
11
 
11
12
  /**
12
13
  * @param {any} node
@@ -17,24 +18,6 @@ const isInjectCall = (node) => {
17
18
  return node.value.callee.type === 'Identifier' && node.value.callee.name === 'inject';
18
19
  };
19
20
 
20
- /**
21
- * @param {any} node
22
- * @param {import('eslint').SourceCode} sourceCode
23
- */
24
- const buildPublicFix = (node, sourceCode) => {
25
- const keyToken = sourceCode.getFirstToken(node.key);
26
- if (!keyToken) return null;
27
-
28
- if (node.accessibility) {
29
- const firstToken = sourceCode.getFirstToken(node);
30
- if (!firstToken) return null;
31
-
32
- return (fixer) => fixer.replaceText(firstToken, 'public');
33
- }
34
-
35
- return (fixer) => fixer.insertTextBefore(keyToken, 'public ');
36
- };
37
-
38
21
  /**
39
22
  * @param {any} node
40
23
  * @param {import('eslint').SourceCode} sourceCode
@@ -127,7 +110,7 @@ const templateMemberAccessibility = {
127
110
  node,
128
111
  messageId: 'shouldBePublic',
129
112
  data: { name: memberName },
130
- fix: buildPublicFix(node, sourceCode),
113
+ fix: buildAccessibilityFix(node, sourceCode, 'public'),
131
114
  });
132
115
 
133
116
  return;
@@ -142,7 +125,7 @@ const templateMemberAccessibility = {
142
125
  node,
143
126
  messageId: 'shouldBeExplicit',
144
127
  data: { name: memberName },
145
- fix: buildPublicFix(node, sourceCode),
128
+ fix: buildAccessibilityFix(node, sourceCode, 'public'),
146
129
  });
147
130
 
148
131
  return;
@@ -184,7 +167,7 @@ const templateMemberAccessibility = {
184
167
  node,
185
168
  messageId: 'shouldBeExplicitPublic',
186
169
  data: { name: memberName },
187
- fix: buildPublicFix(node, sourceCode),
170
+ fix: buildAccessibilityFix(node, sourceCode, 'public'),
188
171
  });
189
172
  };
190
173