@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.
- package/CHANGELOG.md +17 -0
- package/package.json +3 -4
- package/src/configs/recommended.js +20 -10
- package/src/index.js +4 -1
- package/src/rules/class-constant-property.js +27 -0
- package/src/rules/guard-return-newline.js +5 -4
- package/src/rules/inject-member-accessibility.js +17 -21
- package/src/rules/internals/angular-io.js +19 -1
- package/src/rules/internals/angular-member-visibility.js +5 -5
- package/src/rules/internals/angular-metadata-fix.js +22 -0
- package/src/rules/internals/member-accessibility-fix.js +26 -0
- package/src/rules/no-angular-seo-services.js +7 -7
- package/src/rules/no-direct-dom-manipulation.js +17 -0
- package/src/rules/no-effect-cleanup-return.js +8 -6
- package/src/rules/no-empty-angular-metadata-arrays.js +3 -19
- package/src/rules/no-enum.js +115 -0
- package/src/rules/no-leading-underscore-class-member.js +7 -0
- package/src/rules/no-legacy-angular-decorators.js +8 -0
- package/src/rules/no-legacy-prepare-without-injector.js +42 -26
- package/src/rules/no-legacy-query-import.js +1 -1
- package/src/rules/no-locale-id.js +2 -14
- package/src/rules/no-member-alias.js +17 -0
- package/src/rules/no-native-observers.js +4 -1
- package/src/rules/no-pipe-logic.js +1 -0
- package/src/rules/no-readonly-signal.js +13 -5
- package/src/rules/no-redundant-internal.js +21 -2
- package/src/rules/no-redundant-on-push-change-detection.js +28 -38
- package/src/rules/no-standalone-flag.js +4 -19
- package/src/rules/no-trivial-return-type.js +1 -0
- package/src/rules/no-trivial-wrapper-method.js +3 -0
- package/src/rules/no-type-only-import.js +7 -2
- package/src/rules/no-typed-injected-element-ref.js +16 -9
- package/src/rules/no-unused-class-member.js +28 -5
- package/src/rules/prefer-match-media.js +2 -2
- package/src/rules/prefer-scroll-state.js +3 -67
- package/src/rules/prefer-static-boolean-properties.js +30 -0
- package/src/rules/prefer-viewport-size.js +1 -7
- package/src/rules/require-form-submit.js +2 -28
- package/src/rules/require-on-push-change-detection.js +31 -11
- package/src/rules/require-view-encapsulation-none.js +31 -9
- 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
|
-
* -
|
|
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)'
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
)
|
|
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
|
-
|
|
137
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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
|
-
)
|
|
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
|
-
|
|
94
|
-
|
|
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,
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
170
|
+
fix: buildAccessibilityFix(node, sourceCode, 'public'),
|
|
188
171
|
});
|
|
189
172
|
};
|
|
190
173
|
|