@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,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,365 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ const SCREAMING_CASE_RE = /^[A-Z][A-Z0-9_]*$/;
5
+
6
+ /**
7
+ * @param {any} classNode
8
+ */
9
+ const hasPipeDecorator = (classNode) => {
10
+ const decorators = classNode.decorators ?? [];
11
+ return decorators.some((/** @type {any} */ dec) => {
12
+ const expr = dec.expression;
13
+ if (expr.type === 'CallExpression') {
14
+ return expr.callee.type === 'Identifier' && expr.callee.name === 'Pipe';
15
+ }
16
+ if (expr.type === 'Identifier') {
17
+ return expr.name === 'Pipe';
18
+ }
19
+ return false;
20
+ });
21
+ };
22
+
23
+ const REACTIVE_APIS = new Set([
24
+ 'signal',
25
+ 'computed',
26
+ 'linkedSignal',
27
+ 'input',
28
+ 'model',
29
+ 'output',
30
+ 'outputFromObservable',
31
+ 'viewChild',
32
+ 'viewChildren',
33
+ 'contentChild',
34
+ 'contentChildren',
35
+ 'inject',
36
+ 'effect',
37
+ 'afterNextRender',
38
+ 'afterRender',
39
+ 'toSignal',
40
+ 'toObservable',
41
+ ]);
42
+
43
+ /**
44
+ * @param {any} node
45
+ */
46
+ const getMemberName = (node) => {
47
+ if (!node || !node.key) return null;
48
+
49
+ if (node.key.type === 'Identifier' || node.key.type === 'PrivateIdentifier') {
50
+ return node.key.name;
51
+ }
52
+
53
+ if (node.key.type === 'Literal' && typeof node.key.value === 'string') {
54
+ return node.key.value;
55
+ }
56
+
57
+ return null;
58
+ };
59
+
60
+ /**
61
+ * @param {any} node
62
+ */
63
+ const getCallRootName = (node) => {
64
+ if (!node || node.type !== 'CallExpression') return null;
65
+
66
+ const { callee } = node;
67
+
68
+ if (callee.type === 'Identifier') {
69
+ return callee.name;
70
+ }
71
+
72
+ if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier') {
73
+ return callee.object.name;
74
+ }
75
+
76
+ return null;
77
+ };
78
+
79
+ /**
80
+ * @param {any} node
81
+ */
82
+ const isReactiveInitializer = (node) => {
83
+ const apiName = getCallRootName(node);
84
+ return apiName !== null && REACTIVE_APIS.has(apiName);
85
+ };
86
+
87
+ /**
88
+ * @param {any} node
89
+ */
90
+ const isThisReference = (node) => node && (node.type === 'ThisExpression' || node.type === 'Super');
91
+
92
+ /**
93
+ * @param {any} node
94
+ */
95
+ const isConstantExpression = (node) => {
96
+ if (!node) return false;
97
+
98
+ switch (node.type) {
99
+ case 'Literal':
100
+ case 'Identifier':
101
+ return true;
102
+ case 'TemplateLiteral':
103
+ return node.expressions.every((expression) => isConstantExpression(expression));
104
+ case 'UnaryExpression':
105
+ case 'UpdateExpression':
106
+ return isConstantExpression(node.argument);
107
+ case 'BinaryExpression':
108
+ case 'LogicalExpression':
109
+ return isConstantExpression(node.left) && isConstantExpression(node.right);
110
+ case 'ConditionalExpression':
111
+ return (
112
+ isConstantExpression(node.test) && isConstantExpression(node.consequent) && isConstantExpression(node.alternate)
113
+ );
114
+ case 'ArrayExpression':
115
+ return node.elements.every((element) => {
116
+ if (element === null) return true;
117
+
118
+ if (element.type === 'SpreadElement') {
119
+ return isConstantExpression(element.argument);
120
+ }
121
+
122
+ return isConstantExpression(element);
123
+ });
124
+ case 'ObjectExpression':
125
+ return node.properties.every((property) => {
126
+ if (property.type === 'SpreadElement') {
127
+ return isConstantExpression(property.argument);
128
+ }
129
+
130
+ if (property.type !== 'Property' || property.kind !== 'init') {
131
+ return false;
132
+ }
133
+
134
+ if (property.computed && !isConstantExpression(property.key)) {
135
+ return false;
136
+ }
137
+
138
+ return isConstantExpression(property.value);
139
+ });
140
+ case 'MemberExpression':
141
+ if (isThisReference(node.object)) {
142
+ return false;
143
+ }
144
+
145
+ if (!isConstantExpression(node.object)) {
146
+ return false;
147
+ }
148
+
149
+ return node.computed ? isConstantExpression(node.property) : true;
150
+ case 'TSAsExpression':
151
+ case 'TSTypeAssertion':
152
+ case 'TSNonNullExpression':
153
+ case 'ChainExpression':
154
+ return isConstantExpression(node.expression);
155
+ case 'ParenthesizedExpression':
156
+ return isConstantExpression(node.expression);
157
+ case 'SequenceExpression':
158
+ return node.expressions.every((expression) => isConstantExpression(expression));
159
+ default:
160
+ return false;
161
+ }
162
+ };
163
+
164
+ /**
165
+ * @param {any} node
166
+ */
167
+ const getWrittenMemberName = (node) => {
168
+ if (!node || node.type !== 'MemberExpression' || !isThisReference(node.object)) {
169
+ return null;
170
+ }
171
+
172
+ if (!node.computed && (node.property.type === 'Identifier' || node.property.type === 'PrivateIdentifier')) {
173
+ return node.property.name;
174
+ }
175
+
176
+ if (node.computed && node.property.type === 'Literal' && typeof node.property.value === 'string') {
177
+ return node.property.value;
178
+ }
179
+
180
+ return null;
181
+ };
182
+
183
+ /**
184
+ * @param {any} target
185
+ * @param {Set<string>} writtenMembers
186
+ */
187
+ const collectAssignedMemberNames = (target, writtenMembers) => {
188
+ if (!target) return;
189
+
190
+ if (target.type === 'MemberExpression') {
191
+ const memberName = getWrittenMemberName(target);
192
+
193
+ if (memberName !== null) {
194
+ writtenMembers.add(memberName);
195
+ }
196
+
197
+ return;
198
+ }
199
+
200
+ if (target.type === 'ArrayPattern') {
201
+ for (const element of target.elements) {
202
+ collectAssignedMemberNames(element, writtenMembers);
203
+ }
204
+
205
+ return;
206
+ }
207
+
208
+ if (target.type === 'ObjectPattern') {
209
+ for (const property of target.properties) {
210
+ if (property.type === 'Property') {
211
+ collectAssignedMemberNames(property.value, writtenMembers);
212
+ } else if (property.type === 'RestElement') {
213
+ collectAssignedMemberNames(property.argument, writtenMembers);
214
+ }
215
+ }
216
+
217
+ return;
218
+ }
219
+
220
+ if (target.type === 'AssignmentPattern') {
221
+ collectAssignedMemberNames(target.left, writtenMembers);
222
+
223
+ return;
224
+ }
225
+
226
+ if (target.type === 'RestElement') {
227
+ collectAssignedMemberNames(target.argument, writtenMembers);
228
+ }
229
+ };
230
+
231
+ /**
232
+ * @param {any} node
233
+ * @param {Set<string>} writtenMembers
234
+ */
235
+ const collectWrittenMembers = (node, writtenMembers) => {
236
+ if (!node || typeof node !== 'object') return;
237
+
238
+ if (Array.isArray(node)) {
239
+ for (const entry of node) {
240
+ collectWrittenMembers(entry, writtenMembers);
241
+ }
242
+
243
+ return;
244
+ }
245
+
246
+ if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
247
+ return;
248
+ }
249
+
250
+ if (node.type === 'AssignmentExpression') {
251
+ collectAssignedMemberNames(node.left, writtenMembers);
252
+ }
253
+
254
+ if (node.type === 'UpdateExpression') {
255
+ const memberName = getWrittenMemberName(node.argument);
256
+
257
+ if (memberName !== null) {
258
+ writtenMembers.add(memberName);
259
+ }
260
+ }
261
+
262
+ for (const [key, value] of Object.entries(node)) {
263
+ if (key === 'parent' || key === 'range' || key === 'loc') continue;
264
+ if (!value || typeof value !== 'object') continue;
265
+
266
+ collectWrittenMembers(value, writtenMembers);
267
+ }
268
+ };
269
+
270
+ /**
271
+ * @param {any} classBodyNode
272
+ */
273
+ const getWrittenMembers = (classBodyNode) => {
274
+ const writtenMembers = new Set();
275
+
276
+ for (const member of classBodyNode.body) {
277
+ collectWrittenMembers(member, writtenMembers);
278
+ }
279
+
280
+ return writtenMembers;
281
+ };
282
+
283
+ /**
284
+ * @param {any} node
285
+ */
286
+ const isConstantProperty = (node) => {
287
+ if (node.type !== 'PropertyDefinition' || node.static || node.computed || !node.value) {
288
+ return false;
289
+ }
290
+
291
+ if (isReactiveInitializer(node.value)) {
292
+ return false;
293
+ }
294
+
295
+ return isConstantExpression(node.value);
296
+ };
297
+
298
+ /** @type {import('eslint').Rule.RuleModule} */
299
+ const classConstantProperty = {
300
+ meta: {
301
+ type: 'suggestion',
302
+ fixable: 'code',
303
+ docs: {
304
+ description: 'Require true class constants to use readonly and SCREAMING_CASE.',
305
+ recommended: true,
306
+ },
307
+ schema: [],
308
+ messages: {
309
+ shouldBeReadonly: "'{{name}}' is a class constant and should be declared readonly.",
310
+ shouldUseScreamingCase: "Readonly class constant '{{name}}' should use SCREAMING_CASE.",
311
+ },
312
+ },
313
+ create(context) {
314
+ return {
315
+ ClassBody(node) {
316
+ const writtenMembers = getWrittenMembers(node);
317
+
318
+ for (const member of node.body) {
319
+ if (!isConstantProperty(member)) {
320
+ continue;
321
+ }
322
+
323
+ const memberName = getMemberName(member);
324
+
325
+ if (memberName === null || writtenMembers.has(memberName)) {
326
+ continue;
327
+ }
328
+
329
+ if (memberName === 'transform' && hasPipeDecorator(node.parent)) {
330
+ continue;
331
+ }
332
+
333
+ if (!member.readonly) {
334
+ context.report({
335
+ node: member,
336
+ messageId: 'shouldBeReadonly',
337
+ data: { name: memberName },
338
+ fix(fixer) {
339
+ const keyToken = context.sourceCode.getFirstToken(member.key);
340
+
341
+ if (!keyToken) {
342
+ return null;
343
+ }
344
+
345
+ return fixer.insertTextBefore(keyToken, 'readonly ');
346
+ },
347
+ });
348
+
349
+ continue;
350
+ }
351
+
352
+ if (!SCREAMING_CASE_RE.test(memberName)) {
353
+ context.report({
354
+ node: member.key,
355
+ messageId: 'shouldUseScreamingCase',
356
+ data: { name: memberName },
357
+ });
358
+ }
359
+ }
360
+ },
361
+ };
362
+ },
363
+ };
364
+
365
+ module.exports = classConstantProperty;