@ethlete/eslint-plugin 0.1.0-beta.5
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 +39 -0
- package/README.md +25 -0
- package/package.json +14 -0
- package/src/configs/recommended.js +403 -0
- package/src/index.js +136 -0
- package/src/rules/angular-decorator-property-order.js +205 -0
- package/src/rules/class-constant-property.js +344 -0
- package/src/rules/class-member-order.js +431 -0
- package/src/rules/enforce-routing-view-naming.js +84 -0
- package/src/rules/guard-return-newline.js +81 -0
- package/src/rules/inject-member-accessibility.js +93 -0
- package/src/rules/internals/angular-member-visibility.js +233 -0
- package/src/rules/internals/implemented-contract-members.js +294 -0
- package/src/rules/no-angular-router-api.js +158 -0
- package/src/rules/no-angular-seo-services.js +82 -0
- package/src/rules/no-direct-dom-manipulation.js +160 -0
- package/src/rules/no-document-cookie.js +55 -0
- package/src/rules/no-dom-query.js +81 -0
- package/src/rules/no-empty-angular-metadata-arrays.js +104 -0
- package/src/rules/no-empty-newlines-between-imports.js +45 -0
- package/src/rules/no-inject-chain.js +44 -0
- package/src/rules/no-leading-underscore-class-member.js +120 -0
- package/src/rules/no-legacy-angular-decorators.js +405 -0
- package/src/rules/no-locale-id.js +57 -0
- package/src/rules/no-member-alias.js +151 -0
- package/src/rules/no-native-observers.js +102 -0
- package/src/rules/no-pipe-logic.js +76 -0
- package/src/rules/no-public-property.js +85 -0
- package/src/rules/no-readonly-signal.js +119 -0
- package/src/rules/no-redundant-internal.js +94 -0
- package/src/rules/no-rxjs-in-effect.js +88 -0
- package/src/rules/no-screaming-case-local.js +113 -0
- package/src/rules/no-standalone-flag.js +95 -0
- package/src/rules/no-subscribe-in-pipe.js +89 -0
- package/src/rules/no-subscribe-with-body.js +95 -0
- package/src/rules/no-trivial-return-type.js +104 -0
- package/src/rules/no-trivial-wrapper-method.js +120 -0
- package/src/rules/no-type-only-import.js +79 -0
- package/src/rules/no-typed-injected-element-ref.js +88 -0
- package/src/rules/no-unused-class-member.js +209 -0
- package/src/rules/no-window-location.js +109 -0
- package/src/rules/prefer-clone-equal.js +112 -0
- package/src/rules/prefer-concise-angular-host-directives.js +225 -0
- package/src/rules/prefer-concise-angular-style-metadata.js +108 -0
- package/src/rules/prefer-element-dimensions.js +128 -0
- package/src/rules/prefer-linked-signal.js +124 -0
- package/src/rules/prefer-match-media.js +101 -0
- package/src/rules/prefer-rxjs-timer.js +82 -0
- package/src/rules/prefer-scroll-state.js +158 -0
- package/src/rules/prefer-viewport-size.js +75 -0
- package/src/rules/require-dollar-suffix.js +123 -0
- package/src/rules/require-on-push-change-detection.js +271 -0
- package/src/rules/require-view-encapsulation-none.js +264 -0
- package/src/rules/template-member-accessibility.js +190 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/** @typedef {'Component' | 'Directive'} TDecoratorName */
|
|
5
|
+
/** @typedef {{ order: string[]; orderText: string }} TDecoratorConfig */
|
|
6
|
+
/** @typedef {{ range: [number, number] }} TNodeWithRange */
|
|
7
|
+
/** @typedef {import('estree').Property & TNodeWithRange} TPropertyNode */
|
|
8
|
+
/** @typedef {import('estree').SpreadElement & TNodeWithRange} TSpreadElementNode */
|
|
9
|
+
/** @typedef {import('estree').ObjectExpression & TNodeWithRange & { properties: Array<TPropertyNode | TSpreadElementNode> }} TObjectExpressionNode */
|
|
10
|
+
/** @typedef {{ hasComma: boolean; originalIndex: number; orderIndex: number; property: TPropertyNode; segmentText: string }} TPropertyEntry */
|
|
11
|
+
|
|
12
|
+
/** @type {Record<TDecoratorName, TDecoratorConfig>} */
|
|
13
|
+
const DECORATOR_CONFIG = {
|
|
14
|
+
Component: {
|
|
15
|
+
order: [
|
|
16
|
+
'selector',
|
|
17
|
+
'template',
|
|
18
|
+
'styleUrl',
|
|
19
|
+
'encapsulation',
|
|
20
|
+
'changeDetection',
|
|
21
|
+
'imports',
|
|
22
|
+
'providers',
|
|
23
|
+
'viewProviders',
|
|
24
|
+
'animations',
|
|
25
|
+
'hostDirectives',
|
|
26
|
+
'host',
|
|
27
|
+
'styles',
|
|
28
|
+
'inputs',
|
|
29
|
+
'outputs',
|
|
30
|
+
'queries',
|
|
31
|
+
'preserveWhitespaces',
|
|
32
|
+
'schemas',
|
|
33
|
+
'jit',
|
|
34
|
+
'exportAs',
|
|
35
|
+
],
|
|
36
|
+
orderText:
|
|
37
|
+
'selector, template/templateUrl, styleUrl/styleUrls, encapsulation, changeDetection, imports, providers, viewProviders, animations, hostDirectives, host, styles, inputs, outputs, queries, preserveWhitespaces, schemas, jit, exportAs',
|
|
38
|
+
},
|
|
39
|
+
Directive: {
|
|
40
|
+
order: ['selector', 'exportAs', 'providers', 'inputs', 'outputs', 'queries', 'hostDirectives', 'host', 'jit'],
|
|
41
|
+
orderText: 'selector, exportAs, providers, inputs, outputs, queries, hostDirectives, host, jit',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {import('estree').Property['key']} key
|
|
47
|
+
*/
|
|
48
|
+
const getPropertyName = (key) => {
|
|
49
|
+
if (key.type === 'Identifier') return key.name;
|
|
50
|
+
if (key.type === 'Literal' && typeof key.value === 'string') return key.value;
|
|
51
|
+
return null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {TDecoratorName} decoratorName
|
|
56
|
+
* @param {string | null} propertyName
|
|
57
|
+
*/
|
|
58
|
+
const getOrderKey = (decoratorName, propertyName) => {
|
|
59
|
+
if (propertyName === null) return null;
|
|
60
|
+
|
|
61
|
+
if (decoratorName === 'Component') {
|
|
62
|
+
if (propertyName === 'template' || propertyName === 'templateUrl') return 'template';
|
|
63
|
+
if (propertyName === 'styleUrl' || propertyName === 'styleUrls') return 'styleUrl';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return propertyName;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {import('eslint').Rule.Node} node
|
|
71
|
+
* @returns {TDecoratorName | null}
|
|
72
|
+
*/
|
|
73
|
+
const getDecoratorName = (node) => {
|
|
74
|
+
const decorator = /** @type {any} */ (node);
|
|
75
|
+
if (decorator.type !== 'Decorator') return null;
|
|
76
|
+
|
|
77
|
+
const expression = decorator.expression;
|
|
78
|
+
if (expression.type === 'CallExpression') {
|
|
79
|
+
return expression.callee.type === 'Identifier' ? expression.callee.name : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return expression.type === 'Identifier' ? expression.name : null;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
86
|
+
const angularDecoratorPropertyOrder = {
|
|
87
|
+
meta: {
|
|
88
|
+
type: 'layout',
|
|
89
|
+
docs: {
|
|
90
|
+
description: 'Require a consistent property order in Angular @Component and @Directive metadata objects.',
|
|
91
|
+
},
|
|
92
|
+
fixable: 'code',
|
|
93
|
+
messages: {
|
|
94
|
+
outOfOrder: '{{decorator}} metadata properties should be ordered as: {{order}}.',
|
|
95
|
+
},
|
|
96
|
+
schema: [],
|
|
97
|
+
},
|
|
98
|
+
create(context) {
|
|
99
|
+
const sourceCode = context.sourceCode;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
/** @param {import('eslint').Rule.Node} node */
|
|
103
|
+
Decorator(node) {
|
|
104
|
+
const decoratorName = getDecoratorName(node);
|
|
105
|
+
if (decoratorName !== 'Component' && decoratorName !== 'Directive') return;
|
|
106
|
+
|
|
107
|
+
const decorator = /** @type {any} */ (node);
|
|
108
|
+
const expression = decorator.expression;
|
|
109
|
+
if (expression.type !== 'CallExpression' || expression.arguments.length === 0) return;
|
|
110
|
+
|
|
111
|
+
const metadata = /** @type {TObjectExpressionNode | undefined} */ (expression.arguments[0]);
|
|
112
|
+
if (!metadata || metadata.type !== 'ObjectExpression' || metadata.properties.length < 2) return;
|
|
113
|
+
|
|
114
|
+
if (
|
|
115
|
+
metadata.properties.some(
|
|
116
|
+
/** @param {TPropertyNode | TSpreadElementNode} property */ (property) => property.type !== 'Property',
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
return;
|
|
120
|
+
|
|
121
|
+
const config = DECORATOR_CONFIG[decoratorName];
|
|
122
|
+
const orderIndexMap = new Map(
|
|
123
|
+
config.order.map(/** @param {string} value @param {number} index */ (value, index) => [value, index]),
|
|
124
|
+
);
|
|
125
|
+
const openingBrace = sourceCode.getFirstToken(metadata);
|
|
126
|
+
const closingBrace = sourceCode.getLastToken(metadata);
|
|
127
|
+
|
|
128
|
+
if (!openingBrace || !closingBrace || openingBrace.value !== '{' || closingBrace.value !== '}') return;
|
|
129
|
+
|
|
130
|
+
let segmentStart = openingBrace.range[1];
|
|
131
|
+
|
|
132
|
+
const entries = metadata.properties.map(
|
|
133
|
+
/** @param {TPropertyNode | TSpreadElementNode} property @param {number} originalIndex @returns {TPropertyEntry} */
|
|
134
|
+
(property, originalIndex) => {
|
|
135
|
+
const propertyNode = /** @type {TPropertyNode} */ (property);
|
|
136
|
+
const propertyName = getPropertyName(propertyNode.key);
|
|
137
|
+
const orderKey = getOrderKey(decoratorName, propertyName);
|
|
138
|
+
const orderIndex =
|
|
139
|
+
orderKey === null ? config.order.length : (orderIndexMap.get(orderKey) ?? config.order.length);
|
|
140
|
+
const tokenAfter = sourceCode.getTokenAfter(propertyNode);
|
|
141
|
+
const hasComma = Boolean(tokenAfter && tokenAfter.type === 'Punctuator' && tokenAfter.value === ',');
|
|
142
|
+
const segmentEnd = hasComma && tokenAfter ? tokenAfter.range[0] : propertyNode.range[1];
|
|
143
|
+
const segmentText = sourceCode.text.slice(segmentStart, segmentEnd);
|
|
144
|
+
|
|
145
|
+
if (hasComma && tokenAfter) {
|
|
146
|
+
segmentStart = tokenAfter.range[1];
|
|
147
|
+
} else {
|
|
148
|
+
segmentStart = propertyNode.range[1];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
hasComma,
|
|
153
|
+
originalIndex,
|
|
154
|
+
orderIndex,
|
|
155
|
+
property: propertyNode,
|
|
156
|
+
segmentText,
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const trailingComma = entries[entries.length - 1].hasComma;
|
|
162
|
+
const suffix = sourceCode.text.slice(segmentStart, closingBrace.range[0]);
|
|
163
|
+
const sortedEntries = [...entries].sort(
|
|
164
|
+
/** @param {TPropertyEntry} left @param {TPropertyEntry} right */ (left, right) => {
|
|
165
|
+
if (left.orderIndex !== right.orderIndex) return left.orderIndex - right.orderIndex;
|
|
166
|
+
return left.originalIndex - right.originalIndex;
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const isSorted = entries.every(
|
|
171
|
+
/** @param {TPropertyEntry} entry @param {number} index */ (entry, index) =>
|
|
172
|
+
entry.property === sortedEntries[index].property,
|
|
173
|
+
);
|
|
174
|
+
if (isSorted) return;
|
|
175
|
+
|
|
176
|
+
const firstMismatch = entries.find(
|
|
177
|
+
/** @param {TPropertyEntry} entry @param {number} index */ (entry, index) =>
|
|
178
|
+
entry.property !== sortedEntries[index].property,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
context.report({
|
|
182
|
+
node: firstMismatch ? firstMismatch.property : metadata,
|
|
183
|
+
messageId: 'outOfOrder',
|
|
184
|
+
data: {
|
|
185
|
+
decorator: `@${decoratorName}`,
|
|
186
|
+
order: config.orderText,
|
|
187
|
+
},
|
|
188
|
+
fix(fixer) {
|
|
189
|
+
const reorderedBody =
|
|
190
|
+
sortedEntries
|
|
191
|
+
.map(
|
|
192
|
+
(entry, index) =>
|
|
193
|
+
`${entry.segmentText}${index < sortedEntries.length - 1 || trailingComma ? ',' : ''}`,
|
|
194
|
+
)
|
|
195
|
+
.join('') + suffix;
|
|
196
|
+
|
|
197
|
+
return fixer.replaceTextRange([openingBrace.range[1], closingBrace.range[0]], reorderedBody);
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
module.exports = angularDecoratorPropertyOrder;
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const SCREAMING_CASE_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
5
|
+
|
|
6
|
+
const REACTIVE_APIS = new Set([
|
|
7
|
+
'signal',
|
|
8
|
+
'computed',
|
|
9
|
+
'linkedSignal',
|
|
10
|
+
'input',
|
|
11
|
+
'model',
|
|
12
|
+
'output',
|
|
13
|
+
'outputFromObservable',
|
|
14
|
+
'viewChild',
|
|
15
|
+
'viewChildren',
|
|
16
|
+
'contentChild',
|
|
17
|
+
'contentChildren',
|
|
18
|
+
'inject',
|
|
19
|
+
'effect',
|
|
20
|
+
'afterNextRender',
|
|
21
|
+
'afterRender',
|
|
22
|
+
'toSignal',
|
|
23
|
+
'toObservable',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {any} node
|
|
28
|
+
*/
|
|
29
|
+
const getMemberName = (node) => {
|
|
30
|
+
if (!node || !node.key) return null;
|
|
31
|
+
|
|
32
|
+
if (node.key.type === 'Identifier' || node.key.type === 'PrivateIdentifier') {
|
|
33
|
+
return node.key.name;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (node.key.type === 'Literal' && typeof node.key.value === 'string') {
|
|
37
|
+
return node.key.value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {any} node
|
|
45
|
+
*/
|
|
46
|
+
const getCallRootName = (node) => {
|
|
47
|
+
if (!node || node.type !== 'CallExpression') return null;
|
|
48
|
+
|
|
49
|
+
const { callee } = node;
|
|
50
|
+
|
|
51
|
+
if (callee.type === 'Identifier') {
|
|
52
|
+
return callee.name;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier') {
|
|
56
|
+
return callee.object.name;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {any} node
|
|
64
|
+
*/
|
|
65
|
+
const isReactiveInitializer = (node) => {
|
|
66
|
+
const apiName = getCallRootName(node);
|
|
67
|
+
return apiName !== null && REACTIVE_APIS.has(apiName);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {any} node
|
|
72
|
+
*/
|
|
73
|
+
const isThisReference = (node) => node && (node.type === 'ThisExpression' || node.type === 'Super');
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {any} node
|
|
77
|
+
*/
|
|
78
|
+
const isConstantExpression = (node) => {
|
|
79
|
+
if (!node) return false;
|
|
80
|
+
|
|
81
|
+
switch (node.type) {
|
|
82
|
+
case 'Literal':
|
|
83
|
+
case 'Identifier':
|
|
84
|
+
return true;
|
|
85
|
+
case 'TemplateLiteral':
|
|
86
|
+
return node.expressions.every((expression) => isConstantExpression(expression));
|
|
87
|
+
case 'UnaryExpression':
|
|
88
|
+
case 'UpdateExpression':
|
|
89
|
+
return isConstantExpression(node.argument);
|
|
90
|
+
case 'BinaryExpression':
|
|
91
|
+
case 'LogicalExpression':
|
|
92
|
+
return isConstantExpression(node.left) && isConstantExpression(node.right);
|
|
93
|
+
case 'ConditionalExpression':
|
|
94
|
+
return (
|
|
95
|
+
isConstantExpression(node.test) && isConstantExpression(node.consequent) && isConstantExpression(node.alternate)
|
|
96
|
+
);
|
|
97
|
+
case 'ArrayExpression':
|
|
98
|
+
return node.elements.every((element) => {
|
|
99
|
+
if (element === null) return true;
|
|
100
|
+
|
|
101
|
+
if (element.type === 'SpreadElement') {
|
|
102
|
+
return isConstantExpression(element.argument);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return isConstantExpression(element);
|
|
106
|
+
});
|
|
107
|
+
case 'ObjectExpression':
|
|
108
|
+
return node.properties.every((property) => {
|
|
109
|
+
if (property.type === 'SpreadElement') {
|
|
110
|
+
return isConstantExpression(property.argument);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (property.type !== 'Property' || property.kind !== 'init') {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (property.computed && !isConstantExpression(property.key)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return isConstantExpression(property.value);
|
|
122
|
+
});
|
|
123
|
+
case 'MemberExpression':
|
|
124
|
+
if (isThisReference(node.object)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!isConstantExpression(node.object)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return node.computed ? isConstantExpression(node.property) : true;
|
|
133
|
+
case 'TSAsExpression':
|
|
134
|
+
case 'TSTypeAssertion':
|
|
135
|
+
case 'TSNonNullExpression':
|
|
136
|
+
case 'ChainExpression':
|
|
137
|
+
return isConstantExpression(node.expression);
|
|
138
|
+
case 'ParenthesizedExpression':
|
|
139
|
+
return isConstantExpression(node.expression);
|
|
140
|
+
case 'SequenceExpression':
|
|
141
|
+
return node.expressions.every((expression) => isConstantExpression(expression));
|
|
142
|
+
default:
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {any} node
|
|
149
|
+
*/
|
|
150
|
+
const getWrittenMemberName = (node) => {
|
|
151
|
+
if (!node || node.type !== 'MemberExpression' || !isThisReference(node.object)) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!node.computed && (node.property.type === 'Identifier' || node.property.type === 'PrivateIdentifier')) {
|
|
156
|
+
return node.property.name;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (node.computed && node.property.type === 'Literal' && typeof node.property.value === 'string') {
|
|
160
|
+
return node.property.value;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return null;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param {any} target
|
|
168
|
+
* @param {Set<string>} writtenMembers
|
|
169
|
+
*/
|
|
170
|
+
const collectAssignedMemberNames = (target, writtenMembers) => {
|
|
171
|
+
if (!target) return;
|
|
172
|
+
|
|
173
|
+
if (target.type === 'MemberExpression') {
|
|
174
|
+
const memberName = getWrittenMemberName(target);
|
|
175
|
+
|
|
176
|
+
if (memberName !== null) {
|
|
177
|
+
writtenMembers.add(memberName);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (target.type === 'ArrayPattern') {
|
|
184
|
+
for (const element of target.elements) {
|
|
185
|
+
collectAssignedMemberNames(element, writtenMembers);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (target.type === 'ObjectPattern') {
|
|
192
|
+
for (const property of target.properties) {
|
|
193
|
+
if (property.type === 'Property') {
|
|
194
|
+
collectAssignedMemberNames(property.value, writtenMembers);
|
|
195
|
+
} else if (property.type === 'RestElement') {
|
|
196
|
+
collectAssignedMemberNames(property.argument, writtenMembers);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (target.type === 'AssignmentPattern') {
|
|
204
|
+
collectAssignedMemberNames(target.left, writtenMembers);
|
|
205
|
+
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (target.type === 'RestElement') {
|
|
210
|
+
collectAssignedMemberNames(target.argument, writtenMembers);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {any} node
|
|
216
|
+
* @param {Set<string>} writtenMembers
|
|
217
|
+
*/
|
|
218
|
+
const collectWrittenMembers = (node, writtenMembers) => {
|
|
219
|
+
if (!node || typeof node !== 'object') return;
|
|
220
|
+
|
|
221
|
+
if (Array.isArray(node)) {
|
|
222
|
+
for (const entry of node) {
|
|
223
|
+
collectWrittenMembers(entry, writtenMembers);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (node.type === 'AssignmentExpression') {
|
|
234
|
+
collectAssignedMemberNames(node.left, writtenMembers);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (node.type === 'UpdateExpression') {
|
|
238
|
+
const memberName = getWrittenMemberName(node.argument);
|
|
239
|
+
|
|
240
|
+
if (memberName !== null) {
|
|
241
|
+
writtenMembers.add(memberName);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
for (const [key, value] of Object.entries(node)) {
|
|
246
|
+
if (key === 'parent' || key === 'range' || key === 'loc') continue;
|
|
247
|
+
if (!value || typeof value !== 'object') continue;
|
|
248
|
+
|
|
249
|
+
collectWrittenMembers(value, writtenMembers);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param {any} classBodyNode
|
|
255
|
+
*/
|
|
256
|
+
const getWrittenMembers = (classBodyNode) => {
|
|
257
|
+
const writtenMembers = new Set();
|
|
258
|
+
|
|
259
|
+
for (const member of classBodyNode.body) {
|
|
260
|
+
collectWrittenMembers(member, writtenMembers);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return writtenMembers;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @param {any} node
|
|
268
|
+
*/
|
|
269
|
+
const isConstantProperty = (node) => {
|
|
270
|
+
if (node.type !== 'PropertyDefinition' || node.static || node.computed || !node.value) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (isReactiveInitializer(node.value)) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return isConstantExpression(node.value);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
282
|
+
const classConstantProperty = {
|
|
283
|
+
meta: {
|
|
284
|
+
type: 'suggestion',
|
|
285
|
+
fixable: 'code',
|
|
286
|
+
docs: {
|
|
287
|
+
description: 'Require true class constants to use readonly and SCREAMING_CASE.',
|
|
288
|
+
recommended: true,
|
|
289
|
+
},
|
|
290
|
+
schema: [],
|
|
291
|
+
messages: {
|
|
292
|
+
shouldBeReadonly: "'{{name}}' is a class constant and should be declared readonly.",
|
|
293
|
+
shouldUseScreamingCase: "Readonly class constant '{{name}}' should use SCREAMING_CASE.",
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
create(context) {
|
|
297
|
+
return {
|
|
298
|
+
ClassBody(node) {
|
|
299
|
+
const writtenMembers = getWrittenMembers(node);
|
|
300
|
+
|
|
301
|
+
for (const member of node.body) {
|
|
302
|
+
if (!isConstantProperty(member)) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const memberName = getMemberName(member);
|
|
307
|
+
|
|
308
|
+
if (memberName === null || writtenMembers.has(memberName)) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!member.readonly) {
|
|
313
|
+
context.report({
|
|
314
|
+
node: member,
|
|
315
|
+
messageId: 'shouldBeReadonly',
|
|
316
|
+
data: { name: memberName },
|
|
317
|
+
fix(fixer) {
|
|
318
|
+
const keyToken = context.sourceCode.getFirstToken(member.key);
|
|
319
|
+
|
|
320
|
+
if (!keyToken) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return fixer.insertTextBefore(keyToken, 'readonly ');
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!SCREAMING_CASE_RE.test(memberName)) {
|
|
332
|
+
context.report({
|
|
333
|
+
node: member.key,
|
|
334
|
+
messageId: 'shouldUseScreamingCase',
|
|
335
|
+
data: { name: memberName },
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
module.exports = classConstantProperty;
|