@ethlete/eslint-plugin 0.1.0-next.1 → 0.1.0-next.3
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 +12 -0
- package/package.json +3 -2
- package/src/configs/recommended.js +51 -10
- package/src/index.js +22 -2
- package/src/rules/angular-decorator-property-order.js +3 -15
- package/src/rules/class-constant-property.js +344 -0
- package/src/rules/class-member-order.js +431 -0
- package/src/rules/inject-member-accessibility.js +5 -198
- package/src/rules/internals/angular-member-visibility.js +233 -0
- package/src/rules/internals/implemented-contract-members.js +294 -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-leading-underscore-class-member.js +120 -0
- package/src/rules/no-legacy-angular-decorators.js +196 -1
- package/src/rules/no-public-property.js +47 -1
- package/src/rules/no-redundant-internal.js +94 -0
- package/src/rules/no-standalone-flag.js +95 -0
- package/src/rules/no-typed-injected-element-ref.js +88 -0
- package/src/rules/require-on-push-change-detection.js +271 -0
- package/src/rules/require-view-encapsulation-none.js +215 -52
- package/src/rules/template-member-accessibility.js +190 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/** @typedef {'inject' | 'input' | 'output' | 'query' | 'property' | 'constructor' | 'method' | 'private-method'} TMemberGroup */
|
|
5
|
+
/** @typedef {{ dependencies: Set<string>; group: TMemberGroup; groupIndex: number; name: string; node: any; originalIndex: number }} TMemberEntry */
|
|
6
|
+
/** @typedef {TMemberEntry & { segmentText: string }} TSegmentEntry */
|
|
7
|
+
|
|
8
|
+
const INPUT_APIS = new Set(['input', 'model']);
|
|
9
|
+
const OUTPUT_APIS = new Set(['output', 'outputFromObservable']);
|
|
10
|
+
const QUERY_APIS = new Set(['viewChild', 'viewChildren', 'contentChild', 'contentChildren']);
|
|
11
|
+
|
|
12
|
+
/** @type {TMemberGroup[]} */
|
|
13
|
+
const MEMBER_GROUP_ORDER = [
|
|
14
|
+
'inject',
|
|
15
|
+
'input',
|
|
16
|
+
'output',
|
|
17
|
+
'query',
|
|
18
|
+
'property',
|
|
19
|
+
'constructor',
|
|
20
|
+
'method',
|
|
21
|
+
'private-method',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const MEMBER_GROUP_ORDER_TEXT =
|
|
25
|
+
'inject() members, inputs/model(), outputs, view/content queries, other properties, constructor, public methods, private methods';
|
|
26
|
+
|
|
27
|
+
const MEMBER_GROUP_INDEX = new Map(
|
|
28
|
+
MEMBER_GROUP_ORDER.map(/** @param {TMemberGroup} group @param {number} index */ (group, index) => [group, index]),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {any} key
|
|
33
|
+
*/
|
|
34
|
+
const getMemberNameFromKey = (key) => {
|
|
35
|
+
if (!key) return null;
|
|
36
|
+
if (key.type === 'Identifier') return key.name;
|
|
37
|
+
if (key.type === 'Literal' && typeof key.value === 'string') return key.value;
|
|
38
|
+
if (key.type === 'PrivateIdentifier') return key.name;
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {any} node
|
|
45
|
+
*/
|
|
46
|
+
const getMemberName = (node) => {
|
|
47
|
+
if (node.type === 'MethodDefinition' && node.kind === 'constructor') {
|
|
48
|
+
return 'constructor';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return getMemberNameFromKey(node.key);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {any} value
|
|
56
|
+
*/
|
|
57
|
+
const getCallRootName = (value) => {
|
|
58
|
+
if (!value || value.type !== 'CallExpression') return null;
|
|
59
|
+
|
|
60
|
+
const { callee } = value;
|
|
61
|
+
|
|
62
|
+
if (callee.type === 'Identifier') {
|
|
63
|
+
return callee.name;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (callee.type === 'MemberExpression' && !callee.computed && callee.object.type === 'Identifier') {
|
|
67
|
+
return callee.object.name;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {any} node
|
|
75
|
+
* @param {Set<string>} apiNames
|
|
76
|
+
*/
|
|
77
|
+
const isPropertyInitializedWith = (node, apiNames) => {
|
|
78
|
+
if (node.type !== 'PropertyDefinition') return false;
|
|
79
|
+
|
|
80
|
+
const apiName = getCallRootName(node.value);
|
|
81
|
+
return apiName !== null && apiNames.has(apiName);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {any} node
|
|
86
|
+
* @returns {TMemberGroup | null}
|
|
87
|
+
*/
|
|
88
|
+
const getMemberGroup = (node) => {
|
|
89
|
+
if (node.static) return null;
|
|
90
|
+
|
|
91
|
+
if (node.type === 'PropertyDefinition') {
|
|
92
|
+
if (isPropertyInitializedWith(node, new Set(['inject']))) {
|
|
93
|
+
return 'inject';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (isPropertyInitializedWith(node, INPUT_APIS)) {
|
|
97
|
+
return 'input';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (isPropertyInitializedWith(node, OUTPUT_APIS)) {
|
|
101
|
+
return 'output';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (isPropertyInitializedWith(node, QUERY_APIS)) {
|
|
105
|
+
return 'query';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return 'property';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (node.type === 'MethodDefinition') {
|
|
112
|
+
if (node.kind === 'constructor') {
|
|
113
|
+
return 'constructor';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (node.accessibility === 'private') {
|
|
117
|
+
return 'private-method';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return 'method';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {any} node
|
|
128
|
+
* @param {Set<string>} references
|
|
129
|
+
*/
|
|
130
|
+
const collectThisReferences = (node, references) => {
|
|
131
|
+
if (!node || typeof node !== 'object') return;
|
|
132
|
+
|
|
133
|
+
if (Array.isArray(node)) {
|
|
134
|
+
for (const entry of node) {
|
|
135
|
+
collectThisReferences(entry, references);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (
|
|
142
|
+
node.type === 'MemberExpression' &&
|
|
143
|
+
!node.computed &&
|
|
144
|
+
node.object?.type === 'ThisExpression' &&
|
|
145
|
+
(node.property?.type === 'Identifier' || node.property?.type === 'PrivateIdentifier')
|
|
146
|
+
) {
|
|
147
|
+
references.add(node.property.name);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const [key, value] of Object.entries(node)) {
|
|
151
|
+
if (key === 'parent' || key === 'range' || key === 'loc') continue;
|
|
152
|
+
if (!value || typeof value !== 'object') continue;
|
|
153
|
+
|
|
154
|
+
collectThisReferences(value, references);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {any} node
|
|
160
|
+
*/
|
|
161
|
+
const getInitializerDependencies = (node) => {
|
|
162
|
+
if (node.type !== 'PropertyDefinition' || !node.value) {
|
|
163
|
+
return new Set();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const dependencies = new Set();
|
|
167
|
+
collectThisReferences(node.value, dependencies);
|
|
168
|
+
|
|
169
|
+
const memberName = getMemberName(node);
|
|
170
|
+
|
|
171
|
+
if (memberName !== null) {
|
|
172
|
+
dependencies.delete(memberName);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return dependencies;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {any} node
|
|
180
|
+
* @param {number} originalIndex
|
|
181
|
+
* @returns {TMemberEntry | null}
|
|
182
|
+
*/
|
|
183
|
+
const toMemberEntry = (node, originalIndex) => {
|
|
184
|
+
const group = getMemberGroup(node);
|
|
185
|
+
const name = getMemberName(node);
|
|
186
|
+
|
|
187
|
+
if (group === null || name === null) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
dependencies: getInitializerDependencies(node),
|
|
193
|
+
group,
|
|
194
|
+
groupIndex: MEMBER_GROUP_INDEX.get(group),
|
|
195
|
+
name,
|
|
196
|
+
node,
|
|
197
|
+
originalIndex,
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @param {TMemberEntry} currentMember
|
|
203
|
+
* @param {TMemberEntry} earlierMember
|
|
204
|
+
*/
|
|
205
|
+
const getOrderingConflict = (currentMember, earlierMember) => {
|
|
206
|
+
if (currentMember.dependencies.has(earlierMember.name)) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (earlierMember.dependencies.has(currentMember.name)) {
|
|
211
|
+
return {
|
|
212
|
+
type: 'dependency',
|
|
213
|
+
blockingMember: earlierMember,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (currentMember.groupIndex < earlierMember.groupIndex) {
|
|
218
|
+
return {
|
|
219
|
+
type: 'group',
|
|
220
|
+
blockingMember: earlierMember,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return null;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @param {TMemberEntry[]} members
|
|
229
|
+
*/
|
|
230
|
+
const sortMembers = (members) => {
|
|
231
|
+
/** @type {TMemberEntry[]} */
|
|
232
|
+
const remainingMembers = [...members];
|
|
233
|
+
/** @type {TMemberEntry[]} */
|
|
234
|
+
const sortedMembers = [];
|
|
235
|
+
|
|
236
|
+
while (remainingMembers.length > 0) {
|
|
237
|
+
const readyMembers = remainingMembers
|
|
238
|
+
.filter((member) =>
|
|
239
|
+
[...member.dependencies].every(
|
|
240
|
+
(dependencyName) => !remainingMembers.some((remainingMember) => remainingMember.name === dependencyName),
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
.sort((left, right) => {
|
|
244
|
+
if (left.groupIndex !== right.groupIndex) {
|
|
245
|
+
return left.groupIndex - right.groupIndex;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return left.originalIndex - right.originalIndex;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const nextMember = readyMembers[0];
|
|
252
|
+
|
|
253
|
+
if (!nextMember) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
sortedMembers.push(nextMember);
|
|
258
|
+
|
|
259
|
+
const nextMemberIndex = remainingMembers.indexOf(nextMember);
|
|
260
|
+
remainingMembers.splice(nextMemberIndex, 1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return sortedMembers;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
268
|
+
* @param {any} classBodyNode
|
|
269
|
+
* @param {TMemberEntry[]} members
|
|
270
|
+
*/
|
|
271
|
+
const getSegmentEntries = (sourceCode, classBodyNode, members) => {
|
|
272
|
+
const openingBrace = sourceCode.getFirstToken(classBodyNode);
|
|
273
|
+
const closingBrace = sourceCode.getLastToken(classBodyNode);
|
|
274
|
+
|
|
275
|
+
if (!openingBrace || !closingBrace || openingBrace.value !== '{' || closingBrace.value !== '}') {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let segmentStart = openingBrace.range[1];
|
|
280
|
+
|
|
281
|
+
const segmentEntries = members.map((member) => {
|
|
282
|
+
const segmentText = sourceCode.text.slice(segmentStart, member.node.range[1]);
|
|
283
|
+
segmentStart = member.node.range[1];
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
...member,
|
|
287
|
+
segmentText,
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const suffix = sourceCode.text.slice(segmentStart, closingBrace.range[0]);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
closingBrace,
|
|
295
|
+
openingBrace,
|
|
296
|
+
segmentEntries,
|
|
297
|
+
suffix,
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
303
|
+
* @param {any} classBodyNode
|
|
304
|
+
* @param {TMemberEntry[]} members
|
|
305
|
+
*/
|
|
306
|
+
const buildClassOrderFix = (sourceCode, classBodyNode, members) => {
|
|
307
|
+
if (classBodyNode.body.length !== members.length) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const sortedMembers = sortMembers(members);
|
|
312
|
+
|
|
313
|
+
if (!sortedMembers) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const segments = getSegmentEntries(sourceCode, classBodyNode, members);
|
|
318
|
+
|
|
319
|
+
if (!segments) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const { closingBrace, openingBrace, segmentEntries, suffix } = segments;
|
|
324
|
+
const segmentEntriesByOriginalIndex = new Map(
|
|
325
|
+
segmentEntries.map(
|
|
326
|
+
/** @param {TSegmentEntry} segmentEntry */ (segmentEntry) => [segmentEntry.originalIndex, segmentEntry],
|
|
327
|
+
),
|
|
328
|
+
);
|
|
329
|
+
const sortedSegmentEntries = sortedMembers.map((member) => segmentEntriesByOriginalIndex.get(member.originalIndex));
|
|
330
|
+
|
|
331
|
+
if (sortedSegmentEntries.some((entry) => entry === undefined)) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const isAlreadySorted = sortedSegmentEntries.every((entry, index) => entry === segmentEntries[index]);
|
|
336
|
+
|
|
337
|
+
if (isAlreadySorted) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const reorderedBody = sortedSegmentEntries.map((entry) => entry.segmentText).join('') + suffix;
|
|
342
|
+
|
|
343
|
+
return (fixer) => fixer.replaceTextRange([openingBrace.range[1], closingBrace.range[0]], reorderedBody);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
347
|
+
const classMemberOrder = {
|
|
348
|
+
meta: {
|
|
349
|
+
type: 'layout',
|
|
350
|
+
fixable: 'code',
|
|
351
|
+
docs: {
|
|
352
|
+
description:
|
|
353
|
+
'Require class members to follow the styleguide order for inject members, inputs, outputs, queries, properties, constructor, and methods.',
|
|
354
|
+
recommended: true,
|
|
355
|
+
},
|
|
356
|
+
schema: [],
|
|
357
|
+
messages: {
|
|
358
|
+
dependencyOrder:
|
|
359
|
+
"'{{name}}' should be declared before '{{dependentName}}' because '{{dependentName}}' references it in an initializer.",
|
|
360
|
+
groupOrder:
|
|
361
|
+
"'{{name}}' should be declared before '{{blockingName}}'. Class members should be ordered as: {{order}}.",
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
create(context) {
|
|
365
|
+
const sourceCode = context.sourceCode;
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
ClassBody(node) {
|
|
369
|
+
const members = node.body
|
|
370
|
+
.map(
|
|
371
|
+
/** @param {any} member @param {number} originalIndex */ (member, originalIndex) =>
|
|
372
|
+
toMemberEntry(member, originalIndex),
|
|
373
|
+
)
|
|
374
|
+
.filter(Boolean);
|
|
375
|
+
|
|
376
|
+
if (members.length < 2) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const fix = buildClassOrderFix(sourceCode, node, members);
|
|
381
|
+
let hasAttachedFix = false;
|
|
382
|
+
|
|
383
|
+
for (let currentIndex = 1; currentIndex < members.length; currentIndex++) {
|
|
384
|
+
const currentMember = members[currentIndex];
|
|
385
|
+
|
|
386
|
+
for (let earlierIndex = currentIndex - 1; earlierIndex >= 0; earlierIndex--) {
|
|
387
|
+
const earlierMember = members[earlierIndex];
|
|
388
|
+
const conflict = getOrderingConflict(currentMember, earlierMember);
|
|
389
|
+
|
|
390
|
+
if (!conflict) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (conflict.type === 'dependency') {
|
|
395
|
+
context.report({
|
|
396
|
+
node: currentMember.node,
|
|
397
|
+
messageId: 'dependencyOrder',
|
|
398
|
+
data: {
|
|
399
|
+
name: currentMember.name,
|
|
400
|
+
dependentName: conflict.blockingMember.name,
|
|
401
|
+
},
|
|
402
|
+
fix: !hasAttachedFix ? fix : null,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
hasAttachedFix = hasAttachedFix || Boolean(fix);
|
|
406
|
+
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
context.report({
|
|
411
|
+
node: currentMember.node,
|
|
412
|
+
messageId: 'groupOrder',
|
|
413
|
+
data: {
|
|
414
|
+
name: currentMember.name,
|
|
415
|
+
blockingName: conflict.blockingMember.name,
|
|
416
|
+
order: MEMBER_GROUP_ORDER_TEXT,
|
|
417
|
+
},
|
|
418
|
+
fix: !hasAttachedFix ? fix : null,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
hasAttachedFix = hasAttachedFix || Boolean(fix);
|
|
422
|
+
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
module.exports = classMemberOrder;
|
|
@@ -1,188 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
const path = require('path');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @param {string} value
|
|
9
|
-
*/
|
|
10
|
-
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @param {any} decorator
|
|
14
|
-
*/
|
|
15
|
-
const getDecoratorCall = (decorator) => {
|
|
16
|
-
if (!decorator || decorator.type !== 'Decorator') return null;
|
|
17
|
-
|
|
18
|
-
const expression = decorator.expression;
|
|
19
|
-
if (expression.type !== 'CallExpression') return null;
|
|
20
|
-
if (expression.callee.type !== 'Identifier') return null;
|
|
21
|
-
if (expression.arguments.length === 0) return null;
|
|
22
|
-
|
|
23
|
-
const metadata = expression.arguments[0];
|
|
24
|
-
if (!metadata || metadata.type !== 'ObjectExpression') return null;
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
name: expression.callee.name,
|
|
28
|
-
metadata,
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @param {any} property
|
|
34
|
-
*/
|
|
35
|
-
const getPropertyKeyName = (property) => {
|
|
36
|
-
if (!property || property.computed) return null;
|
|
37
|
-
|
|
38
|
-
const key = property.key;
|
|
39
|
-
if (key.type === 'Identifier') return key.name;
|
|
40
|
-
if (key.type === 'Literal' && typeof key.value === 'string') return key.value;
|
|
41
|
-
|
|
42
|
-
return null;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* @param {any} metadata
|
|
47
|
-
* @param {string} propertyName
|
|
48
|
-
*/
|
|
49
|
-
const getMetadataProperty = (metadata, propertyName) => {
|
|
50
|
-
for (const property of metadata.properties) {
|
|
51
|
-
if (!property || property.type !== 'Property' || property.computed) continue;
|
|
52
|
-
|
|
53
|
-
const keyName = getPropertyKeyName(property);
|
|
54
|
-
|
|
55
|
-
if (keyName === propertyName) {
|
|
56
|
-
return property;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return null;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* @param {any} node
|
|
65
|
-
*/
|
|
66
|
-
const getStringValue = (node) => {
|
|
67
|
-
if (!node) return null;
|
|
68
|
-
if (node.type === 'Literal' && typeof node.value === 'string') return node.value;
|
|
69
|
-
|
|
70
|
-
if (node.type === 'TemplateLiteral' && node.expressions.length === 0 && node.quasis.length === 1) {
|
|
71
|
-
return node.quasis[0].value.cooked || '';
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return null;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* @param {string} filePath
|
|
79
|
-
*/
|
|
80
|
-
const readFileIfExists = (filePath) => {
|
|
81
|
-
try {
|
|
82
|
-
if (fs.existsSync(filePath)) {
|
|
83
|
-
return fs.readFileSync(filePath, 'utf8');
|
|
84
|
-
}
|
|
85
|
-
} catch {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return null;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* @param {string} memberName
|
|
94
|
-
* @param {string | null} template
|
|
95
|
-
*/
|
|
96
|
-
const templateReferencesMember = (memberName, template) => {
|
|
97
|
-
if (!template) return false;
|
|
98
|
-
|
|
99
|
-
const escapedName = escapeRegExp(memberName);
|
|
100
|
-
const patterns = [
|
|
101
|
-
new RegExp(`\\{\\{[\\s\\S]*?\\b${escapedName}\\b[\\s\\S]*?\\}\\}`, 'u'),
|
|
102
|
-
new RegExp(`@[a-zA-Z]+\\s*\\([\\s\\S]*?\\b${escapedName}\\b[\\s\\S]*?\\)`, 'u'),
|
|
103
|
-
new RegExp(`\\[[^\\]]+\\]\\s*=\\s*"[^"]*\\b${escapedName}\\b[^"]*"`, 'u'),
|
|
104
|
-
new RegExp(`\\[[^\\]]+\\]\\s*=\\s*'[^']*\\b${escapedName}\\b[^']*'`, 'u'),
|
|
105
|
-
new RegExp(`\\([^\\)]+\\)\\s*=\\s*"[^"]*\\b${escapedName}\\b[^"]*"`, 'u'),
|
|
106
|
-
new RegExp(`\\([^\\)]+\\)\\s*=\\s*'[^']*\\b${escapedName}\\b[^']*'`, 'u'),
|
|
107
|
-
];
|
|
108
|
-
|
|
109
|
-
return patterns.some((pattern) => pattern.test(template));
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* @param {string | null} keyName
|
|
114
|
-
*/
|
|
115
|
-
const isDynamicHostBindingKey = (keyName) => {
|
|
116
|
-
if (!keyName) return false;
|
|
117
|
-
|
|
118
|
-
return keyName.startsWith('[') || keyName.startsWith('(') || keyName.startsWith('@');
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* @param {any} classNode
|
|
123
|
-
* @param {import('eslint').Rule.RuleContext} context
|
|
124
|
-
*/
|
|
125
|
-
const getAngularMetadata = (classNode, context) => {
|
|
126
|
-
for (const decorator of classNode.decorators || []) {
|
|
127
|
-
const call = getDecoratorCall(decorator);
|
|
128
|
-
if (!call) continue;
|
|
129
|
-
|
|
130
|
-
if (call.name === 'Component' || call.name === 'Directive') {
|
|
131
|
-
return call;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return null;
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* @param {string} memberName
|
|
140
|
-
* @param {string | null} text
|
|
141
|
-
*/
|
|
142
|
-
const textReferencesMember = (memberName, text) => {
|
|
143
|
-
if (!text) return false;
|
|
144
|
-
|
|
145
|
-
const pattern = new RegExp(`\\b${escapeRegExp(memberName)}\\b`, 'u');
|
|
146
|
-
return pattern.test(text);
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* @param {string} memberName
|
|
151
|
-
* @param {any} metadata
|
|
152
|
-
* @param {import('eslint').Rule.RuleContext} context
|
|
153
|
-
*/
|
|
154
|
-
const isReferencedFromTemplateOrHost = (memberName, metadata, context) => {
|
|
155
|
-
const templateProperty = getMetadataProperty(metadata, 'template');
|
|
156
|
-
if (templateProperty && templateReferencesMember(memberName, getStringValue(templateProperty.value))) {
|
|
157
|
-
return true;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const templateUrlProperty = getMetadataProperty(metadata, 'templateUrl');
|
|
161
|
-
if (templateUrlProperty) {
|
|
162
|
-
const templateUrl = getStringValue(templateUrlProperty.value);
|
|
163
|
-
if (templateUrl) {
|
|
164
|
-
const templatePath = path.resolve(path.dirname(context.filename), templateUrl);
|
|
165
|
-
const templateText = readFileIfExists(templatePath);
|
|
166
|
-
if (templateReferencesMember(memberName, templateText)) {
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const hostProperty = getMetadataProperty(metadata, 'host');
|
|
173
|
-
if (hostProperty && hostProperty.value.type === 'ObjectExpression') {
|
|
174
|
-
for (const property of hostProperty.value.properties) {
|
|
175
|
-
if (!property || property.type !== 'Property') continue;
|
|
176
|
-
if (!isDynamicHostBindingKey(getPropertyKeyName(property))) continue;
|
|
177
|
-
|
|
178
|
-
if (textReferencesMember(memberName, getStringValue(property.value))) {
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return false;
|
|
185
|
-
};
|
|
4
|
+
const { getMemberName, isReferencedFromTemplateOrHost } = require('./internals/angular-member-visibility');
|
|
186
5
|
|
|
187
6
|
/**
|
|
188
7
|
* @param {any} node
|
|
@@ -193,15 +12,6 @@ const isInjectCall = (node) => {
|
|
|
193
12
|
return node.value.callee.type === 'Identifier' && node.value.callee.name === 'inject';
|
|
194
13
|
};
|
|
195
14
|
|
|
196
|
-
/**
|
|
197
|
-
* @param {any} node
|
|
198
|
-
*/
|
|
199
|
-
const getMemberName = (node) => {
|
|
200
|
-
if (node.key.type === 'Identifier') return node.key.name;
|
|
201
|
-
if (node.key.type === 'Literal' && typeof node.key.value === 'string') return node.key.value;
|
|
202
|
-
return null;
|
|
203
|
-
};
|
|
204
|
-
|
|
205
15
|
/**
|
|
206
16
|
* @param {any} node
|
|
207
17
|
* @param {import('eslint').SourceCode} sourceCode
|
|
@@ -227,7 +37,7 @@ const injectMemberAccessibility = {
|
|
|
227
37
|
type: 'problem',
|
|
228
38
|
docs: {
|
|
229
39
|
description:
|
|
230
|
-
'Require injected class members to be private by default,
|
|
40
|
+
'Require injected class members to be private by default, protected when referenced from an Angular template or host binding, and allow explicit public when intentionally exposing an external API.',
|
|
231
41
|
recommended: true,
|
|
232
42
|
},
|
|
233
43
|
fixable: 'code',
|
|
@@ -236,7 +46,7 @@ const injectMemberAccessibility = {
|
|
|
236
46
|
shouldBePrivate:
|
|
237
47
|
'Injected member `{{name}}` should be private. Use protected only when it is referenced from a template or host binding.',
|
|
238
48
|
shouldBeProtected:
|
|
239
|
-
'Injected member `{{name}}` should be protected because it is referenced from a template or host binding.',
|
|
49
|
+
'Injected member `{{name}}` should be protected because it is referenced from a template or host binding. Use public only when it intentionally exposes an external API across class boundaries.',
|
|
240
50
|
},
|
|
241
51
|
},
|
|
242
52
|
create(context) {
|
|
@@ -252,13 +62,10 @@ const injectMemberAccessibility = {
|
|
|
252
62
|
const memberName = getMemberName(node);
|
|
253
63
|
if (!memberName) return;
|
|
254
64
|
|
|
255
|
-
const
|
|
256
|
-
const isTemplateOrHostVisible = angularMetadata
|
|
257
|
-
? isReferencedFromTemplateOrHost(memberName, angularMetadata.metadata, context)
|
|
258
|
-
: false;
|
|
65
|
+
const isTemplateOrHostVisible = isReferencedFromTemplateOrHost(classNode, memberName, context);
|
|
259
66
|
|
|
260
67
|
if (isTemplateOrHostVisible) {
|
|
261
|
-
if (node.accessibility === 'protected') return;
|
|
68
|
+
if (node.accessibility === 'protected' || node.accessibility === 'public') return;
|
|
262
69
|
|
|
263
70
|
context.report({
|
|
264
71
|
node,
|