@benjavicente/lint-angular 0.0.2 → 0.0.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/README.md +18 -17
- package/dist/index.mjs +530 -647
- package/package.json +12 -12
package/dist/index.mjs
CHANGED
|
@@ -171,19 +171,45 @@ function isShadowedIdentifier(context, node) {
|
|
|
171
171
|
}
|
|
172
172
|
//#endregion
|
|
173
173
|
//#region src/utilities/angular.ts
|
|
174
|
-
function
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
function getProgramNode(context, node) {
|
|
175
|
+
if (node.type === "Program") return node;
|
|
176
|
+
return context.sourceCode.getAncestors(node).find((ancestor) => ancestor.type === "Program") ?? null;
|
|
177
|
+
}
|
|
178
|
+
function getImportedName(context, node, source) {
|
|
179
|
+
if (node?.type !== "Identifier") return null;
|
|
180
|
+
if (isShadowedIdentifier(context, node)) return null;
|
|
181
|
+
const program = getProgramNode(context, node);
|
|
182
|
+
for (const statement of program?.body ?? []) {
|
|
183
|
+
if (statement.type !== "ImportDeclaration" || statement.source?.value !== source) continue;
|
|
184
|
+
for (const specifier of statement.specifiers ?? []) {
|
|
185
|
+
if (specifier.type !== "ImportSpecifier" || specifier.local.name !== node.name) continue;
|
|
186
|
+
return getPropertyName(specifier.imported);
|
|
187
|
+
}
|
|
178
188
|
}
|
|
179
|
-
|
|
189
|
+
return null;
|
|
180
190
|
}
|
|
181
|
-
function
|
|
191
|
+
function isNamespaceImport(context, node, source) {
|
|
192
|
+
if (node?.type !== "Identifier") return false;
|
|
193
|
+
if (isShadowedIdentifier(context, node)) return false;
|
|
194
|
+
const program = getProgramNode(context, node);
|
|
195
|
+
for (const statement of program?.body ?? []) {
|
|
196
|
+
if (statement.type !== "ImportDeclaration" || statement.source?.value !== source) continue;
|
|
197
|
+
if ((statement.specifiers ?? []).some((specifier) => specifier.type === "ImportNamespaceSpecifier" && specifier.local.name === node.name)) return true;
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
function isImportedReference(context, node, source, importedNames) {
|
|
202
|
+
const importedName = getImportedName(context, node, source);
|
|
203
|
+
return !!importedName && importedNames.has(importedName);
|
|
204
|
+
}
|
|
205
|
+
function isImportedNamespaceMember(context, node, source, memberNames) {
|
|
206
|
+
return node?.type === "MemberExpression" && node.object?.type === "Identifier" && isNamespaceImport(context, node.object, source) && memberNames.has(getPropertyName(node.property) ?? "");
|
|
207
|
+
}
|
|
208
|
+
function isAngularCoreDecorator(context, decorator, decoratorNames) {
|
|
182
209
|
if (!decorator) return false;
|
|
183
210
|
const expression = decorator.expression ?? decorator;
|
|
184
211
|
const callee = expression?.type === "CallExpression" ? expression.callee : expression;
|
|
185
|
-
|
|
186
|
-
return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && imports.angularNamespaces.has(callee.object.name) && !isShadowedIdentifier(context, callee.object) && imports.decoratorNames.has(getPropertyName(callee.property) ?? "");
|
|
212
|
+
return isImportedReference(context, callee, "@angular/core", decoratorNames) || isImportedNamespaceMember(context, callee, "@angular/core", decoratorNames);
|
|
187
213
|
}
|
|
188
214
|
//#endregion
|
|
189
215
|
//#region src/rules/class-member-order/index.ts
|
|
@@ -207,38 +233,33 @@ const ORDER_LABELS = [
|
|
|
207
233
|
"outputs",
|
|
208
234
|
"everything else"
|
|
209
235
|
];
|
|
210
|
-
function hasAngularClassDecorator(context, classNode
|
|
236
|
+
function hasAngularClassDecorator(context, classNode) {
|
|
211
237
|
if (!classNode || !Array.isArray(classNode.decorators)) return false;
|
|
212
|
-
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator,
|
|
238
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, ANGULAR_CLASS_DECORATOR_NAMES$1));
|
|
213
239
|
}
|
|
214
|
-
function isApiCall(context, node,
|
|
240
|
+
function isApiCall(context, node, apiNames) {
|
|
215
241
|
if (!node || node.type !== "CallExpression") return false;
|
|
216
242
|
const callee = node.callee;
|
|
217
|
-
if (callee
|
|
243
|
+
if (isImportedReference(context, callee, "@angular/core", apiNames)) return true;
|
|
218
244
|
if (callee?.type !== "MemberExpression") return false;
|
|
219
245
|
const supportsRequiredApi = [...INPUT_MODEL_CALL_NAMES].some((name) => apiNames.has(name));
|
|
220
246
|
if (callee.object?.type === "Identifier") {
|
|
221
|
-
if (getPropertyName(callee.property) === "required") return supportsRequiredApi &&
|
|
222
|
-
return
|
|
247
|
+
if (getPropertyName(callee.property) === "required") return supportsRequiredApi && isImportedReference(context, callee.object, "@angular/core", apiNames);
|
|
248
|
+
return isImportedNamespaceMember(context, callee, "@angular/core", apiNames);
|
|
223
249
|
}
|
|
224
|
-
if (getPropertyName(callee.property) === "required" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier") return supportsRequiredApi &&
|
|
250
|
+
if (getPropertyName(callee.property) === "required" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier") return supportsRequiredApi && isImportedNamespaceMember(context, callee.object, "@angular/core", apiNames) && apiNames.has(getPropertyName(callee.object.property) ?? "");
|
|
225
251
|
return false;
|
|
226
252
|
}
|
|
227
|
-
function hasDecorator(context, element,
|
|
228
|
-
return Array.isArray(element.decorators) ? element.decorators.some((decorator) =>
|
|
229
|
-
const expression = decorator.expression ?? decorator;
|
|
230
|
-
const callee = expression?.type === "CallExpression" ? expression.callee : expression;
|
|
231
|
-
if (callee?.type === "Identifier") return localNames.has(callee.name) && !isShadowedIdentifier(context, callee);
|
|
232
|
-
return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && angularNamespaces.has(callee.object.name) && !isShadowedIdentifier(context, callee.object) && decoratorNames.has(getPropertyName(callee.property) ?? "");
|
|
233
|
-
}) : false;
|
|
253
|
+
function hasDecorator(context, element, decoratorNames) {
|
|
254
|
+
return Array.isArray(element.decorators) ? element.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorNames)) : false;
|
|
234
255
|
}
|
|
235
|
-
function classifyMember(context, element
|
|
256
|
+
function classifyMember(context, element) {
|
|
236
257
|
if (CLASS_FIELD_TYPES$1.has(element.type)) {
|
|
237
|
-
if (isApiCall(context, element.value,
|
|
238
|
-
if (isApiCall(context, element.value,
|
|
239
|
-
if (hasDecorator(context, element,
|
|
240
|
-
if (isApiCall(context, element.value,
|
|
241
|
-
if (hasDecorator(context, element,
|
|
258
|
+
if (isApiCall(context, element.value, new Set(["inject"]))) return 0;
|
|
259
|
+
if (isApiCall(context, element.value, INPUT_MODEL_CALL_NAMES)) return 1;
|
|
260
|
+
if (hasDecorator(context, element, new Set(["Input"]))) return 1;
|
|
261
|
+
if (isApiCall(context, element.value, OUTPUT_CALL_NAMES)) return 2;
|
|
262
|
+
if (hasDecorator(context, element, new Set(["Output"]))) return 2;
|
|
242
263
|
return 3;
|
|
243
264
|
}
|
|
244
265
|
if (element.type === "MethodDefinition") return 3;
|
|
@@ -354,83 +375,47 @@ const classMemberOrder = defineRule({
|
|
|
354
375
|
messages: { outOfOrder: "Angular class member should be ordered before {{previousGroup}} and with {{expectedGroup}}." }
|
|
355
376
|
},
|
|
356
377
|
createOnce(context) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (node.source?.value !== "@angular/core") return;
|
|
379
|
-
for (const specifier of node.specifiers ?? []) {
|
|
380
|
-
addAngularCoreDecoratorImport(specifier, ANGULAR_CLASS_DECORATOR_NAMES$1, imports);
|
|
381
|
-
if (specifier.type === "ImportSpecifier") {
|
|
382
|
-
const importedName = getPropertyName(specifier.imported);
|
|
383
|
-
if (importedName === "inject") imports.injectLocalNames.add(specifier.local.name);
|
|
384
|
-
if (importedName && INPUT_MODEL_CALL_NAMES.has(importedName)) imports.inputModelLocalNames.add(specifier.local.name);
|
|
385
|
-
if (importedName === "Input") imports.inputModelDecoratorLocalNames.add(specifier.local.name);
|
|
386
|
-
if (importedName && OUTPUT_CALL_NAMES.has(importedName)) imports.outputLocalNames.add(specifier.local.name);
|
|
387
|
-
if (importedName === "Output") imports.outputDecoratorLocalNames.add(specifier.local.name);
|
|
388
|
-
}
|
|
389
|
-
if (specifier.type === "ImportNamespaceSpecifier") imports.angularNamespaces.add(specifier.local.name);
|
|
390
|
-
}
|
|
391
|
-
},
|
|
392
|
-
ClassBody(node) {
|
|
393
|
-
const classBody = node;
|
|
394
|
-
if (!hasAngularClassDecorator(context, classBody.parent, imports)) return;
|
|
395
|
-
let highestSeen = null;
|
|
396
|
-
const classifiedMembers = [];
|
|
397
|
-
const outOfOrderReports = [];
|
|
398
|
-
for (const element of classBody.body ?? []) {
|
|
399
|
-
const group = classifyMember(context, element, imports);
|
|
400
|
-
if (group === null) continue;
|
|
401
|
-
classifiedMembers.push({
|
|
378
|
+
return { ClassBody(node) {
|
|
379
|
+
const classBody = node;
|
|
380
|
+
if (!hasAngularClassDecorator(context, classBody.parent)) return;
|
|
381
|
+
let highestSeen = null;
|
|
382
|
+
const classifiedMembers = [];
|
|
383
|
+
const outOfOrderReports = [];
|
|
384
|
+
for (const element of classBody.body ?? []) {
|
|
385
|
+
const group = classifyMember(context, element);
|
|
386
|
+
if (group === null) continue;
|
|
387
|
+
classifiedMembers.push({
|
|
388
|
+
element,
|
|
389
|
+
group,
|
|
390
|
+
effectiveGroup: group,
|
|
391
|
+
name: getMemberName$1(element.key),
|
|
392
|
+
dependencies: collectThisMemberReferences(element.value)
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
applyDependencyGroups(classifiedMembers);
|
|
396
|
+
for (const { element, effectiveGroup } of classifiedMembers) {
|
|
397
|
+
if (highestSeen !== null && effectiveGroup < highestSeen) {
|
|
398
|
+
outOfOrderReports.push({
|
|
402
399
|
element,
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
name: getMemberName$1(element.key),
|
|
406
|
-
dependencies: collectThisMemberReferences(element.value)
|
|
400
|
+
effectiveGroup,
|
|
401
|
+
previousEffectiveGroup: highestSeen
|
|
407
402
|
});
|
|
403
|
+
continue;
|
|
408
404
|
}
|
|
409
|
-
|
|
410
|
-
for (const { element, effectiveGroup } of classifiedMembers) {
|
|
411
|
-
if (highestSeen !== null && effectiveGroup < highestSeen) {
|
|
412
|
-
outOfOrderReports.push({
|
|
413
|
-
element,
|
|
414
|
-
effectiveGroup,
|
|
415
|
-
previousEffectiveGroup: highestSeen
|
|
416
|
-
});
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
highestSeen = effectiveGroup;
|
|
420
|
-
}
|
|
421
|
-
if (!outOfOrderReports.length) return;
|
|
422
|
-
const fix = getSortedMembersFix(context, classBody, classifiedMembers);
|
|
423
|
-
for (const [index, report] of outOfOrderReports.entries()) context.report({
|
|
424
|
-
node: report.element,
|
|
425
|
-
messageId: "outOfOrder",
|
|
426
|
-
data: {
|
|
427
|
-
expectedGroup: ORDER_LABELS[report.effectiveGroup],
|
|
428
|
-
previousGroup: ORDER_LABELS[report.previousEffectiveGroup]
|
|
429
|
-
},
|
|
430
|
-
fix: index === 0 ? fix : void 0
|
|
431
|
-
});
|
|
405
|
+
highestSeen = effectiveGroup;
|
|
432
406
|
}
|
|
433
|
-
|
|
407
|
+
if (!outOfOrderReports.length) return;
|
|
408
|
+
const fix = getSortedMembersFix(context, classBody, classifiedMembers);
|
|
409
|
+
for (const [index, report] of outOfOrderReports.entries()) context.report({
|
|
410
|
+
node: report.element,
|
|
411
|
+
messageId: "outOfOrder",
|
|
412
|
+
data: {
|
|
413
|
+
expectedGroup: ORDER_LABELS[report.effectiveGroup],
|
|
414
|
+
previousGroup: ORDER_LABELS[report.previousEffectiveGroup]
|
|
415
|
+
},
|
|
416
|
+
fix: index === 0 ? fix : void 0
|
|
417
|
+
});
|
|
418
|
+
} };
|
|
434
419
|
}
|
|
435
420
|
});
|
|
436
421
|
//#endregion
|
|
@@ -438,25 +423,22 @@ const classMemberOrder = defineRule({
|
|
|
438
423
|
const DEFAULT_DISALLOW_INJECT_INJECTOR = true;
|
|
439
424
|
const DEFAULT_DISALLOW_RUN_IN_INJECTION_CONTEXT = true;
|
|
440
425
|
const RUN_IN_INJECTION_CONTEXT_NAMES = new Set(["runInInjectionContext", "runInContext"]);
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
function isInjectCall(context, callNode, injectNames, angularNamespaces) {
|
|
426
|
+
const INJECT_NAMES = new Set(["inject"]);
|
|
427
|
+
const INJECTOR_NAMES = new Set(["Injector"]);
|
|
428
|
+
function isInjectCall(context, callNode) {
|
|
445
429
|
const callee = callNode.callee;
|
|
446
|
-
return callee
|
|
430
|
+
return isImportedReference(context, callee, "@angular/core", INJECT_NAMES) || isImportedNamespaceMember(context, callee, "@angular/core", INJECT_NAMES);
|
|
447
431
|
}
|
|
448
|
-
function isInjectorReference(context, node
|
|
449
|
-
return node
|
|
432
|
+
function isInjectorReference(context, node) {
|
|
433
|
+
return isImportedReference(context, node, "@angular/core", INJECTOR_NAMES) || isImportedNamespaceMember(context, node, "@angular/core", INJECTOR_NAMES);
|
|
450
434
|
}
|
|
451
|
-
function isDisallowedInjectInjector(context, callNode
|
|
452
|
-
if (!isInjectCall(context, callNode
|
|
453
|
-
return isInjectorReference(context, callNode.arguments?.[0]
|
|
435
|
+
function isDisallowedInjectInjector(context, callNode) {
|
|
436
|
+
if (!isInjectCall(context, callNode)) return false;
|
|
437
|
+
return isInjectorReference(context, callNode.arguments?.[0]);
|
|
454
438
|
}
|
|
455
|
-
function isDisallowedRunInInjectionContext(context, callNode
|
|
439
|
+
function isDisallowedRunInInjectionContext(context, callNode) {
|
|
456
440
|
const callee = callNode.callee;
|
|
457
|
-
|
|
458
|
-
if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && angularNamespaces.has(callee.object.name) && !isShadowedIdentifier(context, callee.object)) return RUN_IN_INJECTION_CONTEXT_NAMES.has(getPropertyName(callee.property) ?? "");
|
|
459
|
-
return false;
|
|
441
|
+
return isImportedReference(context, callee, "@angular/core", RUN_IN_INJECTION_CONTEXT_NAMES) || isImportedNamespaceMember(context, callee, "@angular/core", RUN_IN_INJECTION_CONTEXT_NAMES);
|
|
460
442
|
}
|
|
461
443
|
const avoidExplicitInjectionContext = defineRule({
|
|
462
444
|
meta: {
|
|
@@ -485,47 +467,20 @@ const avoidExplicitInjectionContext = defineRule({
|
|
|
485
467
|
}
|
|
486
468
|
},
|
|
487
469
|
createOnce(context) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (node.source?.value !== "@angular/core") return;
|
|
503
|
-
for (const specifier of node.specifiers ?? []) {
|
|
504
|
-
if (specifier.type === "ImportSpecifier") {
|
|
505
|
-
const importedName = getPropertyName(specifier.imported);
|
|
506
|
-
if (importedName === "inject") injectNames.add(specifier.local.name);
|
|
507
|
-
if (importedName === "Injector") injectorNames.add(specifier.local.name);
|
|
508
|
-
if (importedName === "runInInjectionContext") runInInjectionContextNames.add(specifier.local.name);
|
|
509
|
-
if (importedName === "runInContext") runInContextNames.add(specifier.local.name);
|
|
510
|
-
}
|
|
511
|
-
if (specifier.type === "ImportNamespaceSpecifier") angularNamespaces.add(specifier.local.name);
|
|
512
|
-
}
|
|
513
|
-
},
|
|
514
|
-
CallExpression(node) {
|
|
515
|
-
const callNode = node;
|
|
516
|
-
const options = context.options[0] ?? {};
|
|
517
|
-
const disallowInjectInjector = options.disallowInjectInjector ?? DEFAULT_DISALLOW_INJECT_INJECTOR;
|
|
518
|
-
const disallowRunInInjectionContext = options.disallowRunInInjectionContext ?? DEFAULT_DISALLOW_RUN_IN_INJECTION_CONTEXT;
|
|
519
|
-
if (disallowInjectInjector && isDisallowedInjectInjector(context, callNode, injectNames, injectorNames, angularNamespaces)) context.report({
|
|
520
|
-
node: callNode.callee,
|
|
521
|
-
messageId: "avoidInjectInjector"
|
|
522
|
-
});
|
|
523
|
-
if (disallowRunInInjectionContext && isDisallowedRunInInjectionContext(context, callNode, runInInjectionContextNames, runInContextNames, angularNamespaces)) context.report({
|
|
524
|
-
node: callNode.callee,
|
|
525
|
-
messageId: "avoidRunInInjectionContext"
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
};
|
|
470
|
+
return { CallExpression(node) {
|
|
471
|
+
const callNode = node;
|
|
472
|
+
const options = context.options[0] ?? {};
|
|
473
|
+
const disallowInjectInjector = options.disallowInjectInjector ?? DEFAULT_DISALLOW_INJECT_INJECTOR;
|
|
474
|
+
const disallowRunInInjectionContext = options.disallowRunInInjectionContext ?? DEFAULT_DISALLOW_RUN_IN_INJECTION_CONTEXT;
|
|
475
|
+
if (disallowInjectInjector && isDisallowedInjectInjector(context, callNode)) context.report({
|
|
476
|
+
node: callNode.callee,
|
|
477
|
+
messageId: "avoidInjectInjector"
|
|
478
|
+
});
|
|
479
|
+
if (disallowRunInInjectionContext && isDisallowedRunInInjectionContext(context, callNode)) context.report({
|
|
480
|
+
node: callNode.callee,
|
|
481
|
+
messageId: "avoidRunInInjectionContext"
|
|
482
|
+
});
|
|
483
|
+
} };
|
|
529
484
|
}
|
|
530
485
|
});
|
|
531
486
|
//#endregion
|
|
@@ -547,9 +502,9 @@ const RXJS_SUBSCRIBABLE_NAMES = new Set([
|
|
|
547
502
|
"ReplaySubject",
|
|
548
503
|
"Subject"
|
|
549
504
|
]);
|
|
550
|
-
function hasTargetDecorator$
|
|
505
|
+
function hasTargetDecorator$3(context, classNode) {
|
|
551
506
|
if (!classNode || !Array.isArray(classNode.decorators)) return false;
|
|
552
|
-
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator,
|
|
507
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$2));
|
|
553
508
|
}
|
|
554
509
|
function hasSubscriptionTypeReference(context, node, subscriptionLocalNames, rxjsNamespaces) {
|
|
555
510
|
if (!node) return false;
|
|
@@ -808,11 +763,6 @@ const avoidExplicitSubscriptionManagement = defineRule({
|
|
|
808
763
|
const rxjsNamespaces = /* @__PURE__ */ new Set();
|
|
809
764
|
const takeUntilDestroyedLocalNames = /* @__PURE__ */ new Set();
|
|
810
765
|
const interopNamespaces = /* @__PURE__ */ new Set();
|
|
811
|
-
const decoratorImports = {
|
|
812
|
-
decoratorNames: TARGET_DECORATORS$2,
|
|
813
|
-
decoratorLocalNames: /* @__PURE__ */ new Set(),
|
|
814
|
-
angularNamespaces: /* @__PURE__ */ new Set()
|
|
815
|
-
};
|
|
816
766
|
return {
|
|
817
767
|
before() {
|
|
818
768
|
subscriptionLocalNames.clear();
|
|
@@ -820,12 +770,9 @@ const avoidExplicitSubscriptionManagement = defineRule({
|
|
|
820
770
|
rxjsNamespaces.clear();
|
|
821
771
|
takeUntilDestroyedLocalNames.clear();
|
|
822
772
|
interopNamespaces.clear();
|
|
823
|
-
decoratorImports.decoratorLocalNames.clear();
|
|
824
|
-
decoratorImports.angularNamespaces.clear();
|
|
825
773
|
},
|
|
826
774
|
ImportDeclaration(node) {
|
|
827
775
|
const source = node.source?.value;
|
|
828
|
-
if (source === "@angular/core") for (const specifier of node.specifiers ?? []) addAngularCoreDecoratorImport(specifier, TARGET_DECORATORS$2, decoratorImports);
|
|
829
776
|
if (source === "rxjs") for (const specifier of node.specifiers ?? []) {
|
|
830
777
|
if (specifier.type === "ImportSpecifier") {
|
|
831
778
|
const importedName = getPropertyName(specifier.imported);
|
|
@@ -846,7 +793,7 @@ const avoidExplicitSubscriptionManagement = defineRule({
|
|
|
846
793
|
ClassBody(node) {
|
|
847
794
|
const classBody = node;
|
|
848
795
|
const classNode = classBody.parent;
|
|
849
|
-
if (!classNode || !hasTargetDecorator$
|
|
796
|
+
if (!classNode || !hasTargetDecorator$3(context, classNode)) return;
|
|
850
797
|
const rxjsSubscribableReferences = collectRxjsSubscribableReferences(context, classBody, classNode, rxjsSubscribableLocalNames, rxjsNamespaces);
|
|
851
798
|
const subscriptionReferences = collectSubscriptionReferences(context, classBody, classNode, subscriptionLocalNames, rxjsNamespaces, rxjsSubscribableReferences, rxjsSubscribableLocalNames, takeUntilDestroyedLocalNames, interopNamespaces);
|
|
852
799
|
walkNode$1(classBody, classNode, (current) => {
|
|
@@ -902,18 +849,15 @@ const avoidExplicitSubscriptionManagement = defineRule({
|
|
|
902
849
|
const DEFAULT_ALLOW_FOR_GROUPING = true;
|
|
903
850
|
const DEFAULT_ALLOW_FOR_PROVIDING = false;
|
|
904
851
|
const DEFAULT_ALLOW_FOR_ROUTING = false;
|
|
905
|
-
function isNgModuleDecorator(context, node
|
|
852
|
+
function isNgModuleDecorator(context, node) {
|
|
906
853
|
if (node.type !== "Decorator") return false;
|
|
907
854
|
const expression = node.expression ?? node;
|
|
908
855
|
const callee = expression?.type === "CallExpression" ? expression.callee : expression;
|
|
909
|
-
if (callee?.type === "Identifier")
|
|
910
|
-
|
|
911
|
-
return !!binding && binding.source === "@angular/core" && binding.importedName === "NgModule" && !isShadowedIdentifier(context, callee);
|
|
912
|
-
}
|
|
913
|
-
return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && namespaceImportBindings.get(callee.object.name) === "@angular/core" && !isShadowedIdentifier(context, callee.object) && getPropertyName(callee.property) === "NgModule";
|
|
856
|
+
if (callee?.type === "Identifier") return getImportedName(context, callee, "@angular/core") === "NgModule";
|
|
857
|
+
return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && isNamespaceImport(context, callee.object, "@angular/core") && getPropertyName(callee.property) === "NgModule";
|
|
914
858
|
}
|
|
915
|
-
function getNgModuleMetadata(context, node
|
|
916
|
-
if (!isNgModuleDecorator(context, node
|
|
859
|
+
function getNgModuleMetadata(context, node) {
|
|
860
|
+
if (!isNgModuleDecorator(context, node)) return null;
|
|
917
861
|
const expression = node.expression;
|
|
918
862
|
if (expression?.type !== "CallExpression") return null;
|
|
919
863
|
const metadata = expression.arguments?.[0];
|
|
@@ -927,18 +871,15 @@ function getArrayElementsFromProperty(metadata, propertyName) {
|
|
|
927
871
|
function isMemberCall(node, methodName) {
|
|
928
872
|
return node.type === "CallExpression" && getPropertyName(node.callee?.property) === methodName;
|
|
929
873
|
}
|
|
930
|
-
function isModuleCallFromImport(node, expectedSource, expectedModuleName
|
|
874
|
+
function isModuleCallFromImport(context, node, expectedSource, expectedModuleName) {
|
|
931
875
|
if (node.type !== "CallExpression") return false;
|
|
932
876
|
const callee = node.callee;
|
|
933
877
|
if (callee?.type !== "MemberExpression") return false;
|
|
934
878
|
const moduleRef = callee.object;
|
|
935
|
-
if (moduleRef?.type === "Identifier")
|
|
936
|
-
const binding = importBindings.get(moduleRef.name);
|
|
937
|
-
return !!binding && binding.source === expectedSource && binding.importedName === expectedModuleName;
|
|
938
|
-
}
|
|
879
|
+
if (moduleRef?.type === "Identifier") return getImportedName(context, moduleRef, expectedSource) === expectedModuleName;
|
|
939
880
|
if (moduleRef?.type !== "MemberExpression") return false;
|
|
940
881
|
if (moduleRef.object?.type !== "Identifier") return false;
|
|
941
|
-
if (
|
|
882
|
+
if (!isNamespaceImport(context, moduleRef.object, expectedSource)) return false;
|
|
942
883
|
return getPropertyName(moduleRef.property) === expectedModuleName;
|
|
943
884
|
}
|
|
944
885
|
function getNgModuleClassNode(decoratorNode) {
|
|
@@ -989,118 +930,94 @@ const avoidNgModules = defineRule({
|
|
|
989
930
|
}
|
|
990
931
|
},
|
|
991
932
|
createOnce(context) {
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
933
|
+
return { Decorator(node) {
|
|
934
|
+
const options = context.options[0] ?? {};
|
|
935
|
+
const allowForGrouping = options.allowForGrouping ?? DEFAULT_ALLOW_FOR_GROUPING;
|
|
936
|
+
const allowForProviding = options.allowForProviding ?? DEFAULT_ALLOW_FOR_PROVIDING;
|
|
937
|
+
const allowForRouting = options.allowForRouting ?? DEFAULT_ALLOW_FOR_ROUTING;
|
|
938
|
+
const metadata = getNgModuleMetadata(context, node);
|
|
939
|
+
if (!metadata) return;
|
|
940
|
+
const importsElements = getArrayElementsFromProperty(metadata, "imports");
|
|
941
|
+
const exportsElements = getArrayElementsFromProperty(metadata, "exports");
|
|
942
|
+
if (!allowForGrouping && (importsElements.length > 0 || exportsElements.length > 0)) context.report({
|
|
943
|
+
node,
|
|
944
|
+
messageId: "avoidModuleImportsExports"
|
|
945
|
+
});
|
|
946
|
+
const importCalls = importsElements.filter((element) => element.type === "CallExpression");
|
|
947
|
+
if (!allowForProviding) {
|
|
948
|
+
for (const call of importCalls) {
|
|
949
|
+
const methodName = getPropertyName(call.callee?.property);
|
|
950
|
+
if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@angular/router", "RouterModule") && !allowForRouting) {
|
|
951
|
+
context.report({
|
|
952
|
+
node: call,
|
|
953
|
+
messageId: "avoidRouterForRoot"
|
|
1009
954
|
});
|
|
1010
955
|
continue;
|
|
1011
956
|
}
|
|
1012
|
-
if (
|
|
1013
|
-
}
|
|
1014
|
-
},
|
|
1015
|
-
Decorator(node) {
|
|
1016
|
-
const options = context.options[0] ?? {};
|
|
1017
|
-
const allowForGrouping = options.allowForGrouping ?? DEFAULT_ALLOW_FOR_GROUPING;
|
|
1018
|
-
const allowForProviding = options.allowForProviding ?? DEFAULT_ALLOW_FOR_PROVIDING;
|
|
1019
|
-
const allowForRouting = options.allowForRouting ?? DEFAULT_ALLOW_FOR_ROUTING;
|
|
1020
|
-
const metadata = getNgModuleMetadata(context, node, importBindings, namespaceImportBindings);
|
|
1021
|
-
if (!metadata) return;
|
|
1022
|
-
const importsElements = getArrayElementsFromProperty(metadata, "imports");
|
|
1023
|
-
const exportsElements = getArrayElementsFromProperty(metadata, "exports");
|
|
1024
|
-
if (!allowForGrouping && (importsElements.length > 0 || exportsElements.length > 0)) context.report({
|
|
1025
|
-
node,
|
|
1026
|
-
messageId: "avoidModuleImportsExports"
|
|
1027
|
-
});
|
|
1028
|
-
const importCalls = importsElements.filter((element) => element.type === "CallExpression");
|
|
1029
|
-
if (!allowForProviding) {
|
|
1030
|
-
for (const call of importCalls) {
|
|
1031
|
-
const methodName = getPropertyName(call.callee?.property);
|
|
1032
|
-
if (methodName === "forRoot" && isModuleCallFromImport(call, "@angular/router", "RouterModule", importBindings, namespaceImportBindings) && !allowForRouting) {
|
|
1033
|
-
context.report({
|
|
1034
|
-
node: call,
|
|
1035
|
-
messageId: "avoidRouterForRoot"
|
|
1036
|
-
});
|
|
1037
|
-
continue;
|
|
1038
|
-
}
|
|
1039
|
-
if (methodName === "forRoot" && isModuleCallFromImport(call, "@ngrx/store", "StoreModule", importBindings, namespaceImportBindings)) {
|
|
1040
|
-
context.report({
|
|
1041
|
-
node: call,
|
|
1042
|
-
messageId: "avoidNgrxStoreForRoot"
|
|
1043
|
-
});
|
|
1044
|
-
continue;
|
|
1045
|
-
}
|
|
1046
|
-
if (methodName === "forRoot" && isModuleCallFromImport(call, "@ngrx/effects", "EffectsModule", importBindings, namespaceImportBindings)) {
|
|
1047
|
-
context.report({
|
|
1048
|
-
node: call,
|
|
1049
|
-
messageId: "avoidNgrxEffectsForRoot"
|
|
1050
|
-
});
|
|
1051
|
-
continue;
|
|
1052
|
-
}
|
|
1053
|
-
if (methodName === "forFeature" && isModuleCallFromImport(call, "@ngrx/store", "StoreModule", importBindings, namespaceImportBindings)) {
|
|
1054
|
-
context.report({
|
|
1055
|
-
node: call,
|
|
1056
|
-
messageId: "avoidNgrxStoreForFeature"
|
|
1057
|
-
});
|
|
1058
|
-
continue;
|
|
1059
|
-
}
|
|
1060
|
-
if (methodName === "forFeature" && isModuleCallFromImport(call, "@ngrx/effects", "EffectsModule", importBindings, namespaceImportBindings)) {
|
|
1061
|
-
context.report({
|
|
1062
|
-
node: call,
|
|
1063
|
-
messageId: "avoidNgrxEffectsForFeature"
|
|
1064
|
-
});
|
|
1065
|
-
continue;
|
|
1066
|
-
}
|
|
1067
|
-
if (methodName === "instrument" && isModuleCallFromImport(call, "@ngrx/store-devtools", "StoreDevtoolsModule", importBindings, namespaceImportBindings)) {
|
|
1068
|
-
context.report({
|
|
1069
|
-
node: call,
|
|
1070
|
-
messageId: "avoidNgrxStoreDevtoolsInstrument"
|
|
1071
|
-
});
|
|
1072
|
-
continue;
|
|
1073
|
-
}
|
|
1074
|
-
if (methodName === "forRoot" && isModuleCallFromImport(call, "@ngrx/router-store", "StoreRouterConnectingModule", importBindings, namespaceImportBindings)) {
|
|
1075
|
-
context.report({
|
|
1076
|
-
node: call,
|
|
1077
|
-
messageId: "avoidNgrxRouterStoreForRoot"
|
|
1078
|
-
});
|
|
1079
|
-
continue;
|
|
1080
|
-
}
|
|
1081
|
-
if (methodName === "forRoot" && isModuleCallFromImport(call, "@angular/router", "RouterModule", importBindings, namespaceImportBindings) && allowForRouting) continue;
|
|
1082
|
-
if (!isMemberCall(call, "forRoot")) continue;
|
|
957
|
+
if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@ngrx/store", "StoreModule")) {
|
|
1083
958
|
context.report({
|
|
1084
959
|
node: call,
|
|
1085
|
-
messageId: "
|
|
960
|
+
messageId: "avoidNgrxStoreForRoot"
|
|
1086
961
|
});
|
|
962
|
+
continue;
|
|
1087
963
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
if (
|
|
1096
|
-
|
|
964
|
+
if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@ngrx/effects", "EffectsModule")) {
|
|
965
|
+
context.report({
|
|
966
|
+
node: call,
|
|
967
|
+
messageId: "avoidNgrxEffectsForRoot"
|
|
968
|
+
});
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
if (methodName === "forFeature" && isModuleCallFromImport(context, call, "@ngrx/store", "StoreModule")) {
|
|
972
|
+
context.report({
|
|
973
|
+
node: call,
|
|
974
|
+
messageId: "avoidNgrxStoreForFeature"
|
|
975
|
+
});
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
if (methodName === "forFeature" && isModuleCallFromImport(context, call, "@ngrx/effects", "EffectsModule")) {
|
|
979
|
+
context.report({
|
|
980
|
+
node: call,
|
|
981
|
+
messageId: "avoidNgrxEffectsForFeature"
|
|
982
|
+
});
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
if (methodName === "instrument" && isModuleCallFromImport(context, call, "@ngrx/store-devtools", "StoreDevtoolsModule")) {
|
|
986
|
+
context.report({
|
|
987
|
+
node: call,
|
|
988
|
+
messageId: "avoidNgrxStoreDevtoolsInstrument"
|
|
989
|
+
});
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@ngrx/router-store", "StoreRouterConnectingModule")) {
|
|
993
|
+
context.report({
|
|
994
|
+
node: call,
|
|
995
|
+
messageId: "avoidNgrxRouterStoreForRoot"
|
|
996
|
+
});
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@angular/router", "RouterModule") && allowForRouting) continue;
|
|
1000
|
+
if (!isMemberCall(call, "forRoot")) continue;
|
|
1097
1001
|
context.report({
|
|
1098
1002
|
node: call,
|
|
1099
|
-
messageId: "
|
|
1003
|
+
messageId: "avoidForRoot"
|
|
1100
1004
|
});
|
|
1101
1005
|
}
|
|
1006
|
+
const staticForRootMethod = getStaticForRootMethod(getNgModuleClassNode(node));
|
|
1007
|
+
if (staticForRootMethod) context.report({
|
|
1008
|
+
node: staticForRootMethod.key ?? staticForRootMethod,
|
|
1009
|
+
messageId: "avoidForRoot"
|
|
1010
|
+
});
|
|
1102
1011
|
}
|
|
1103
|
-
|
|
1012
|
+
if (!allowForRouting) for (const call of importCalls) {
|
|
1013
|
+
if (!isMemberCall(call, "forChild")) continue;
|
|
1014
|
+
if (!isModuleCallFromImport(context, call, "@angular/router", "RouterModule")) continue;
|
|
1015
|
+
context.report({
|
|
1016
|
+
node: call,
|
|
1017
|
+
messageId: "avoidRouterForChild"
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
} };
|
|
1104
1021
|
}
|
|
1105
1022
|
});
|
|
1106
1023
|
//#endregion
|
|
@@ -1116,37 +1033,31 @@ const FIELD_NODE_TYPES$1 = new Set([
|
|
|
1116
1033
|
"FieldDefinition",
|
|
1117
1034
|
"PropertyDefinition"
|
|
1118
1035
|
]);
|
|
1119
|
-
function hasTargetDecorator$
|
|
1036
|
+
function hasTargetDecorator$2(context, classNode) {
|
|
1120
1037
|
if (!classNode || !Array.isArray(classNode.decorators)) return false;
|
|
1121
|
-
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator,
|
|
1038
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$1));
|
|
1122
1039
|
}
|
|
1123
|
-
function getImportedSubjectKind(
|
|
1124
|
-
|
|
1040
|
+
function getImportedSubjectKind(context, node) {
|
|
1041
|
+
const importedName = getImportedName(context, node, "rxjs");
|
|
1042
|
+
return SUBJECT_NAMES.has(importedName) ? importedName : null;
|
|
1125
1043
|
}
|
|
1126
|
-
function getSubjectKindFromType(context, node
|
|
1044
|
+
function getSubjectKindFromType(context, node) {
|
|
1127
1045
|
if (!node) return null;
|
|
1128
1046
|
if (node.type !== "TSTypeReference") return null;
|
|
1129
|
-
|
|
1130
|
-
if (
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
if (!rxjsNamespaces.has(namespaceName)) return null;
|
|
1136
|
-
if (node.typeName?.type === "TSQualifiedName" && node.typeName.left?.type === "Identifier" && isShadowedIdentifier(context, node.typeName.left)) return null;
|
|
1137
|
-
return SUBJECT_NAMES.has(memberName) ? memberName : null;
|
|
1047
|
+
if (node.typeName?.type === "Identifier") return getImportedSubjectKind(context, node.typeName);
|
|
1048
|
+
if (node.typeName?.type === "TSQualifiedName" && node.typeName.left?.type === "Identifier") {
|
|
1049
|
+
const memberName = getPropertyName(node.typeName.right);
|
|
1050
|
+
return isNamespaceImport(context, node.typeName.left, "rxjs") && SUBJECT_NAMES.has(memberName) ? memberName : null;
|
|
1051
|
+
}
|
|
1052
|
+
return null;
|
|
1138
1053
|
}
|
|
1139
|
-
function getSubjectKindFromConstructor(context, node
|
|
1054
|
+
function getSubjectKindFromConstructor(context, node) {
|
|
1140
1055
|
const expression = unwrapExpression(node);
|
|
1141
1056
|
if (expression?.type !== "NewExpression") return null;
|
|
1142
1057
|
const callee = unwrapExpression(expression.callee);
|
|
1143
|
-
if (callee?.type === "Identifier")
|
|
1144
|
-
if (isShadowedIdentifier(context, callee)) return null;
|
|
1145
|
-
return getImportedSubjectKind(callee.name, subjectLocalNames);
|
|
1146
|
-
}
|
|
1058
|
+
if (callee?.type === "Identifier") return getImportedSubjectKind(context, callee);
|
|
1147
1059
|
if (callee?.type !== "MemberExpression") return null;
|
|
1148
|
-
if (callee.object?.type !== "Identifier" || !
|
|
1149
|
-
if (isShadowedIdentifier(context, callee.object)) return null;
|
|
1060
|
+
if (callee.object?.type !== "Identifier" || !isNamespaceImport(context, callee.object, "rxjs")) return null;
|
|
1150
1061
|
const memberName = getPropertyName(callee.property);
|
|
1151
1062
|
return SUBJECT_NAMES.has(memberName) ? memberName : null;
|
|
1152
1063
|
}
|
|
@@ -1205,14 +1116,14 @@ function getUsage(usages, fieldName) {
|
|
|
1205
1116
|
usages.set(fieldName, next);
|
|
1206
1117
|
return next;
|
|
1207
1118
|
}
|
|
1208
|
-
function collectSubjectFields(context, classBody
|
|
1119
|
+
function collectSubjectFields(context, classBody) {
|
|
1209
1120
|
const fields = /* @__PURE__ */ new Map();
|
|
1210
1121
|
for (const member of classBody.body ?? []) {
|
|
1211
1122
|
if (!FIELD_NODE_TYPES$1.has(member.type)) continue;
|
|
1212
1123
|
const fieldName = getMemberName(member.key);
|
|
1213
1124
|
if (!fieldName) continue;
|
|
1214
1125
|
const typeNode = member.typeAnnotation?.typeAnnotation;
|
|
1215
|
-
const kind = getSubjectKindFromConstructor(context, member.value
|
|
1126
|
+
const kind = getSubjectKindFromConstructor(context, member.value) ?? getSubjectKindFromType(context, typeNode);
|
|
1216
1127
|
if (!kind) continue;
|
|
1217
1128
|
fields.set(fieldName, {
|
|
1218
1129
|
key: member.key ?? member,
|
|
@@ -1260,58 +1171,28 @@ const avoidRxjsStateInComponent = defineRule({
|
|
|
1260
1171
|
}
|
|
1261
1172
|
},
|
|
1262
1173
|
createOnce(context) {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
subjectLocalNames.clear();
|
|
1273
|
-
rxjsNamespaces.clear();
|
|
1274
|
-
decoratorImports.decoratorLocalNames.clear();
|
|
1275
|
-
decoratorImports.angularNamespaces.clear();
|
|
1276
|
-
},
|
|
1277
|
-
ImportDeclaration(node) {
|
|
1278
|
-
if (node.source?.value === "@angular/core") {
|
|
1279
|
-
for (const specifier of node.specifiers ?? []) addAngularCoreDecoratorImport(specifier, TARGET_DECORATORS$1, decoratorImports);
|
|
1280
|
-
return;
|
|
1281
|
-
}
|
|
1282
|
-
if (node.source?.value !== "rxjs") return;
|
|
1283
|
-
for (const specifier of node.specifiers ?? []) {
|
|
1284
|
-
if (specifier.type === "ImportSpecifier") {
|
|
1285
|
-
const importedName = getPropertyName(specifier.imported);
|
|
1286
|
-
if (SUBJECT_NAMES.has(importedName)) subjectLocalNames.set(specifier.local.name, importedName);
|
|
1287
|
-
continue;
|
|
1288
|
-
}
|
|
1289
|
-
if (specifier.type === "ImportNamespaceSpecifier") rxjsNamespaces.add(specifier.local.name);
|
|
1290
|
-
}
|
|
1291
|
-
},
|
|
1292
|
-
ClassBody(node) {
|
|
1293
|
-
const classBody = node;
|
|
1294
|
-
const classNode = classBody.parent;
|
|
1295
|
-
if (!hasTargetDecorator$1(context, classNode, decoratorImports)) return;
|
|
1296
|
-
const fields = collectSubjectFields(context, classBody, subjectLocalNames, rxjsNamespaces);
|
|
1297
|
-
const usages = collectFieldUsages(classBody, fields);
|
|
1298
|
-
for (const [fieldName, field] of fields) {
|
|
1299
|
-
const usage = usages.get(fieldName);
|
|
1300
|
-
if (field.kind === "Subject" && isDestroySubjectUsage(usage)) {
|
|
1301
|
-
context.report({
|
|
1302
|
-
node: field.key,
|
|
1303
|
-
messageId: "avoidDestroySubject"
|
|
1304
|
-
});
|
|
1305
|
-
continue;
|
|
1306
|
-
}
|
|
1174
|
+
return { ClassBody(node) {
|
|
1175
|
+
const classBody = node;
|
|
1176
|
+
const classNode = classBody.parent;
|
|
1177
|
+
if (!hasTargetDecorator$2(context, classNode)) return;
|
|
1178
|
+
const fields = collectSubjectFields(context, classBody);
|
|
1179
|
+
const usages = collectFieldUsages(classBody, fields);
|
|
1180
|
+
for (const [fieldName, field] of fields) {
|
|
1181
|
+
const usage = usages.get(fieldName);
|
|
1182
|
+
if (field.kind === "Subject" && isDestroySubjectUsage(usage)) {
|
|
1307
1183
|
context.report({
|
|
1308
1184
|
node: field.key,
|
|
1309
|
-
messageId: "
|
|
1310
|
-
data: { kind: field.kind }
|
|
1185
|
+
messageId: "avoidDestroySubject"
|
|
1311
1186
|
});
|
|
1187
|
+
continue;
|
|
1312
1188
|
}
|
|
1189
|
+
context.report({
|
|
1190
|
+
node: field.key,
|
|
1191
|
+
messageId: "avoidRxjsState",
|
|
1192
|
+
data: { kind: field.kind }
|
|
1193
|
+
});
|
|
1313
1194
|
}
|
|
1314
|
-
};
|
|
1195
|
+
} };
|
|
1315
1196
|
}
|
|
1316
1197
|
});
|
|
1317
1198
|
//#endregion
|
|
@@ -1328,22 +1209,21 @@ const KNOWN_SIGNAL_CREATION_FUNCTIONS = new Set([
|
|
|
1328
1209
|
]);
|
|
1329
1210
|
const LINKED_SIGNAL_CREATOR_NAME = "linkedSignal";
|
|
1330
1211
|
const COMPUTED_CREATOR_NAME = "computed";
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
function isSignalCreatorCall(context, node, signalCreatorNames, angularNamespaces) {
|
|
1212
|
+
const EFFECT_CREATOR_NAMES = new Set(["effect"]);
|
|
1213
|
+
const COMPUTED_CREATOR_NAMES = new Set([COMPUTED_CREATOR_NAME]);
|
|
1214
|
+
const LINKED_SIGNAL_CREATOR_NAMES = new Set([LINKED_SIGNAL_CREATOR_NAME]);
|
|
1215
|
+
function isSignalCreatorCall(context, node) {
|
|
1336
1216
|
if (node?.type !== "CallExpression") return false;
|
|
1337
1217
|
const callee = node.callee;
|
|
1338
|
-
return
|
|
1218
|
+
return isImportedReference(context, callee, "@angular/core", KNOWN_SIGNAL_CREATION_FUNCTIONS) || isImportedNamespaceMember(context, callee, "@angular/core", KNOWN_SIGNAL_CREATION_FUNCTIONS);
|
|
1339
1219
|
}
|
|
1340
|
-
function isEffectCall(context, node
|
|
1220
|
+
function isEffectCall(context, node) {
|
|
1341
1221
|
const callee = node.callee;
|
|
1342
|
-
return callee
|
|
1222
|
+
return isImportedReference(context, callee, "@angular/core", EFFECT_CREATOR_NAMES) || isImportedNamespaceMember(context, callee, "@angular/core", EFFECT_CREATOR_NAMES);
|
|
1343
1223
|
}
|
|
1344
|
-
function isReactiveCreatorCall(context, node, creatorNames
|
|
1224
|
+
function isReactiveCreatorCall(context, node, creatorNames) {
|
|
1345
1225
|
const callee = node.callee;
|
|
1346
|
-
return callee
|
|
1226
|
+
return isImportedReference(context, callee, "@angular/core", creatorNames) || isImportedNamespaceMember(context, callee, "@angular/core", creatorNames);
|
|
1347
1227
|
}
|
|
1348
1228
|
function isKnownSignalObject(context, objectNode, signalVariableBindings, classSignalProperties) {
|
|
1349
1229
|
if (!objectNode) return false;
|
|
@@ -1398,40 +1278,17 @@ const avoidWritingSignalsInReactiveContext = defineRule({
|
|
|
1398
1278
|
messages: { avoidSignalWriteInReactiveContext: "Avoid setting signal values inside {{contextName}}; move writes outside reactive derivations and effects." }
|
|
1399
1279
|
},
|
|
1400
1280
|
createOnce(context) {
|
|
1401
|
-
const effectNames = /* @__PURE__ */ new Set();
|
|
1402
|
-
const computedNames = /* @__PURE__ */ new Set();
|
|
1403
|
-
const linkedSignalNames = /* @__PURE__ */ new Set();
|
|
1404
|
-
const signalCreatorNames = /* @__PURE__ */ new Set();
|
|
1405
|
-
const angularNamespaces = /* @__PURE__ */ new Set();
|
|
1406
1281
|
const signalVariableBindings = /* @__PURE__ */ new Map();
|
|
1407
1282
|
const classSignalProperties = /* @__PURE__ */ new Set();
|
|
1408
1283
|
return {
|
|
1409
1284
|
before() {
|
|
1410
|
-
effectNames.clear();
|
|
1411
|
-
computedNames.clear();
|
|
1412
|
-
linkedSignalNames.clear();
|
|
1413
|
-
signalCreatorNames.clear();
|
|
1414
|
-
angularNamespaces.clear();
|
|
1415
1285
|
signalVariableBindings.clear();
|
|
1416
1286
|
classSignalProperties.clear();
|
|
1417
1287
|
},
|
|
1418
|
-
ImportDeclaration(node) {
|
|
1419
|
-
if (node.source?.value !== "@angular/core") return;
|
|
1420
|
-
for (const specifier of node.specifiers ?? []) {
|
|
1421
|
-
if (specifier.type === "ImportSpecifier") {
|
|
1422
|
-
const importedName = getPropertyName(specifier.imported);
|
|
1423
|
-
if (importedName === EFFECT_CREATOR_NAME) effectNames.add(specifier.local.name);
|
|
1424
|
-
if (importedName === COMPUTED_CREATOR_NAME) computedNames.add(specifier.local.name);
|
|
1425
|
-
if (importedName === LINKED_SIGNAL_CREATOR_NAME) linkedSignalNames.add(specifier.local.name);
|
|
1426
|
-
if (importedName && KNOWN_SIGNAL_CREATION_FUNCTIONS.has(importedName)) signalCreatorNames.add(specifier.local.name);
|
|
1427
|
-
}
|
|
1428
|
-
if (specifier.type === "ImportNamespaceSpecifier") angularNamespaces.add(specifier.local.name);
|
|
1429
|
-
}
|
|
1430
|
-
},
|
|
1431
1288
|
VariableDeclarator(node) {
|
|
1432
1289
|
const declarator = node;
|
|
1433
1290
|
if (declarator.id?.type !== "Identifier") return;
|
|
1434
|
-
if (!isSignalCreatorCall(context, declarator.init
|
|
1291
|
+
if (!isSignalCreatorCall(context, declarator.init)) return;
|
|
1435
1292
|
const bindings = signalVariableBindings.get(declarator.id.name) ?? /* @__PURE__ */ new Set();
|
|
1436
1293
|
bindings.add(declarator.id);
|
|
1437
1294
|
signalVariableBindings.set(declarator.id.name, bindings);
|
|
@@ -1439,7 +1296,7 @@ const avoidWritingSignalsInReactiveContext = defineRule({
|
|
|
1439
1296
|
"PropertyDefinition, FieldDefinition, AccessorProperty"(node) {
|
|
1440
1297
|
const property = node;
|
|
1441
1298
|
if (property.key?.type !== "Identifier") return;
|
|
1442
|
-
if (!isSignalCreatorCall(context, property.value
|
|
1299
|
+
if (!isSignalCreatorCall(context, property.value)) return;
|
|
1443
1300
|
classSignalProperties.add(property.key.name);
|
|
1444
1301
|
},
|
|
1445
1302
|
CallExpression(node) {
|
|
@@ -1448,21 +1305,21 @@ const avoidWritingSignalsInReactiveContext = defineRule({
|
|
|
1448
1305
|
const allowComputedAndLinkedSignals = options.allowComputedAndLinkedSignals ?? false;
|
|
1449
1306
|
const callNode = node;
|
|
1450
1307
|
const callbackCandidates = [];
|
|
1451
|
-
if (!allowEffects && isEffectCall(context, callNode
|
|
1308
|
+
if (!allowEffects && isEffectCall(context, callNode)) {
|
|
1452
1309
|
const callback = callNode.arguments?.[0];
|
|
1453
1310
|
if (callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression") callbackCandidates.push({
|
|
1454
1311
|
callback,
|
|
1455
1312
|
contextName: "effect()"
|
|
1456
1313
|
});
|
|
1457
1314
|
}
|
|
1458
|
-
if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode,
|
|
1315
|
+
if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode, COMPUTED_CREATOR_NAMES)) {
|
|
1459
1316
|
const callback = callNode.arguments?.[0];
|
|
1460
1317
|
if (callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression") callbackCandidates.push({
|
|
1461
1318
|
callback,
|
|
1462
1319
|
contextName: "computed()"
|
|
1463
1320
|
});
|
|
1464
1321
|
}
|
|
1465
|
-
if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode,
|
|
1322
|
+
if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode, LINKED_SIGNAL_CREATOR_NAMES)) for (const argumentNode of callNode.arguments ?? []) {
|
|
1466
1323
|
const argument = argumentNode;
|
|
1467
1324
|
if (argument.type === "ArrowFunctionExpression" || argument.type === "FunctionExpression") {
|
|
1468
1325
|
callbackCandidates.push({
|
|
@@ -1498,11 +1355,6 @@ const avoidWritingSignalsInReactiveContext = defineRule({
|
|
|
1498
1355
|
}, true);
|
|
1499
1356
|
},
|
|
1500
1357
|
after() {
|
|
1501
|
-
effectNames.clear();
|
|
1502
|
-
computedNames.clear();
|
|
1503
|
-
linkedSignalNames.clear();
|
|
1504
|
-
signalCreatorNames.clear();
|
|
1505
|
-
angularNamespaces.clear();
|
|
1506
1358
|
signalVariableBindings.clear();
|
|
1507
1359
|
classSignalProperties.clear();
|
|
1508
1360
|
}
|
|
@@ -1510,64 +1362,145 @@ const avoidWritingSignalsInReactiveContext = defineRule({
|
|
|
1510
1362
|
}
|
|
1511
1363
|
});
|
|
1512
1364
|
//#endregion
|
|
1513
|
-
//#region src/rules/
|
|
1514
|
-
const
|
|
1365
|
+
//#region src/rules/class-matches-filename/index.ts
|
|
1366
|
+
const CLASS_FILENAME_MATCHERS = [
|
|
1367
|
+
{
|
|
1368
|
+
pattern: /^(.*)\.component\.ts$/u,
|
|
1369
|
+
decoratorNames: ["Component"]
|
|
1370
|
+
},
|
|
1371
|
+
{
|
|
1372
|
+
pattern: /^(.*)\.directive\.ts$/u,
|
|
1373
|
+
decoratorNames: ["Directive"]
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
pattern: /^(.*)\.service\.ts$/u,
|
|
1377
|
+
decoratorNames: ["Service", "Injectable"]
|
|
1378
|
+
}
|
|
1379
|
+
];
|
|
1515
1380
|
function toPascalCase(raw) {
|
|
1516
1381
|
return raw.split(/[-_\s.]+/u).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1517
1382
|
}
|
|
1518
|
-
function
|
|
1383
|
+
function getClassKind(suffix) {
|
|
1384
|
+
return suffix.toLowerCase();
|
|
1385
|
+
}
|
|
1386
|
+
function getExpectedClassNames(filename, ignoreClassSuffix) {
|
|
1519
1387
|
const base = filename.split(/[/\\]/u).at(-1) ?? "";
|
|
1520
|
-
const
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1388
|
+
for (const { decoratorNames, pattern } of CLASS_FILENAME_MATCHERS) {
|
|
1389
|
+
const match = pattern.exec(base);
|
|
1390
|
+
if (!match) continue;
|
|
1391
|
+
const stem = match[1];
|
|
1392
|
+
const pascal = toPascalCase(stem);
|
|
1393
|
+
if (!pascal) return null;
|
|
1394
|
+
const suffix = decoratorNames[0];
|
|
1395
|
+
return ignoreClassSuffix ? [`${pascal}${suffix}`, pascal] : [`${pascal}${suffix}`];
|
|
1396
|
+
}
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1399
|
+
function getFileMatcher(filename) {
|
|
1400
|
+
const base = filename.split(/[/\\]/u).at(-1) ?? "";
|
|
1401
|
+
return CLASS_FILENAME_MATCHERS.find(({ pattern }) => pattern.test(base)) ?? null;
|
|
1402
|
+
}
|
|
1403
|
+
function getIgnoreClassSuffixOption(options, kind) {
|
|
1404
|
+
const ignoreClassSuffix = options.ignoreClassSuffix ?? false;
|
|
1405
|
+
if (typeof ignoreClassSuffix === "boolean") return ignoreClassSuffix;
|
|
1406
|
+
return ignoreClassSuffix[kind] ?? false;
|
|
1407
|
+
}
|
|
1408
|
+
function getFileKind(matcher) {
|
|
1409
|
+
return getClassKind(matcher.decoratorNames[0]);
|
|
1525
1410
|
}
|
|
1526
|
-
|
|
1411
|
+
function formatExpectedNames(expectedNames) {
|
|
1412
|
+
return expectedNames.join("' or '");
|
|
1413
|
+
}
|
|
1414
|
+
function getNodeName(node) {
|
|
1415
|
+
return node.id?.name ?? null;
|
|
1416
|
+
}
|
|
1417
|
+
function getBaseFilename$1(filename) {
|
|
1418
|
+
return filename.split(/[/\\]/u).at(-1) ?? filename;
|
|
1419
|
+
}
|
|
1420
|
+
function hasTargetDecorator$1(context, classNode, decoratorNames) {
|
|
1421
|
+
if (!Array.isArray(classNode.decorators)) return false;
|
|
1422
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorNames));
|
|
1423
|
+
}
|
|
1424
|
+
function getDecoratedClassCountDescription(count) {
|
|
1425
|
+
return count === 0 ? "no decorated classes" : `${count} decorated classes`;
|
|
1426
|
+
}
|
|
1427
|
+
const classMatchesFilename = defineRule({
|
|
1527
1428
|
meta: {
|
|
1528
1429
|
type: "suggestion",
|
|
1529
1430
|
docs: {
|
|
1530
|
-
description: "Require
|
|
1431
|
+
description: "Require Angular component, directive, and service class names to match their filenames.",
|
|
1531
1432
|
recommended: true
|
|
1532
1433
|
},
|
|
1533
|
-
schema: [
|
|
1534
|
-
|
|
1434
|
+
schema: [{
|
|
1435
|
+
type: "object",
|
|
1436
|
+
additionalProperties: false,
|
|
1437
|
+
properties: { ignoreClassSuffix: {
|
|
1438
|
+
anyOf: [{ type: "boolean" }, {
|
|
1439
|
+
type: "object",
|
|
1440
|
+
additionalProperties: false,
|
|
1441
|
+
properties: {
|
|
1442
|
+
component: { type: "boolean" },
|
|
1443
|
+
directive: { type: "boolean" },
|
|
1444
|
+
service: { type: "boolean" }
|
|
1445
|
+
}
|
|
1446
|
+
}],
|
|
1447
|
+
default: false
|
|
1448
|
+
} }
|
|
1449
|
+
}],
|
|
1450
|
+
messages: {
|
|
1451
|
+
classNameMismatch: "Angular class name should be '{{expectedName}}' to match the filename '{{filename}}'.",
|
|
1452
|
+
decoratedClassCount: "Angular filename '{{filename}}' should contain exactly one matching decorated class, but found {{actual}}."
|
|
1453
|
+
}
|
|
1535
1454
|
},
|
|
1536
1455
|
createOnce(context) {
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
angularNamespaces: /* @__PURE__ */ new Set()
|
|
1542
|
-
};
|
|
1456
|
+
let programNode = null;
|
|
1457
|
+
let fileMatcher = null;
|
|
1458
|
+
let fileDecoratorNames = /* @__PURE__ */ new Set();
|
|
1459
|
+
const decoratedClasses = [];
|
|
1543
1460
|
return {
|
|
1544
1461
|
before() {
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1462
|
+
programNode = null;
|
|
1463
|
+
decoratedClasses.length = 0;
|
|
1464
|
+
fileMatcher = getFileMatcher(context.filename ?? "");
|
|
1465
|
+
if (!fileMatcher) return false;
|
|
1466
|
+
fileDecoratorNames = new Set(fileMatcher.decoratorNames);
|
|
1548
1467
|
},
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
for (const specifier of node.specifiers ?? []) addAngularCoreDecoratorImport(specifier, COMPONENT_DECORATORS$1, decoratorImports);
|
|
1468
|
+
Program(node) {
|
|
1469
|
+
programNode = node;
|
|
1552
1470
|
},
|
|
1553
1471
|
ClassDeclaration(node) {
|
|
1472
|
+
if (!fileMatcher) return;
|
|
1554
1473
|
const classNode = node;
|
|
1555
|
-
if (
|
|
1556
|
-
if (classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorImports))) componentClasses.push(classNode);
|
|
1474
|
+
if (hasTargetDecorator$1(context, classNode, fileDecoratorNames)) decoratedClasses.push(classNode);
|
|
1557
1475
|
},
|
|
1558
1476
|
after() {
|
|
1559
1477
|
const filename = context.filename ?? "";
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1478
|
+
if (!fileMatcher) return;
|
|
1479
|
+
const baseFilename = getBaseFilename$1(filename);
|
|
1480
|
+
if (decoratedClasses.length !== 1) {
|
|
1481
|
+
const reportNode = decoratedClasses[0]?.id ?? decoratedClasses[0] ?? programNode;
|
|
1482
|
+
if (!reportNode) return;
|
|
1483
|
+
context.report({
|
|
1484
|
+
node: reportNode,
|
|
1485
|
+
messageId: "decoratedClassCount",
|
|
1486
|
+
data: {
|
|
1487
|
+
actual: getDecoratedClassCountDescription(decoratedClasses.length),
|
|
1488
|
+
filename: baseFilename
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
const expectedNames = getExpectedClassNames(filename, getIgnoreClassSuffixOption(context.options[0] ?? {}, getFileKind(fileMatcher)));
|
|
1494
|
+
if (!expectedNames) return;
|
|
1495
|
+
const classNode = decoratedClasses[0];
|
|
1496
|
+
const className = getNodeName(classNode);
|
|
1497
|
+
if (!className) return;
|
|
1498
|
+
if (expectedNames.includes(className)) return;
|
|
1566
1499
|
context.report({
|
|
1567
1500
|
node: classNode.id,
|
|
1568
1501
|
messageId: "classNameMismatch",
|
|
1569
1502
|
data: {
|
|
1570
|
-
expectedName,
|
|
1503
|
+
expectedName: formatExpectedNames(expectedNames),
|
|
1571
1504
|
filename: baseFilename
|
|
1572
1505
|
}
|
|
1573
1506
|
});
|
|
@@ -1577,7 +1510,7 @@ const componentClassMatchesFilename = defineRule({
|
|
|
1577
1510
|
});
|
|
1578
1511
|
//#endregion
|
|
1579
1512
|
//#region src/rules/component-resource-filenames/index.ts
|
|
1580
|
-
const COMPONENT_DECORATORS = new Set(["Component"]);
|
|
1513
|
+
const COMPONENT_DECORATORS$1 = new Set(["Component"]);
|
|
1581
1514
|
const STYLE_EXTENSIONS = new Set([
|
|
1582
1515
|
"css",
|
|
1583
1516
|
"less",
|
|
@@ -1609,9 +1542,9 @@ function getStaticString(node) {
|
|
|
1609
1542
|
if (node.type === "TemplateLiteral" && node.expressions?.length === 0) return { value: node.quasis?.[0]?.value?.cooked ?? node.quasis?.[0]?.value?.raw ?? "" };
|
|
1610
1543
|
return null;
|
|
1611
1544
|
}
|
|
1612
|
-
function getComponentMetadata(context, node
|
|
1545
|
+
function getComponentMetadata(context, node) {
|
|
1613
1546
|
if (node.type !== "Decorator") return null;
|
|
1614
|
-
if (!isAngularCoreDecorator(context, node,
|
|
1547
|
+
if (!isAngularCoreDecorator(context, node, COMPONENT_DECORATORS$1)) return null;
|
|
1615
1548
|
const expression = node.expression;
|
|
1616
1549
|
if (expression?.type !== "CallExpression") return null;
|
|
1617
1550
|
const metadata = expression.arguments?.[0];
|
|
@@ -1632,13 +1565,8 @@ const componentResourceFilenames = defineRule({
|
|
|
1632
1565
|
}
|
|
1633
1566
|
},
|
|
1634
1567
|
createOnce(context) {
|
|
1635
|
-
|
|
1636
|
-
decoratorNames: COMPONENT_DECORATORS,
|
|
1637
|
-
decoratorLocalNames: /* @__PURE__ */ new Set(),
|
|
1638
|
-
angularNamespaces: /* @__PURE__ */ new Set()
|
|
1639
|
-
};
|
|
1568
|
+
let expectedStem = null;
|
|
1640
1569
|
function reportTemplateUrl(valueNode) {
|
|
1641
|
-
const expectedStem = getExpectedStem(context.filename ?? "");
|
|
1642
1570
|
if (!expectedStem) return;
|
|
1643
1571
|
const staticString = getStaticString(valueNode);
|
|
1644
1572
|
if (!staticString) return;
|
|
@@ -1651,7 +1579,6 @@ const componentResourceFilenames = defineRule({
|
|
|
1651
1579
|
});
|
|
1652
1580
|
}
|
|
1653
1581
|
function reportStyleUrl(valueNode) {
|
|
1654
|
-
const expectedStem = getExpectedStem(context.filename ?? "");
|
|
1655
1582
|
if (!expectedStem) return;
|
|
1656
1583
|
const staticString = getStaticString(valueNode);
|
|
1657
1584
|
if (!staticString) return;
|
|
@@ -1673,15 +1600,11 @@ const componentResourceFilenames = defineRule({
|
|
|
1673
1600
|
}
|
|
1674
1601
|
return {
|
|
1675
1602
|
before() {
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
},
|
|
1679
|
-
ImportDeclaration(node) {
|
|
1680
|
-
if (node.source?.value !== "@angular/core") return;
|
|
1681
|
-
for (const specifier of node.specifiers ?? []) addAngularCoreDecoratorImport(specifier, COMPONENT_DECORATORS, decoratorImports);
|
|
1603
|
+
expectedStem = getExpectedStem(context.filename ?? "");
|
|
1604
|
+
if (!expectedStem) return false;
|
|
1682
1605
|
},
|
|
1683
1606
|
Decorator(node) {
|
|
1684
|
-
const metadata = getComponentMetadata(context, node
|
|
1607
|
+
const metadata = getComponentMetadata(context, node);
|
|
1685
1608
|
if (!metadata) return;
|
|
1686
1609
|
for (const property of metadata.properties ?? []) {
|
|
1687
1610
|
if (property.type !== "Property" || property.computed) continue;
|
|
@@ -1700,34 +1623,94 @@ const componentResourceFilenames = defineRule({
|
|
|
1700
1623
|
}
|
|
1701
1624
|
});
|
|
1702
1625
|
//#endregion
|
|
1626
|
+
//#region src/rules/decorator-filename-suffix/index.ts
|
|
1627
|
+
const FILENAME_SUFFIX_CONVENTIONS = [
|
|
1628
|
+
{
|
|
1629
|
+
kind: "component",
|
|
1630
|
+
suffix: ".component.ts",
|
|
1631
|
+
decoratorNames: new Set(["Component"])
|
|
1632
|
+
},
|
|
1633
|
+
{
|
|
1634
|
+
kind: "directive",
|
|
1635
|
+
suffix: ".directive.ts",
|
|
1636
|
+
decoratorNames: new Set(["Directive"])
|
|
1637
|
+
},
|
|
1638
|
+
{
|
|
1639
|
+
kind: "service",
|
|
1640
|
+
suffix: ".service.ts",
|
|
1641
|
+
decoratorNames: new Set(["Service", "Injectable"])
|
|
1642
|
+
}
|
|
1643
|
+
];
|
|
1644
|
+
function getBaseFilename(filename) {
|
|
1645
|
+
return filename.split(/[/\\]/u).at(-1) ?? filename;
|
|
1646
|
+
}
|
|
1647
|
+
function isTypeScriptFile(filename) {
|
|
1648
|
+
return getBaseFilename(filename).endsWith(".ts");
|
|
1649
|
+
}
|
|
1650
|
+
function getMatchingConvention(context, decorator) {
|
|
1651
|
+
return FILENAME_SUFFIX_CONVENTIONS.find((convention) => isAngularCoreDecorator(context, decorator, convention.decoratorNames)) ?? null;
|
|
1652
|
+
}
|
|
1653
|
+
const decoratorFilenameSuffix = defineRule({
|
|
1654
|
+
meta: {
|
|
1655
|
+
type: "suggestion",
|
|
1656
|
+
docs: {
|
|
1657
|
+
description: "Require Angular component, directive, and service decorators to be declared in files with matching suffixes.",
|
|
1658
|
+
recommended: true
|
|
1659
|
+
},
|
|
1660
|
+
schema: [],
|
|
1661
|
+
messages: { filenameSuffix: "Angular {{kind}} decorator should be declared in a file ending with '{{suffix}}'." }
|
|
1662
|
+
},
|
|
1663
|
+
createOnce(context) {
|
|
1664
|
+
return {
|
|
1665
|
+
before() {
|
|
1666
|
+
if (!isTypeScriptFile(context.filename ?? "")) return false;
|
|
1667
|
+
},
|
|
1668
|
+
Decorator(node) {
|
|
1669
|
+
const convention = getMatchingConvention(context, node);
|
|
1670
|
+
if (!convention) return;
|
|
1671
|
+
if (getBaseFilename(context.filename ?? "").endsWith(convention.suffix)) return;
|
|
1672
|
+
context.report({
|
|
1673
|
+
node,
|
|
1674
|
+
messageId: "filenameSuffix",
|
|
1675
|
+
data: {
|
|
1676
|
+
kind: convention.kind,
|
|
1677
|
+
suffix: convention.suffix
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
});
|
|
1684
|
+
//#endregion
|
|
1703
1685
|
//#region src/rules/prefer-load-component-over-load-children/index.ts
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
if (
|
|
1708
|
-
|
|
1709
|
-
|
|
1686
|
+
const ROUTE_TYPE_NAMES = new Set(["Route"]);
|
|
1687
|
+
const ROUTES_TYPE_NAMES = new Set(["Routes"]);
|
|
1688
|
+
function isImportedTypeName(context, typeNode, importedNames) {
|
|
1689
|
+
if (typeNode?.type === "Identifier") {
|
|
1690
|
+
const importedName = getImportedName(context, typeNode, "@angular/router");
|
|
1691
|
+
return !!importedName && importedNames.has(importedName);
|
|
1692
|
+
}
|
|
1693
|
+
return typeNode?.type === "TSQualifiedName" && typeNode.left?.type === "Identifier" && isNamespaceImport(context, typeNode.left, "@angular/router") && importedNames.has(getPropertyName(typeNode.right) ?? "");
|
|
1710
1694
|
}
|
|
1711
1695
|
function getTypeParameterNodes(typeNode) {
|
|
1712
1696
|
return typeNode.typeParameters?.params ?? typeNode.typeArguments?.params ?? [];
|
|
1713
1697
|
}
|
|
1714
|
-
function isRouteType(
|
|
1715
|
-
return typeNode?.type === "TSTypeReference" && isImportedTypeName(typeNode.typeName,
|
|
1698
|
+
function isRouteType(context, typeNode) {
|
|
1699
|
+
return typeNode?.type === "TSTypeReference" && isImportedTypeName(context, typeNode.typeName, ROUTE_TYPE_NAMES);
|
|
1716
1700
|
}
|
|
1717
|
-
function isRouteArrayType(
|
|
1701
|
+
function isRouteArrayType(context, typeNode) {
|
|
1718
1702
|
if (!typeNode) return false;
|
|
1719
|
-
if (typeNode.type === "TSTypeReference" && isImportedTypeName(typeNode.typeName,
|
|
1720
|
-
if (typeNode.type === "TSArrayType") return isRouteType(typeNode.elementType
|
|
1703
|
+
if (typeNode.type === "TSTypeReference" && isImportedTypeName(context, typeNode.typeName, ROUTES_TYPE_NAMES)) return true;
|
|
1704
|
+
if (typeNode.type === "TSArrayType") return isRouteType(context, typeNode.elementType);
|
|
1721
1705
|
if (typeNode.type !== "TSTypeReference") return false;
|
|
1722
|
-
|
|
1723
|
-
if (typeName !== "Array" && typeName !== "ReadonlyArray") return false;
|
|
1706
|
+
if (getPropertyName(typeNode.typeName) !== "Array" && getPropertyName(typeNode.typeName) !== "ReadonlyArray") return false;
|
|
1724
1707
|
const [elementType] = getTypeParameterNodes(typeNode);
|
|
1725
|
-
return isRouteType(
|
|
1708
|
+
return isRouteType(context, elementType);
|
|
1726
1709
|
}
|
|
1727
|
-
function isRoutesTypeAnnotation(
|
|
1710
|
+
function isRoutesTypeAnnotation(context, node) {
|
|
1728
1711
|
const typeAnnotation = node?.typeAnnotation;
|
|
1729
1712
|
if (!typeAnnotation || typeAnnotation.type !== "TSTypeAnnotation") return false;
|
|
1730
|
-
return isRouteArrayType(typeAnnotation.typeAnnotation
|
|
1713
|
+
return isRouteArrayType(context, typeAnnotation.typeAnnotation);
|
|
1731
1714
|
}
|
|
1732
1715
|
function isExportedConstDeclarator(node) {
|
|
1733
1716
|
if (node.type !== "VariableDeclarator") return false;
|
|
@@ -1746,11 +1729,6 @@ const preferLoadComponentOverLoadChildren = defineRule({
|
|
|
1746
1729
|
messages: { avoidLoadChildren: "Avoid loadChildren in Routes arrays; prefer loadComponent for lazy-loading standalone route components." }
|
|
1747
1730
|
},
|
|
1748
1731
|
createOnce(context) {
|
|
1749
|
-
const imports = {
|
|
1750
|
-
routeTypeLocalNames: /* @__PURE__ */ new Set(),
|
|
1751
|
-
routesTypeLocalNames: /* @__PURE__ */ new Set(),
|
|
1752
|
-
routerNamespaces: /* @__PURE__ */ new Set()
|
|
1753
|
-
};
|
|
1754
1732
|
function reportLoadChildrenInRouteObject(routeObject) {
|
|
1755
1733
|
for (const property of routeObject.properties ?? []) {
|
|
1756
1734
|
if (property.type !== "Property" || property.computed) continue;
|
|
@@ -1768,36 +1746,14 @@ const preferLoadComponentOverLoadChildren = defineRule({
|
|
|
1768
1746
|
function reportLoadChildrenInRouteArray(routeArray) {
|
|
1769
1747
|
for (const element of routeArray.elements ?? []) if (element?.type === "ObjectExpression") reportLoadChildrenInRouteObject(element);
|
|
1770
1748
|
}
|
|
1771
|
-
return {
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
for (const specifier of node.specifiers ?? []) {
|
|
1780
|
-
if (specifier.type === "ImportSpecifier") {
|
|
1781
|
-
const importedName = getPropertyName(specifier.imported);
|
|
1782
|
-
if (importedName === "Route") imports.routeTypeLocalNames.add(specifier.local.name);
|
|
1783
|
-
if (importedName === "Routes") imports.routesTypeLocalNames.add(specifier.local.name);
|
|
1784
|
-
}
|
|
1785
|
-
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
1786
|
-
imports.routerNamespaces.add(specifier.local.name);
|
|
1787
|
-
imports.routeTypeLocalNames.add("Route");
|
|
1788
|
-
imports.routesTypeLocalNames.add("Routes");
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
},
|
|
1792
|
-
VariableDeclarator(node) {
|
|
1793
|
-
const declarator = node;
|
|
1794
|
-
if (!isExportedConstDeclarator(declarator)) return;
|
|
1795
|
-
if (declarator.id?.type !== "Identifier") return;
|
|
1796
|
-
if (!isRoutesTypeAnnotation(declarator.id, imports)) return;
|
|
1797
|
-
if (declarator.init?.type !== "ArrayExpression") return;
|
|
1798
|
-
reportLoadChildrenInRouteArray(declarator.init);
|
|
1799
|
-
}
|
|
1800
|
-
};
|
|
1749
|
+
return { VariableDeclarator(node) {
|
|
1750
|
+
const declarator = node;
|
|
1751
|
+
if (!isExportedConstDeclarator(declarator)) return;
|
|
1752
|
+
if (declarator.id?.type !== "Identifier") return;
|
|
1753
|
+
if (!isRoutesTypeAnnotation(context, declarator.id)) return;
|
|
1754
|
+
if (declarator.init?.type !== "ArrayExpression") return;
|
|
1755
|
+
reportLoadChildrenInRouteArray(declarator.init);
|
|
1756
|
+
} };
|
|
1801
1757
|
}
|
|
1802
1758
|
});
|
|
1803
1759
|
//#endregion
|
|
@@ -1960,10 +1916,10 @@ const preferPrivateElements = defineRule({
|
|
|
1960
1916
|
});
|
|
1961
1917
|
//#endregion
|
|
1962
1918
|
//#region src/rules/prefer-style-url/index.ts
|
|
1963
|
-
|
|
1919
|
+
const COMPONENT_DECORATORS = new Set(["Component"]);
|
|
1920
|
+
function isComponentDecoratorCall(context, node) {
|
|
1964
1921
|
const callee = node.callee;
|
|
1965
|
-
|
|
1966
|
-
return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && angularNamespaces.has(callee.object.name) && !isShadowedIdentifier(context, callee.object) && getPropertyName(callee.property) === "Component";
|
|
1922
|
+
return isImportedReference(context, callee, "@angular/core", COMPONENT_DECORATORS) || isImportedNamespaceMember(context, callee, "@angular/core", COMPONENT_DECORATORS);
|
|
1967
1923
|
}
|
|
1968
1924
|
function isSingleStyleFileNode(node) {
|
|
1969
1925
|
if (!node) return false;
|
|
@@ -1982,47 +1938,30 @@ const preferStyleUrl = defineRule({
|
|
|
1982
1938
|
messages: { preferStyleUrl: "Use styleUrl instead of styleUrls when a component has one style file." }
|
|
1983
1939
|
},
|
|
1984
1940
|
createOnce(context) {
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
if (
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
if (property.type !== "Property") continue;
|
|
2008
|
-
if (property.computed) continue;
|
|
2009
|
-
if (getPropertyName(property.key) !== "styleUrls") continue;
|
|
2010
|
-
if (property.value?.type !== "ArrayExpression") continue;
|
|
2011
|
-
const elements = property.value.elements?.filter(Boolean) ?? [];
|
|
2012
|
-
if (elements.length !== 1) continue;
|
|
2013
|
-
const styleFile = elements[0];
|
|
2014
|
-
if (!isSingleStyleFileNode(styleFile)) continue;
|
|
2015
|
-
const keyRange = getRange(property.key);
|
|
2016
|
-
const valueRange = getRange(property.value);
|
|
2017
|
-
const styleFileRange = getRange(styleFile);
|
|
2018
|
-
context.report({
|
|
2019
|
-
node: property.key,
|
|
2020
|
-
messageId: "preferStyleUrl",
|
|
2021
|
-
fix: keyRange && valueRange && styleFileRange ? (fixer) => [fixer.replaceTextRange(keyRange, "styleUrl"), fixer.replaceTextRange(valueRange, context.sourceCode.text.slice(styleFileRange[0], styleFileRange[1]))] : void 0
|
|
2022
|
-
});
|
|
2023
|
-
}
|
|
1941
|
+
return { CallExpression(node) {
|
|
1942
|
+
const call = node;
|
|
1943
|
+
if (!isComponentDecoratorCall(context, call)) return;
|
|
1944
|
+
const metadata = call.arguments?.[0];
|
|
1945
|
+
if (metadata?.type !== "ObjectExpression") return;
|
|
1946
|
+
for (const property of metadata.properties ?? []) {
|
|
1947
|
+
if (property.type !== "Property") continue;
|
|
1948
|
+
if (property.computed) continue;
|
|
1949
|
+
if (getPropertyName(property.key) !== "styleUrls") continue;
|
|
1950
|
+
if (property.value?.type !== "ArrayExpression") continue;
|
|
1951
|
+
const elements = property.value.elements?.filter(Boolean) ?? [];
|
|
1952
|
+
if (elements.length !== 1) continue;
|
|
1953
|
+
const styleFile = elements[0];
|
|
1954
|
+
if (!isSingleStyleFileNode(styleFile)) continue;
|
|
1955
|
+
const keyRange = getRange(property.key);
|
|
1956
|
+
const valueRange = getRange(property.value);
|
|
1957
|
+
const styleFileRange = getRange(styleFile);
|
|
1958
|
+
context.report({
|
|
1959
|
+
node: property.key,
|
|
1960
|
+
messageId: "preferStyleUrl",
|
|
1961
|
+
fix: keyRange && valueRange && styleFileRange ? (fixer) => [fixer.replaceTextRange(keyRange, "styleUrl"), fixer.replaceTextRange(valueRange, context.sourceCode.text.slice(styleFileRange[0], styleFileRange[1]))] : void 0
|
|
1962
|
+
});
|
|
2024
1963
|
}
|
|
2025
|
-
};
|
|
1964
|
+
} };
|
|
2026
1965
|
}
|
|
2027
1966
|
});
|
|
2028
1967
|
//#endregion
|
|
@@ -2036,24 +1975,21 @@ const FIELD_NODE_TYPES = new Set([
|
|
|
2036
1975
|
"FieldDefinition",
|
|
2037
1976
|
"PropertyDefinition"
|
|
2038
1977
|
]);
|
|
2039
|
-
function hasTargetDecorator(context, classNode
|
|
1978
|
+
function hasTargetDecorator(context, classNode) {
|
|
2040
1979
|
if (!classNode || !Array.isArray(classNode.decorators)) return false;
|
|
2041
|
-
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator,
|
|
1980
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS));
|
|
2042
1981
|
}
|
|
2043
|
-
function
|
|
1982
|
+
function isApiCallFromAngularCore(context, node, apiNames) {
|
|
2044
1983
|
if (!node || node.type !== "CallExpression") return false;
|
|
2045
1984
|
const callee = node.callee;
|
|
2046
|
-
if (callee
|
|
1985
|
+
if (isImportedReference(context, callee, "@angular/core", apiNames)) return true;
|
|
2047
1986
|
if (callee?.type !== "MemberExpression") return false;
|
|
2048
1987
|
const supportsRequiredApi = [...INPUT_MODEL_APIS].some((name) => apiNames.has(name));
|
|
2049
|
-
if (getPropertyName(callee.property) === "required" && callee.object?.type === "Identifier") return supportsRequiredApi &&
|
|
2050
|
-
if (callee
|
|
2051
|
-
|
|
2052
|
-
return !!namespaceApiName && apiNames.has(namespaceApiName) && !isShadowedIdentifier(context, callee.object);
|
|
2053
|
-
}
|
|
2054
|
-
if (getPropertyName(callee.property) === "required" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier" && angularNamespaces.has(callee.object.object.name)) {
|
|
1988
|
+
if (getPropertyName(callee.property) === "required" && callee.object?.type === "Identifier") return supportsRequiredApi && isImportedReference(context, callee.object, "@angular/core", apiNames);
|
|
1989
|
+
if (isImportedNamespaceMember(context, callee, "@angular/core", apiNames)) return true;
|
|
1990
|
+
if (getPropertyName(callee.property) === "required" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier") {
|
|
2055
1991
|
const namespaceApiName = getPropertyName(callee.object.property);
|
|
2056
|
-
return supportsRequiredApi && !!namespaceApiName && apiNames.has(namespaceApiName) &&
|
|
1992
|
+
return supportsRequiredApi && !!namespaceApiName && apiNames.has(namespaceApiName) && isImportedNamespaceMember(context, callee.object, "@angular/core", apiNames);
|
|
2057
1993
|
}
|
|
2058
1994
|
return false;
|
|
2059
1995
|
}
|
|
@@ -2098,87 +2034,48 @@ const publicComponentInterface = defineRule({
|
|
|
2098
2034
|
}
|
|
2099
2035
|
},
|
|
2100
2036
|
createOnce(context) {
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
decoratorImports.decoratorLocalNames.clear();
|
|
2117
|
-
},
|
|
2118
|
-
ImportDeclaration(node) {
|
|
2119
|
-
if (node.source?.value !== "@angular/core") return;
|
|
2120
|
-
for (const specifier of node.specifiers ?? []) {
|
|
2121
|
-
addAngularCoreDecoratorImport(specifier, TARGET_DECORATORS, decoratorImports);
|
|
2122
|
-
if (specifier.type === "ImportSpecifier") {
|
|
2123
|
-
const importedName = getPropertyName(specifier.imported);
|
|
2124
|
-
if (!importedName) continue;
|
|
2125
|
-
if (INPUT_MODEL_APIS.has(importedName)) inputModelLocalNames.add(specifier.local.name);
|
|
2126
|
-
if (OUTPUT_APIS.has(importedName)) outputLocalNames.add(specifier.local.name);
|
|
2127
|
-
if (INJECT_APIS.has(importedName)) injectLocalNames.add(specifier.local.name);
|
|
2128
|
-
continue;
|
|
2129
|
-
}
|
|
2130
|
-
if (specifier.type === "ImportNamespaceSpecifier") angularNamespaces.add(specifier.local.name);
|
|
2037
|
+
return { ClassBody(node) {
|
|
2038
|
+
const classNode = node.parent;
|
|
2039
|
+
if (!hasTargetDecorator(context, classNode)) return;
|
|
2040
|
+
for (const member of node.body ?? []) {
|
|
2041
|
+
if (!FIELD_NODE_TYPES.has(member.type)) continue;
|
|
2042
|
+
const isInputModelMember = isApiCallFromAngularCore(context, member.value, INPUT_MODEL_APIS);
|
|
2043
|
+
const isOutputMember = isApiCallFromAngularCore(context, member.value, OUTPUT_APIS);
|
|
2044
|
+
if (isInputModelMember && isNonPublicMember(member)) {
|
|
2045
|
+
context.report({
|
|
2046
|
+
node: member.key ?? member,
|
|
2047
|
+
messageId: "nonPublicInputModel",
|
|
2048
|
+
data: { name: getPropertyName(member.key) ?? "member" },
|
|
2049
|
+
fix: getVisibilityFix(context, member, "public")
|
|
2050
|
+
});
|
|
2051
|
+
continue;
|
|
2131
2052
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
const classNode = node.parent;
|
|
2135
|
-
if (!hasTargetDecorator(context, classNode, decoratorImports)) return;
|
|
2136
|
-
for (const member of node.body ?? []) {
|
|
2137
|
-
if (!FIELD_NODE_TYPES.has(member.type)) continue;
|
|
2138
|
-
const isInputModelMember = isApiCallFromTrackedImports(context, member.value, inputModelLocalNames, angularNamespaces, INPUT_MODEL_APIS);
|
|
2139
|
-
const isOutputMember = isApiCallFromTrackedImports(context, member.value, outputLocalNames, angularNamespaces, OUTPUT_APIS);
|
|
2140
|
-
if (isInputModelMember && isNonPublicMember(member)) {
|
|
2141
|
-
context.report({
|
|
2142
|
-
node: member.key ?? member,
|
|
2143
|
-
messageId: "nonPublicInputModel",
|
|
2144
|
-
data: { name: getPropertyName(member.key) ?? "member" },
|
|
2145
|
-
fix: getVisibilityFix(context, member, "public")
|
|
2146
|
-
});
|
|
2147
|
-
continue;
|
|
2148
|
-
}
|
|
2149
|
-
if (isOutputMember && isNonPublicMember(member)) {
|
|
2150
|
-
context.report({
|
|
2151
|
-
node: member.key ?? member,
|
|
2152
|
-
messageId: "nonPublicOutput",
|
|
2153
|
-
data: { name: getPropertyName(member.key) ?? "member" },
|
|
2154
|
-
fix: getVisibilityFix(context, member, "public")
|
|
2155
|
-
});
|
|
2156
|
-
continue;
|
|
2157
|
-
}
|
|
2158
|
-
if (isPublicMember(member) && isApiCallFromTrackedImports(context, member.value, injectLocalNames, angularNamespaces, INJECT_APIS)) context.report({
|
|
2053
|
+
if (isOutputMember && isNonPublicMember(member)) {
|
|
2054
|
+
context.report({
|
|
2159
2055
|
node: member.key ?? member,
|
|
2160
|
-
messageId: "
|
|
2056
|
+
messageId: "nonPublicOutput",
|
|
2161
2057
|
data: { name: getPropertyName(member.key) ?? "member" },
|
|
2162
|
-
fix: getVisibilityFix(context, member, "
|
|
2058
|
+
fix: getVisibilityFix(context, member, "public")
|
|
2163
2059
|
});
|
|
2060
|
+
continue;
|
|
2164
2061
|
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2062
|
+
if (isPublicMember(member) && isApiCallFromAngularCore(context, member.value, INJECT_APIS)) context.report({
|
|
2063
|
+
node: member.key ?? member,
|
|
2064
|
+
messageId: "publicInjectMember",
|
|
2065
|
+
data: { name: getPropertyName(member.key) ?? "member" },
|
|
2066
|
+
fix: getVisibilityFix(context, member, "protected")
|
|
2067
|
+
});
|
|
2171
2068
|
}
|
|
2172
|
-
};
|
|
2069
|
+
} };
|
|
2173
2070
|
}
|
|
2174
2071
|
});
|
|
2175
2072
|
//#endregion
|
|
2176
2073
|
//#region src/rules/restrict-injectable-provided-in/index.ts
|
|
2177
2074
|
const ALLOWED_PROVIDED_IN_VALUES = new Set(["root", "platform"]);
|
|
2178
2075
|
const INJECTABLE_DECORATORS = new Set(["Injectable"]);
|
|
2179
|
-
function getInjectableMetadata(context, node
|
|
2076
|
+
function getInjectableMetadata(context, node) {
|
|
2180
2077
|
if (node.type !== "Decorator") return null;
|
|
2181
|
-
if (!isAngularCoreDecorator(context, node,
|
|
2078
|
+
if (!isAngularCoreDecorator(context, node, INJECTABLE_DECORATORS)) return null;
|
|
2182
2079
|
const expression = node.expression;
|
|
2183
2080
|
if (expression?.type !== "CallExpression") return null;
|
|
2184
2081
|
const metadata = expression.arguments?.[0];
|
|
@@ -2203,35 +2100,20 @@ const restrictInjectableProvidedIn = defineRule({
|
|
|
2203
2100
|
messages: { disallowedProvidedIn: "@Injectable providedIn should be the literal 'root' or 'platform', not {{actual}}." }
|
|
2204
2101
|
},
|
|
2205
2102
|
createOnce(context) {
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
Decorator(node) {
|
|
2221
|
-
const metadata = getInjectableMetadata(context, node, decoratorImports);
|
|
2222
|
-
if (!metadata) return;
|
|
2223
|
-
const providedInProperty = getProvidedInProperty(metadata);
|
|
2224
|
-
if (!providedInProperty) return;
|
|
2225
|
-
const value = providedInProperty.value;
|
|
2226
|
-
if (isAllowedProvidedInValue(value)) return;
|
|
2227
|
-
const actual = context.sourceCode.text.slice(value?.range?.[0] ?? providedInProperty.range?.[0] ?? 0, value?.range?.[1] ?? providedInProperty.range?.[1] ?? 0);
|
|
2228
|
-
context.report({
|
|
2229
|
-
node: value ?? providedInProperty,
|
|
2230
|
-
messageId: "disallowedProvidedIn",
|
|
2231
|
-
data: { actual: actual || "this value" }
|
|
2232
|
-
});
|
|
2233
|
-
}
|
|
2234
|
-
};
|
|
2103
|
+
return { Decorator(node) {
|
|
2104
|
+
const metadata = getInjectableMetadata(context, node);
|
|
2105
|
+
if (!metadata) return;
|
|
2106
|
+
const providedInProperty = getProvidedInProperty(metadata);
|
|
2107
|
+
if (!providedInProperty) return;
|
|
2108
|
+
const value = providedInProperty.value;
|
|
2109
|
+
if (isAllowedProvidedInValue(value)) return;
|
|
2110
|
+
const actual = context.sourceCode.text.slice(value?.range?.[0] ?? providedInProperty.range?.[0] ?? 0, value?.range?.[1] ?? providedInProperty.range?.[1] ?? 0);
|
|
2111
|
+
context.report({
|
|
2112
|
+
node: value ?? providedInProperty,
|
|
2113
|
+
messageId: "disallowedProvidedIn",
|
|
2114
|
+
data: { actual: actual || "this value" }
|
|
2115
|
+
});
|
|
2116
|
+
} };
|
|
2235
2117
|
}
|
|
2236
2118
|
});
|
|
2237
2119
|
//#endregion
|
|
@@ -2458,8 +2340,9 @@ const plugin = eslintCompatPlugin({
|
|
|
2458
2340
|
"avoid-rxjs-state-in-component": avoidRxjsStateInComponent,
|
|
2459
2341
|
"avoid-writing-signals-in-reactive-context": avoidWritingSignalsInReactiveContext,
|
|
2460
2342
|
"class-member-order": classMemberOrder,
|
|
2461
|
-
"
|
|
2343
|
+
"class-matches-filename": classMatchesFilename,
|
|
2462
2344
|
"component-resource-filenames": componentResourceFilenames,
|
|
2345
|
+
"decorator-filename-suffix": decoratorFilenameSuffix,
|
|
2463
2346
|
"prefer-load-component-over-load-children": preferLoadComponentOverLoadChildren,
|
|
2464
2347
|
"prefer-private-elements": preferPrivateElements,
|
|
2465
2348
|
"prefer-style-url": preferStyleUrl,
|