@ethlete/eslint-plugin 0.1.0-next.2 → 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 +6 -0
- package/package.json +3 -2
- package/src/configs/recommended.js +31 -8
- package/src/index.js +12 -2
- 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 -216
- package/src/rules/internals/angular-member-visibility.js +233 -0
- package/src/rules/internals/implemented-contract-members.js +294 -0
- package/src/rules/no-public-property.js +42 -7
- package/src/rules/no-redundant-internal.js +94 -0
- package/src/rules/no-typed-injected-element-ref.js +88 -0
- 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,206 +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 {import('eslint').Rule.RuleContext} context
|
|
94
|
-
*/
|
|
95
|
-
const getContextFilename = (context) => {
|
|
96
|
-
const filename = context.physicalFilename || context.filename;
|
|
97
|
-
|
|
98
|
-
if (!filename || filename === '<input>' || filename === '<text>') {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return filename;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* @param {string} memberName
|
|
107
|
-
* @param {string | null} template
|
|
108
|
-
*/
|
|
109
|
-
const templateReferencesMember = (memberName, template) => {
|
|
110
|
-
if (!template) return false;
|
|
111
|
-
|
|
112
|
-
const escapedName = escapeRegExp(memberName);
|
|
113
|
-
const patterns = [
|
|
114
|
-
new RegExp(`\\{\\{[\\s\\S]*?\\b${escapedName}\\b[\\s\\S]*?\\}\\}`, 'u'),
|
|
115
|
-
new RegExp(`@[a-zA-Z]+\\s*\\([\\s\\S]*?\\b${escapedName}\\b[\\s\\S]*?\\)`, 'u'),
|
|
116
|
-
new RegExp(`@let\\s+[a-zA-Z_$][\\w$]*\\s*=\\s*[\\s\\S]*?\\b${escapedName}\\b`, 'u'),
|
|
117
|
-
new RegExp(`\\*[a-zA-Z0-9_\\-]+\\s*=\\s*"[^"]*\\b${escapedName}\\b[^"]*"`, 'u'),
|
|
118
|
-
new RegExp(`\\*[a-zA-Z0-9_\\-]+\\s*=\\s*'[^']*\\b${escapedName}\\b[^']*'`, 'u'),
|
|
119
|
-
new RegExp(`\\[[^\\]]+\\]\\s*=\\s*"[^"]*\\b${escapedName}\\b[^"]*"`, 'u'),
|
|
120
|
-
new RegExp(`\\[[^\\]]+\\]\\s*=\\s*'[^']*\\b${escapedName}\\b[^']*'`, 'u'),
|
|
121
|
-
new RegExp(`\\([^\\)]+\\)\\s*=\\s*"[^"]*\\b${escapedName}\\b[^"]*"`, 'u'),
|
|
122
|
-
new RegExp(`\\([^\\)]+\\)\\s*=\\s*'[^']*\\b${escapedName}\\b[^']*'`, 'u'),
|
|
123
|
-
];
|
|
124
|
-
|
|
125
|
-
return patterns.some((pattern) => pattern.test(template));
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* @param {string | null} keyName
|
|
130
|
-
*/
|
|
131
|
-
const isDynamicHostBindingKey = (keyName) => {
|
|
132
|
-
if (!keyName) return false;
|
|
133
|
-
|
|
134
|
-
return keyName.startsWith('[') || keyName.startsWith('(') || keyName.startsWith('@');
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* @param {any} classNode
|
|
139
|
-
* @param {import('eslint').Rule.RuleContext} context
|
|
140
|
-
*/
|
|
141
|
-
const getAngularMetadata = (classNode, context) => {
|
|
142
|
-
for (const decorator of classNode.decorators || []) {
|
|
143
|
-
const call = getDecoratorCall(decorator);
|
|
144
|
-
if (!call) continue;
|
|
145
|
-
|
|
146
|
-
if (call.name === 'Component' || call.name === 'Directive') {
|
|
147
|
-
return call;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return null;
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* @param {string} memberName
|
|
156
|
-
* @param {string | null} text
|
|
157
|
-
*/
|
|
158
|
-
const textReferencesMember = (memberName, text) => {
|
|
159
|
-
if (!text) return false;
|
|
160
|
-
|
|
161
|
-
const pattern = new RegExp(`\\b${escapeRegExp(memberName)}\\b`, 'u');
|
|
162
|
-
return pattern.test(text);
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* @param {string} memberName
|
|
167
|
-
* @param {any} metadata
|
|
168
|
-
* @param {import('eslint').Rule.RuleContext} context
|
|
169
|
-
*/
|
|
170
|
-
const isReferencedFromTemplateOrHost = (memberName, metadata, context) => {
|
|
171
|
-
const templateProperty = getMetadataProperty(metadata, 'template');
|
|
172
|
-
if (templateProperty && templateReferencesMember(memberName, getStringValue(templateProperty.value))) {
|
|
173
|
-
return true;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const templateUrlProperty = getMetadataProperty(metadata, 'templateUrl');
|
|
177
|
-
if (templateUrlProperty) {
|
|
178
|
-
const templateUrl = getStringValue(templateUrlProperty.value);
|
|
179
|
-
const filename = getContextFilename(context);
|
|
180
|
-
|
|
181
|
-
if (templateUrl && filename) {
|
|
182
|
-
const templatePath = path.resolve(path.dirname(filename), templateUrl);
|
|
183
|
-
const templateText = readFileIfExists(templatePath);
|
|
184
|
-
if (templateReferencesMember(memberName, templateText)) {
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const hostProperty = getMetadataProperty(metadata, 'host');
|
|
191
|
-
if (hostProperty && hostProperty.value.type === 'ObjectExpression') {
|
|
192
|
-
for (const property of hostProperty.value.properties) {
|
|
193
|
-
if (!property || property.type !== 'Property') continue;
|
|
194
|
-
if (!isDynamicHostBindingKey(getPropertyKeyName(property))) continue;
|
|
195
|
-
|
|
196
|
-
if (textReferencesMember(memberName, getStringValue(property.value))) {
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return false;
|
|
203
|
-
};
|
|
4
|
+
const { getMemberName, isReferencedFromTemplateOrHost } = require('./internals/angular-member-visibility');
|
|
204
5
|
|
|
205
6
|
/**
|
|
206
7
|
* @param {any} node
|
|
@@ -211,15 +12,6 @@ const isInjectCall = (node) => {
|
|
|
211
12
|
return node.value.callee.type === 'Identifier' && node.value.callee.name === 'inject';
|
|
212
13
|
};
|
|
213
14
|
|
|
214
|
-
/**
|
|
215
|
-
* @param {any} node
|
|
216
|
-
*/
|
|
217
|
-
const getMemberName = (node) => {
|
|
218
|
-
if (node.key.type === 'Identifier') return node.key.name;
|
|
219
|
-
if (node.key.type === 'Literal' && typeof node.key.value === 'string') return node.key.value;
|
|
220
|
-
return null;
|
|
221
|
-
};
|
|
222
|
-
|
|
223
15
|
/**
|
|
224
16
|
* @param {any} node
|
|
225
17
|
* @param {import('eslint').SourceCode} sourceCode
|
|
@@ -245,7 +37,7 @@ const injectMemberAccessibility = {
|
|
|
245
37
|
type: 'problem',
|
|
246
38
|
docs: {
|
|
247
39
|
description:
|
|
248
|
-
'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.',
|
|
249
41
|
recommended: true,
|
|
250
42
|
},
|
|
251
43
|
fixable: 'code',
|
|
@@ -254,7 +46,7 @@ const injectMemberAccessibility = {
|
|
|
254
46
|
shouldBePrivate:
|
|
255
47
|
'Injected member `{{name}}` should be private. Use protected only when it is referenced from a template or host binding.',
|
|
256
48
|
shouldBeProtected:
|
|
257
|
-
'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.',
|
|
258
50
|
},
|
|
259
51
|
},
|
|
260
52
|
create(context) {
|
|
@@ -270,13 +62,10 @@ const injectMemberAccessibility = {
|
|
|
270
62
|
const memberName = getMemberName(node);
|
|
271
63
|
if (!memberName) return;
|
|
272
64
|
|
|
273
|
-
const
|
|
274
|
-
const isTemplateOrHostVisible = angularMetadata
|
|
275
|
-
? isReferencedFromTemplateOrHost(memberName, angularMetadata.metadata, context)
|
|
276
|
-
: false;
|
|
65
|
+
const isTemplateOrHostVisible = isReferencedFromTemplateOrHost(classNode, memberName, context);
|
|
277
66
|
|
|
278
67
|
if (isTemplateOrHostVisible) {
|
|
279
|
-
if (node.accessibility === 'protected') return;
|
|
68
|
+
if (node.accessibility === 'protected' || node.accessibility === 'public') return;
|
|
280
69
|
|
|
281
70
|
context.report({
|
|
282
71
|
node,
|