@benjavicente/lint-angular 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -19
- package/dist/index.mjs +884 -131
- package/package.json +12 -12
package/dist/index.mjs
CHANGED
|
@@ -55,13 +55,13 @@ function unwrapExpression(node) {
|
|
|
55
55
|
}
|
|
56
56
|
//#endregion
|
|
57
57
|
//#region src/utilities/scope.ts
|
|
58
|
-
const FUNCTION_TYPES$
|
|
58
|
+
const FUNCTION_TYPES$2 = new Set([
|
|
59
59
|
"ArrowFunctionExpression",
|
|
60
60
|
"FunctionDeclaration",
|
|
61
61
|
"FunctionExpression"
|
|
62
62
|
]);
|
|
63
63
|
function isFunction$1(node) {
|
|
64
|
-
return !!node && FUNCTION_TYPES$
|
|
64
|
+
return !!node && FUNCTION_TYPES$2.has(node.type);
|
|
65
65
|
}
|
|
66
66
|
function addBindingIdentifierNodes(node, identifiers) {
|
|
67
67
|
if (!node) return;
|
|
@@ -222,7 +222,7 @@ const ANGULAR_CLASS_DECORATOR_NAMES$1 = new Set([
|
|
|
222
222
|
]);
|
|
223
223
|
const INPUT_MODEL_CALL_NAMES = new Set(["input", "model"]);
|
|
224
224
|
const OUTPUT_CALL_NAMES = new Set(["output", "outputFromObservable"]);
|
|
225
|
-
const CLASS_FIELD_TYPES$
|
|
225
|
+
const CLASS_FIELD_TYPES$2 = new Set([
|
|
226
226
|
"AccessorProperty",
|
|
227
227
|
"FieldDefinition",
|
|
228
228
|
"PropertyDefinition"
|
|
@@ -254,7 +254,7 @@ function hasDecorator(context, element, decoratorNames) {
|
|
|
254
254
|
return Array.isArray(element.decorators) ? element.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorNames)) : false;
|
|
255
255
|
}
|
|
256
256
|
function classifyMember(context, element) {
|
|
257
|
-
if (CLASS_FIELD_TYPES$
|
|
257
|
+
if (CLASS_FIELD_TYPES$2.has(element.type)) {
|
|
258
258
|
if (isApiCall(context, element.value, new Set(["inject"]))) return 0;
|
|
259
259
|
if (isApiCall(context, element.value, INPUT_MODEL_CALL_NAMES)) return 1;
|
|
260
260
|
if (hasDecorator(context, element, new Set(["Input"]))) return 1;
|
|
@@ -265,7 +265,7 @@ function classifyMember(context, element) {
|
|
|
265
265
|
if (element.type === "MethodDefinition") return 3;
|
|
266
266
|
return 3;
|
|
267
267
|
}
|
|
268
|
-
function getMemberName$
|
|
268
|
+
function getMemberName$2(node) {
|
|
269
269
|
if (!node) return null;
|
|
270
270
|
if (node.type === "Identifier" || node.type === "PrivateIdentifier") return node.name;
|
|
271
271
|
return null;
|
|
@@ -335,7 +335,7 @@ function getSortedMembersFix(context, classBody, classifiedMembers) {
|
|
|
335
335
|
for (const member of classifiedMembers) if (member.name) membersByName.set(member.name, member);
|
|
336
336
|
for (const member of classifiedMembers) {
|
|
337
337
|
const { element } = member;
|
|
338
|
-
if (!CLASS_FIELD_TYPES$
|
|
338
|
+
if (!CLASS_FIELD_TYPES$2.has(element.type)) return void 0;
|
|
339
339
|
if (element.type === "AccessorProperty") return void 0;
|
|
340
340
|
if (element.computed) return void 0;
|
|
341
341
|
if (Array.isArray(element.decorators) && element.decorators.length > 0) return void 0;
|
|
@@ -388,7 +388,7 @@ const classMemberOrder = defineRule({
|
|
|
388
388
|
element,
|
|
389
389
|
group,
|
|
390
390
|
effectiveGroup: group,
|
|
391
|
-
name: getMemberName$
|
|
391
|
+
name: getMemberName$2(element.key),
|
|
392
392
|
dependencies: collectThisMemberReferences(element.value)
|
|
393
393
|
});
|
|
394
394
|
}
|
|
@@ -423,17 +423,17 @@ const classMemberOrder = defineRule({
|
|
|
423
423
|
const DEFAULT_DISALLOW_INJECT_INJECTOR = true;
|
|
424
424
|
const DEFAULT_DISALLOW_RUN_IN_INJECTION_CONTEXT = true;
|
|
425
425
|
const RUN_IN_INJECTION_CONTEXT_NAMES = new Set(["runInInjectionContext", "runInContext"]);
|
|
426
|
-
const INJECT_NAMES = new Set(["inject"]);
|
|
426
|
+
const INJECT_NAMES$2 = new Set(["inject"]);
|
|
427
427
|
const INJECTOR_NAMES = new Set(["Injector"]);
|
|
428
|
-
function isInjectCall(context, callNode) {
|
|
428
|
+
function isInjectCall$1(context, callNode) {
|
|
429
429
|
const callee = callNode.callee;
|
|
430
|
-
return isImportedReference(context, callee, "@angular/core", INJECT_NAMES) || isImportedNamespaceMember(context, callee, "@angular/core", INJECT_NAMES);
|
|
430
|
+
return isImportedReference(context, callee, "@angular/core", INJECT_NAMES$2) || isImportedNamespaceMember(context, callee, "@angular/core", INJECT_NAMES$2);
|
|
431
431
|
}
|
|
432
432
|
function isInjectorReference(context, node) {
|
|
433
433
|
return isImportedReference(context, node, "@angular/core", INJECTOR_NAMES) || isImportedNamespaceMember(context, node, "@angular/core", INJECTOR_NAMES);
|
|
434
434
|
}
|
|
435
435
|
function isDisallowedInjectInjector(context, callNode) {
|
|
436
|
-
if (!isInjectCall(context, callNode)) return false;
|
|
436
|
+
if (!isInjectCall$1(context, callNode)) return false;
|
|
437
437
|
return isInjectorReference(context, callNode.arguments?.[0]);
|
|
438
438
|
}
|
|
439
439
|
function isDisallowedRunInInjectionContext(context, callNode) {
|
|
@@ -485,12 +485,12 @@ const avoidExplicitInjectionContext = defineRule({
|
|
|
485
485
|
});
|
|
486
486
|
//#endregion
|
|
487
487
|
//#region src/rules/avoid-explicit-subscription-management/index.ts
|
|
488
|
-
const TARGET_DECORATORS$
|
|
488
|
+
const TARGET_DECORATORS$4 = new Set([
|
|
489
489
|
"Component",
|
|
490
490
|
"Directive",
|
|
491
491
|
"Injectable"
|
|
492
492
|
]);
|
|
493
|
-
const FIELD_NODE_TYPES$
|
|
493
|
+
const FIELD_NODE_TYPES$3 = new Set([
|
|
494
494
|
"AccessorProperty",
|
|
495
495
|
"FieldDefinition",
|
|
496
496
|
"PropertyDefinition"
|
|
@@ -502,9 +502,9 @@ const RXJS_SUBSCRIBABLE_NAMES = new Set([
|
|
|
502
502
|
"ReplaySubject",
|
|
503
503
|
"Subject"
|
|
504
504
|
]);
|
|
505
|
-
function hasTargetDecorator$
|
|
505
|
+
function hasTargetDecorator$4(context, classNode) {
|
|
506
506
|
if (!classNode || !Array.isArray(classNode.decorators)) return false;
|
|
507
|
-
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$
|
|
507
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$4));
|
|
508
508
|
}
|
|
509
509
|
function hasSubscriptionTypeReference(context, node, subscriptionLocalNames, rxjsNamespaces) {
|
|
510
510
|
if (!node) return false;
|
|
@@ -644,10 +644,10 @@ function isUnmanagedSubscribeCall(context, node, rxjsSubscribableReferences, rxj
|
|
|
644
644
|
if (getPropertyName(callee.property) !== "subscribe") return false;
|
|
645
645
|
return isKnownRxjsSubscribableExpression(context, callee.object, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces) && !isTakeUntilDestroyedSubscribe(node, takeUntilDestroyedLocalNames, interopNamespaces);
|
|
646
646
|
}
|
|
647
|
-
function walkNode$
|
|
647
|
+
function walkNode$2(node, containingClass, visitor) {
|
|
648
648
|
if (!node) return;
|
|
649
649
|
if (Array.isArray(node)) {
|
|
650
|
-
for (const child of node) walkNode$
|
|
650
|
+
for (const child of node) walkNode$2(child, containingClass, visitor);
|
|
651
651
|
return;
|
|
652
652
|
}
|
|
653
653
|
if (node !== containingClass && (node.type === "ClassDeclaration" || node.type === "ClassExpression")) return;
|
|
@@ -655,12 +655,12 @@ function walkNode$1(node, containingClass, visitor) {
|
|
|
655
655
|
for (const [key, value] of Object.entries(node)) {
|
|
656
656
|
if (key === "parent") continue;
|
|
657
657
|
if (!value || typeof value !== "object") continue;
|
|
658
|
-
walkNode$
|
|
658
|
+
walkNode$2(value, containingClass, visitor);
|
|
659
659
|
}
|
|
660
660
|
}
|
|
661
661
|
function containsUnmanagedSubscribeCall(context, node, containingClass, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces) {
|
|
662
662
|
let found = false;
|
|
663
|
-
walkNode$
|
|
663
|
+
walkNode$2(node, containingClass, (current) => {
|
|
664
664
|
if (isUnmanagedSubscribeCall(context, current, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces)) {
|
|
665
665
|
found = true;
|
|
666
666
|
return false;
|
|
@@ -673,8 +673,8 @@ function collectRxjsSubscribableReferences(context, classBody, classNode, rxjsSu
|
|
|
673
673
|
let changed = true;
|
|
674
674
|
while (changed) {
|
|
675
675
|
changed = false;
|
|
676
|
-
walkNode$
|
|
677
|
-
if (FIELD_NODE_TYPES$
|
|
676
|
+
walkNode$2(classBody, classNode, (node) => {
|
|
677
|
+
if (FIELD_NODE_TYPES$3.has(node.type)) {
|
|
678
678
|
const name = getDeclaredName(node.key);
|
|
679
679
|
if (!name || hasTrackedReferenceName(references, name)) return;
|
|
680
680
|
if (isObservableReferenceName(name) || hasRxjsSubscribableTypeReference(context, node.typeAnnotation?.typeAnnotation, rxjsSubscribableLocalNames, rxjsNamespaces) || isRxjsSubscribableConstructor(context, node.value, rxjsSubscribableLocalNames, rxjsNamespaces) || isKnownRxjsSubscribableExpression(context, node.value, references, rxjsSubscribableLocalNames, rxjsNamespaces)) {
|
|
@@ -705,8 +705,8 @@ function collectRxjsSubscribableReferences(context, classBody, classNode, rxjsSu
|
|
|
705
705
|
}
|
|
706
706
|
function collectSubscriptionReferences(context, classBody, classNode, subscriptionLocalNames, rxjsNamespaces, rxjsSubscribableReferences, rxjsSubscribableLocalNames, takeUntilDestroyedLocalNames, interopNamespaces) {
|
|
707
707
|
const references = /* @__PURE__ */ new Map();
|
|
708
|
-
walkNode$
|
|
709
|
-
if (FIELD_NODE_TYPES$
|
|
708
|
+
walkNode$2(classBody, classNode, (node) => {
|
|
709
|
+
if (FIELD_NODE_TYPES$3.has(node.type)) {
|
|
710
710
|
const name = getDeclaredName(node.key);
|
|
711
711
|
if (!name) return;
|
|
712
712
|
if (hasSubscriptionTypeReference(context, node.typeAnnotation?.typeAnnotation, subscriptionLocalNames, rxjsNamespaces) || isSubscriptionConstructor(context, node.value, subscriptionLocalNames, rxjsNamespaces)) addTrackedReference(references, name, node.key);
|
|
@@ -793,11 +793,11 @@ const avoidExplicitSubscriptionManagement = defineRule({
|
|
|
793
793
|
ClassBody(node) {
|
|
794
794
|
const classBody = node;
|
|
795
795
|
const classNode = classBody.parent;
|
|
796
|
-
if (!classNode || !hasTargetDecorator$
|
|
796
|
+
if (!classNode || !hasTargetDecorator$4(context, classNode)) return;
|
|
797
797
|
const rxjsSubscribableReferences = collectRxjsSubscribableReferences(context, classBody, classNode, rxjsSubscribableLocalNames, rxjsNamespaces);
|
|
798
798
|
const subscriptionReferences = collectSubscriptionReferences(context, classBody, classNode, subscriptionLocalNames, rxjsNamespaces, rxjsSubscribableReferences, rxjsSubscribableLocalNames, takeUntilDestroyedLocalNames, interopNamespaces);
|
|
799
|
-
walkNode$
|
|
800
|
-
if (FIELD_NODE_TYPES$
|
|
799
|
+
walkNode$2(classBody, classNode, (current) => {
|
|
800
|
+
if (FIELD_NODE_TYPES$3.has(current.type)) {
|
|
801
801
|
if (hasSubscriptionTypeReference(context, current.typeAnnotation?.typeAnnotation, subscriptionLocalNames, rxjsNamespaces)) context.report({
|
|
802
802
|
node: current.key ?? current,
|
|
803
803
|
messageId: "explicitSubscriptionType"
|
|
@@ -845,6 +845,46 @@ const avoidExplicitSubscriptionManagement = defineRule({
|
|
|
845
845
|
}
|
|
846
846
|
});
|
|
847
847
|
//#endregion
|
|
848
|
+
//#region src/rules/avoid-inappropriate-intimacy/index.ts
|
|
849
|
+
const TARGET_DECORATORS$3 = new Set([
|
|
850
|
+
"Component",
|
|
851
|
+
"Directive",
|
|
852
|
+
"Injectable",
|
|
853
|
+
"Service"
|
|
854
|
+
]);
|
|
855
|
+
function getAngularClassKind(context, classNode) {
|
|
856
|
+
if (!classNode || !Array.isArray(classNode.decorators)) return null;
|
|
857
|
+
if (classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, new Set(["Component"])))) return "component";
|
|
858
|
+
if (classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, new Set(["Directive"])))) return "directive";
|
|
859
|
+
if (classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$3))) return "service";
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
const avoidInappropriateIntimacy = defineRule({
|
|
863
|
+
meta: {
|
|
864
|
+
type: "problem",
|
|
865
|
+
docs: {
|
|
866
|
+
description: "Disallow passing Angular component, directive, and service instances as function arguments.",
|
|
867
|
+
recommended: true
|
|
868
|
+
},
|
|
869
|
+
schema: [],
|
|
870
|
+
messages: { avoidThisArgument: "Avoid passing this {{kind}} instance as an argument. Pass the specific values or callbacks the callee needs." }
|
|
871
|
+
},
|
|
872
|
+
createOnce(context) {
|
|
873
|
+
return { CallExpression(node) {
|
|
874
|
+
const callNode = node;
|
|
875
|
+
const thisArguments = (callNode.arguments ?? []).filter((argument) => argument.type === "ThisExpression");
|
|
876
|
+
if (!thisArguments.length) return;
|
|
877
|
+
const kind = getAngularClassKind(context, context.sourceCode.getAncestors(callNode).findLast((ancestor) => ancestor.type === "ClassDeclaration" || ancestor.type === "ClassExpression"));
|
|
878
|
+
if (!kind) return;
|
|
879
|
+
for (const argument of thisArguments) context.report({
|
|
880
|
+
node: argument,
|
|
881
|
+
messageId: "avoidThisArgument",
|
|
882
|
+
data: { kind }
|
|
883
|
+
});
|
|
884
|
+
} };
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
//#endregion
|
|
848
888
|
//#region src/rules/avoid-ng-modules/index.ts
|
|
849
889
|
const DEFAULT_ALLOW_FOR_GROUPING = true;
|
|
850
890
|
const DEFAULT_ALLOW_FOR_PROVIDING = false;
|
|
@@ -1022,20 +1062,20 @@ const avoidNgModules = defineRule({
|
|
|
1022
1062
|
});
|
|
1023
1063
|
//#endregion
|
|
1024
1064
|
//#region src/rules/avoid-rxjs-state-in-component/index.ts
|
|
1025
|
-
const TARGET_DECORATORS$
|
|
1065
|
+
const TARGET_DECORATORS$2 = new Set(["Component", "Directive"]);
|
|
1026
1066
|
const SUBJECT_NAMES = new Set([
|
|
1027
1067
|
"BehaviorSubject",
|
|
1028
1068
|
"ReplaySubject",
|
|
1029
1069
|
"Subject"
|
|
1030
1070
|
]);
|
|
1031
|
-
const FIELD_NODE_TYPES$
|
|
1071
|
+
const FIELD_NODE_TYPES$2 = new Set([
|
|
1032
1072
|
"AccessorProperty",
|
|
1033
1073
|
"FieldDefinition",
|
|
1034
1074
|
"PropertyDefinition"
|
|
1035
1075
|
]);
|
|
1036
|
-
function hasTargetDecorator$
|
|
1076
|
+
function hasTargetDecorator$3(context, classNode) {
|
|
1037
1077
|
if (!classNode || !Array.isArray(classNode.decorators)) return false;
|
|
1038
|
-
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$
|
|
1078
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$2));
|
|
1039
1079
|
}
|
|
1040
1080
|
function getImportedSubjectKind(context, node) {
|
|
1041
1081
|
const importedName = getImportedName(context, node, "rxjs");
|
|
@@ -1061,7 +1101,7 @@ function getSubjectKindFromConstructor(context, node) {
|
|
|
1061
1101
|
const memberName = getPropertyName(callee.property);
|
|
1062
1102
|
return SUBJECT_NAMES.has(memberName) ? memberName : null;
|
|
1063
1103
|
}
|
|
1064
|
-
function getMemberName(node) {
|
|
1104
|
+
function getMemberName$1(node) {
|
|
1065
1105
|
const expression = unwrapExpression(node);
|
|
1066
1106
|
if (!expression) return null;
|
|
1067
1107
|
if (expression.type === "Identifier") return expression.name;
|
|
@@ -1091,17 +1131,17 @@ function isCallOnThisField(node, fields) {
|
|
|
1091
1131
|
function isNgOnDestroyMethod(member) {
|
|
1092
1132
|
return member.type === "MethodDefinition" && getPropertyName(member.key) === "ngOnDestroy" && !!member.value;
|
|
1093
1133
|
}
|
|
1094
|
-
function walkNode(node, visitor) {
|
|
1134
|
+
function walkNode$1(node, visitor) {
|
|
1095
1135
|
if (!node) return;
|
|
1096
1136
|
if (Array.isArray(node)) {
|
|
1097
|
-
for (const child of node) walkNode(child, visitor);
|
|
1137
|
+
for (const child of node) walkNode$1(child, visitor);
|
|
1098
1138
|
return;
|
|
1099
1139
|
}
|
|
1100
1140
|
visitor(node);
|
|
1101
1141
|
for (const [key, value] of Object.entries(node)) {
|
|
1102
1142
|
if (key === "parent") continue;
|
|
1103
1143
|
if (!value || typeof value !== "object") continue;
|
|
1104
|
-
walkNode(value, visitor);
|
|
1144
|
+
walkNode$1(value, visitor);
|
|
1105
1145
|
}
|
|
1106
1146
|
}
|
|
1107
1147
|
function getUsage(usages, fieldName) {
|
|
@@ -1119,8 +1159,8 @@ function getUsage(usages, fieldName) {
|
|
|
1119
1159
|
function collectSubjectFields(context, classBody) {
|
|
1120
1160
|
const fields = /* @__PURE__ */ new Map();
|
|
1121
1161
|
for (const member of classBody.body ?? []) {
|
|
1122
|
-
if (!FIELD_NODE_TYPES$
|
|
1123
|
-
const fieldName = getMemberName(member.key);
|
|
1162
|
+
if (!FIELD_NODE_TYPES$2.has(member.type)) continue;
|
|
1163
|
+
const fieldName = getMemberName$1(member.key);
|
|
1124
1164
|
if (!fieldName) continue;
|
|
1125
1165
|
const typeNode = member.typeAnnotation?.typeAnnotation;
|
|
1126
1166
|
const kind = getSubjectKindFromConstructor(context, member.value) ?? getSubjectKindFromType(context, typeNode);
|
|
@@ -1136,7 +1176,7 @@ function collectFieldUsages(classBody, fields) {
|
|
|
1136
1176
|
const usages = /* @__PURE__ */ new Map();
|
|
1137
1177
|
const ngOnDestroyMethods = /* @__PURE__ */ new Set();
|
|
1138
1178
|
for (const member of classBody.body ?? []) if (isNgOnDestroyMethod(member)) ngOnDestroyMethods.add(member.value);
|
|
1139
|
-
walkNode(classBody, (node) => {
|
|
1179
|
+
walkNode$1(classBody, (node) => {
|
|
1140
1180
|
const call = isCallOnThisField(node, fields);
|
|
1141
1181
|
if (!call) return;
|
|
1142
1182
|
const usage = getUsage(usages, call.fieldName);
|
|
@@ -1174,7 +1214,7 @@ const avoidRxjsStateInComponent = defineRule({
|
|
|
1174
1214
|
return { ClassBody(node) {
|
|
1175
1215
|
const classBody = node;
|
|
1176
1216
|
const classNode = classBody.parent;
|
|
1177
|
-
if (!hasTargetDecorator$
|
|
1217
|
+
if (!hasTargetDecorator$3(context, classNode)) return;
|
|
1178
1218
|
const fields = collectSubjectFields(context, classBody);
|
|
1179
1219
|
const usages = collectFieldUsages(classBody, fields);
|
|
1180
1220
|
for (const [fieldName, field] of fields) {
|
|
@@ -1417,7 +1457,7 @@ function getNodeName(node) {
|
|
|
1417
1457
|
function getBaseFilename$1(filename) {
|
|
1418
1458
|
return filename.split(/[/\\]/u).at(-1) ?? filename;
|
|
1419
1459
|
}
|
|
1420
|
-
function hasTargetDecorator$
|
|
1460
|
+
function hasTargetDecorator$2(context, classNode, decoratorNames) {
|
|
1421
1461
|
if (!Array.isArray(classNode.decorators)) return false;
|
|
1422
1462
|
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorNames));
|
|
1423
1463
|
}
|
|
@@ -1471,7 +1511,7 @@ const classMatchesFilename = defineRule({
|
|
|
1471
1511
|
ClassDeclaration(node) {
|
|
1472
1512
|
if (!fileMatcher) return;
|
|
1473
1513
|
const classNode = node;
|
|
1474
|
-
if (hasTargetDecorator$
|
|
1514
|
+
if (hasTargetDecorator$2(context, classNode, fileDecoratorNames)) decoratedClasses.push(classNode);
|
|
1475
1515
|
},
|
|
1476
1516
|
after() {
|
|
1477
1517
|
const filename = context.filename ?? "";
|
|
@@ -1682,6 +1722,284 @@ const decoratorFilenameSuffix = defineRule({
|
|
|
1682
1722
|
}
|
|
1683
1723
|
});
|
|
1684
1724
|
//#endregion
|
|
1725
|
+
//#region src/rules/no-manual-change-detection/index.ts
|
|
1726
|
+
const ANGULAR_CORE_SOURCE$1 = "@angular/core";
|
|
1727
|
+
const CHANGE_DETECTOR_REF_NAMES = new Set(["ChangeDetectorRef"]);
|
|
1728
|
+
const INJECT_NAMES$1 = new Set(["inject"]);
|
|
1729
|
+
const MANUAL_CHANGE_DETECTION_METHODS = new Set([
|
|
1730
|
+
"checkNoChanges",
|
|
1731
|
+
"detach",
|
|
1732
|
+
"detectChanges",
|
|
1733
|
+
"markForCheck",
|
|
1734
|
+
"reattach"
|
|
1735
|
+
]);
|
|
1736
|
+
const FIELD_NODE_TYPES$1 = new Set([
|
|
1737
|
+
"AccessorProperty",
|
|
1738
|
+
"FieldDefinition",
|
|
1739
|
+
"PropertyDefinition"
|
|
1740
|
+
]);
|
|
1741
|
+
function isChangeDetectorRefReference(context, node) {
|
|
1742
|
+
return isImportedReference(context, node, ANGULAR_CORE_SOURCE$1, CHANGE_DETECTOR_REF_NAMES) || isImportedNamespaceMember(context, node, ANGULAR_CORE_SOURCE$1, CHANGE_DETECTOR_REF_NAMES);
|
|
1743
|
+
}
|
|
1744
|
+
function isInjectCall(context, node) {
|
|
1745
|
+
const expression = unwrapExpression(node);
|
|
1746
|
+
if (expression?.type !== "CallExpression") return false;
|
|
1747
|
+
return isImportedReference(context, expression.callee, ANGULAR_CORE_SOURCE$1, INJECT_NAMES$1) && isChangeDetectorRefReference(context, expression.arguments?.[0]);
|
|
1748
|
+
}
|
|
1749
|
+
function isChangeDetectorRefType(context, node) {
|
|
1750
|
+
if (!node) return false;
|
|
1751
|
+
if (node.type === "TSTypeReference") {
|
|
1752
|
+
if (isChangeDetectorRefReference(context, node.typeName)) return true;
|
|
1753
|
+
if (node.typeName?.type === "TSQualifiedName" && node.typeName.left?.type === "Identifier") return isNamespaceImport(context, node.typeName.left, ANGULAR_CORE_SOURCE$1) && getPropertyName(node.typeName.right) === "ChangeDetectorRef";
|
|
1754
|
+
}
|
|
1755
|
+
return false;
|
|
1756
|
+
}
|
|
1757
|
+
function getMemberName(node) {
|
|
1758
|
+
const expression = unwrapExpression(node);
|
|
1759
|
+
if (!expression) return null;
|
|
1760
|
+
if (expression.type === "Identifier") return expression.name;
|
|
1761
|
+
if (expression.type === "PrivateIdentifier") return expression.name;
|
|
1762
|
+
return null;
|
|
1763
|
+
}
|
|
1764
|
+
function getParameterName(node) {
|
|
1765
|
+
const parameter = node?.type === "TSParameterProperty" ? node.parameter : node;
|
|
1766
|
+
if (parameter?.type === "Identifier") return parameter.name;
|
|
1767
|
+
if (parameter?.type === "AssignmentPattern" && parameter.left?.type === "Identifier") return parameter.left.name;
|
|
1768
|
+
return null;
|
|
1769
|
+
}
|
|
1770
|
+
function getParameterType(node) {
|
|
1771
|
+
const parameter = node?.type === "TSParameterProperty" ? node.parameter : node;
|
|
1772
|
+
if (parameter?.type === "AssignmentPattern") return parameter.left?.typeAnnotation?.typeAnnotation ?? null;
|
|
1773
|
+
return parameter?.typeAnnotation?.typeAnnotation ?? null;
|
|
1774
|
+
}
|
|
1775
|
+
function getParameterBinding(node) {
|
|
1776
|
+
const parameter = node?.type === "TSParameterProperty" ? node.parameter : node;
|
|
1777
|
+
if (parameter?.type === "Identifier") return parameter;
|
|
1778
|
+
if (parameter?.type === "AssignmentPattern" && parameter.left?.type === "Identifier") return parameter.left;
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
function collectChangeDetectorRefBindings(context, classNode) {
|
|
1782
|
+
const fields = /* @__PURE__ */ new Set();
|
|
1783
|
+
const parameters = /* @__PURE__ */ new Set();
|
|
1784
|
+
for (const member of classNode.body?.body ?? []) {
|
|
1785
|
+
if (member.type === "MethodDefinition" && getPropertyName(member.key) === "constructor") for (const parameter of member.value?.params ?? []) {
|
|
1786
|
+
const name = getParameterName(parameter);
|
|
1787
|
+
if (name && isChangeDetectorRefType(context, getParameterType(parameter))) {
|
|
1788
|
+
const binding = getParameterBinding(parameter);
|
|
1789
|
+
if (binding) parameters.add(binding);
|
|
1790
|
+
if (parameter.type === "TSParameterProperty") fields.add(name);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
if (!FIELD_NODE_TYPES$1.has(member.type)) continue;
|
|
1794
|
+
const name = getMemberName(member.key);
|
|
1795
|
+
if (!name) continue;
|
|
1796
|
+
const typeNode = member.typeAnnotation?.typeAnnotation;
|
|
1797
|
+
if (isChangeDetectorRefType(context, typeNode) || isInjectCall(context, member.value)) fields.add(name);
|
|
1798
|
+
}
|
|
1799
|
+
return {
|
|
1800
|
+
fields,
|
|
1801
|
+
parameters
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
function getManualChangeDetectionCall(context, callNode, bindings) {
|
|
1805
|
+
const callee = unwrapExpression(callNode.callee);
|
|
1806
|
+
if (callee?.type !== "MemberExpression") return null;
|
|
1807
|
+
const methodName = getPropertyName(callee.property);
|
|
1808
|
+
if (!methodName || !MANUAL_CHANGE_DETECTION_METHODS.has(methodName)) return null;
|
|
1809
|
+
const object = unwrapExpression(callee.object);
|
|
1810
|
+
if (object?.type === "Identifier") {
|
|
1811
|
+
const binding = findNearestBindingIdentifier(context, object);
|
|
1812
|
+
return binding && bindings.parameters.has(binding) ? {
|
|
1813
|
+
methodName,
|
|
1814
|
+
node: callee.property ?? callee
|
|
1815
|
+
} : null;
|
|
1816
|
+
}
|
|
1817
|
+
if (object?.type !== "MemberExpression") return null;
|
|
1818
|
+
if (object.object?.type !== "ThisExpression") return null;
|
|
1819
|
+
const fieldName = getPropertyName(object.property);
|
|
1820
|
+
return fieldName && bindings.fields.has(fieldName) ? {
|
|
1821
|
+
methodName,
|
|
1822
|
+
node: callee.property ?? callee
|
|
1823
|
+
} : null;
|
|
1824
|
+
}
|
|
1825
|
+
function walkNode(node, visitor) {
|
|
1826
|
+
if (!node) return;
|
|
1827
|
+
if (Array.isArray(node)) {
|
|
1828
|
+
for (const child of node) walkNode(child, visitor);
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
visitor(node);
|
|
1832
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1833
|
+
if (key === "parent") continue;
|
|
1834
|
+
if (!value || typeof value !== "object") continue;
|
|
1835
|
+
walkNode(value, visitor);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
const noManualChangeDetection = defineRule({
|
|
1839
|
+
meta: {
|
|
1840
|
+
type: "suggestion",
|
|
1841
|
+
docs: {
|
|
1842
|
+
description: "Disallow manual Angular change detection through ChangeDetectorRef APIs.",
|
|
1843
|
+
recommended: true
|
|
1844
|
+
},
|
|
1845
|
+
schema: [],
|
|
1846
|
+
messages: { noManualChangeDetection: "Avoid manual change detection with ChangeDetectorRef. Prefer Angular's normal change detection triggers, signals, async bindings, or input updates." }
|
|
1847
|
+
},
|
|
1848
|
+
createOnce(context) {
|
|
1849
|
+
return { ClassDeclaration(node) {
|
|
1850
|
+
const classNode = node;
|
|
1851
|
+
const changeDetectorRefBindings = collectChangeDetectorRefBindings(context, classNode);
|
|
1852
|
+
if (changeDetectorRefBindings.fields.size === 0 && changeDetectorRefBindings.parameters.size === 0) return;
|
|
1853
|
+
walkNode(classNode.body, (child) => {
|
|
1854
|
+
if (child.type !== "CallExpression") return;
|
|
1855
|
+
const manualCall = getManualChangeDetectionCall(context, child, changeDetectorRefBindings);
|
|
1856
|
+
if (!manualCall) return;
|
|
1857
|
+
context.report({
|
|
1858
|
+
node: manualCall.node,
|
|
1859
|
+
messageId: "noManualChangeDetection"
|
|
1860
|
+
});
|
|
1861
|
+
});
|
|
1862
|
+
} };
|
|
1863
|
+
}
|
|
1864
|
+
});
|
|
1865
|
+
//#endregion
|
|
1866
|
+
//#region src/rules/no-resource-api/index.ts
|
|
1867
|
+
const RESOURCE_APIS = new Set(["resource"]);
|
|
1868
|
+
const RXJS_RESOURCE_APIS = new Set(["rxResource"]);
|
|
1869
|
+
const HTTP_RESOURCE_APIS = new Set(["httpResource"]);
|
|
1870
|
+
function isResourceApiCall(context, callNode) {
|
|
1871
|
+
const callee = callNode.callee;
|
|
1872
|
+
if (isImportedReference(context, callee, "@angular/core", RESOURCE_APIS)) return true;
|
|
1873
|
+
if (isImportedNamespaceMember(context, callee, "@angular/core", RESOURCE_APIS)) return true;
|
|
1874
|
+
if (isImportedReference(context, callee, "@angular/core/rxjs-interop", RXJS_RESOURCE_APIS)) return true;
|
|
1875
|
+
if (isImportedNamespaceMember(context, callee, "@angular/core/rxjs-interop", RXJS_RESOURCE_APIS)) return true;
|
|
1876
|
+
if (isImportedReference(context, callee, "@angular/common/http", HTTP_RESOURCE_APIS)) return true;
|
|
1877
|
+
if (isImportedNamespaceMember(context, callee, "@angular/common/http", HTTP_RESOURCE_APIS)) return true;
|
|
1878
|
+
if (callee?.type === "MemberExpression" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier" && isNamespaceImport(context, callee.object.object, "@angular/common/http") && HTTP_RESOURCE_APIS.has(getPropertyName(callee.object.property) ?? "")) return true;
|
|
1879
|
+
return callee?.type === "MemberExpression" && isImportedReference(context, callee.object, "@angular/common/http", HTTP_RESOURCE_APIS);
|
|
1880
|
+
}
|
|
1881
|
+
const noResourceApi = defineRule({
|
|
1882
|
+
meta: {
|
|
1883
|
+
type: "suggestion",
|
|
1884
|
+
docs: {
|
|
1885
|
+
description: "Disallow Angular resource APIs in favor of a dedicated server-state library.",
|
|
1886
|
+
recommended: true
|
|
1887
|
+
},
|
|
1888
|
+
schema: [],
|
|
1889
|
+
messages: { noResourceApi: "Avoid Angular resource APIs for server state. Prefer a dedicated server-state helper such as TanStack Query." }
|
|
1890
|
+
},
|
|
1891
|
+
createOnce(context) {
|
|
1892
|
+
return { CallExpression(node) {
|
|
1893
|
+
const callNode = node;
|
|
1894
|
+
if (!isResourceApiCall(context, callNode)) return;
|
|
1895
|
+
context.report({
|
|
1896
|
+
node: callNode.callee ?? callNode,
|
|
1897
|
+
messageId: "noResourceApi"
|
|
1898
|
+
});
|
|
1899
|
+
} };
|
|
1900
|
+
}
|
|
1901
|
+
});
|
|
1902
|
+
//#endregion
|
|
1903
|
+
//#region src/rules/no-route-resolvers/index.ts
|
|
1904
|
+
const ROUTE_TYPE_NAMES$1 = new Set(["Route"]);
|
|
1905
|
+
const ROUTES_TYPE_NAMES$1 = new Set(["Routes"]);
|
|
1906
|
+
function isImportedTypeName$1(context, typeNode, importedNames) {
|
|
1907
|
+
if (typeNode?.type === "Identifier") {
|
|
1908
|
+
const importedName = getImportedName(context, typeNode, "@angular/router");
|
|
1909
|
+
return !!importedName && importedNames.has(importedName);
|
|
1910
|
+
}
|
|
1911
|
+
return typeNode?.type === "TSQualifiedName" && typeNode.left?.type === "Identifier" && isNamespaceImport(context, typeNode.left, "@angular/router") && importedNames.has(getPropertyName(typeNode.right) ?? "");
|
|
1912
|
+
}
|
|
1913
|
+
function getTypeParameterNodes$1(typeNode) {
|
|
1914
|
+
return typeNode.typeParameters?.params ?? typeNode.typeArguments?.params ?? [];
|
|
1915
|
+
}
|
|
1916
|
+
function isRouteType$1(context, typeNode) {
|
|
1917
|
+
return typeNode?.type === "TSTypeReference" && isImportedTypeName$1(context, typeNode.typeName, ROUTE_TYPE_NAMES$1);
|
|
1918
|
+
}
|
|
1919
|
+
function isRouteArrayType$1(context, typeNode) {
|
|
1920
|
+
if (!typeNode) return false;
|
|
1921
|
+
if (typeNode.type === "TSTypeReference" && isImportedTypeName$1(context, typeNode.typeName, ROUTES_TYPE_NAMES$1)) return true;
|
|
1922
|
+
if (typeNode.type === "TSArrayType") return isRouteType$1(context, typeNode.elementType);
|
|
1923
|
+
if (typeNode.type !== "TSTypeReference") return false;
|
|
1924
|
+
if (getPropertyName(typeNode.typeName) !== "Array" && getPropertyName(typeNode.typeName) !== "ReadonlyArray") return false;
|
|
1925
|
+
const [elementType] = getTypeParameterNodes$1(typeNode);
|
|
1926
|
+
return isRouteType$1(context, elementType);
|
|
1927
|
+
}
|
|
1928
|
+
function getTypeAnnotation(node) {
|
|
1929
|
+
const typeAnnotation = node?.typeAnnotation;
|
|
1930
|
+
return typeAnnotation?.type === "TSTypeAnnotation" ? typeAnnotation.typeAnnotation : null;
|
|
1931
|
+
}
|
|
1932
|
+
const noRouteResolvers = defineRule({
|
|
1933
|
+
meta: {
|
|
1934
|
+
type: "suggestion",
|
|
1935
|
+
docs: {
|
|
1936
|
+
description: "Disallow Angular route resolvers in typed Route/Routes declarations.",
|
|
1937
|
+
recommended: true
|
|
1938
|
+
},
|
|
1939
|
+
schema: [],
|
|
1940
|
+
messages: { noRouteResolvers: "Avoid Angular route resolvers for data loading. Prefer component-level loading with signals or server-state helpers." }
|
|
1941
|
+
},
|
|
1942
|
+
createOnce(context) {
|
|
1943
|
+
function reportResolveInRouteObject(routeObject) {
|
|
1944
|
+
for (const property of routeObject.properties ?? []) {
|
|
1945
|
+
if (property.type !== "Property" || property.computed) continue;
|
|
1946
|
+
const propertyName = getPropertyName(property.key);
|
|
1947
|
+
if (propertyName === "resolve") {
|
|
1948
|
+
context.report({
|
|
1949
|
+
node: property.key ?? property,
|
|
1950
|
+
messageId: "noRouteResolvers"
|
|
1951
|
+
});
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
if (propertyName === "children" && property.value?.type === "ArrayExpression") reportResolveInRouteArray(property.value);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
function reportResolveInRouteArray(routeArray) {
|
|
1958
|
+
for (const element of routeArray.elements ?? []) if (element?.type === "ObjectExpression") reportResolveInRouteObject(element);
|
|
1959
|
+
}
|
|
1960
|
+
return { VariableDeclarator(node) {
|
|
1961
|
+
const declarator = node;
|
|
1962
|
+
const typeNode = getTypeAnnotation(declarator.id);
|
|
1963
|
+
if (isRouteArrayType$1(context, typeNode) && declarator.init?.type === "ArrayExpression") {
|
|
1964
|
+
reportResolveInRouteArray(declarator.init);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
if (isRouteType$1(context, typeNode) && declarator.init?.type === "ObjectExpression") reportResolveInRouteObject(declarator.init);
|
|
1968
|
+
} };
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
//#endregion
|
|
1972
|
+
//#region src/rules/no-ui-inheritance/index.ts
|
|
1973
|
+
const UI_DECORATORS = new Set(["Component", "Directive"]);
|
|
1974
|
+
function hasUiDecorator(context, classNode) {
|
|
1975
|
+
if (!Array.isArray(classNode.decorators)) return false;
|
|
1976
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, UI_DECORATORS));
|
|
1977
|
+
}
|
|
1978
|
+
const noUiInheritance = defineRule({
|
|
1979
|
+
meta: {
|
|
1980
|
+
type: "suggestion",
|
|
1981
|
+
docs: {
|
|
1982
|
+
description: "Disallow inheritance for Angular components and directives.",
|
|
1983
|
+
recommended: true
|
|
1984
|
+
},
|
|
1985
|
+
schema: [],
|
|
1986
|
+
messages: { noUiInheritance: "Avoid inheritance for Angular {{kind}} classes. Prefer composition with services or inject* helpers." }
|
|
1987
|
+
},
|
|
1988
|
+
createOnce(context) {
|
|
1989
|
+
return { "ClassDeclaration, ClassExpression"(node) {
|
|
1990
|
+
const classNode = node;
|
|
1991
|
+
if (!classNode.superClass) return;
|
|
1992
|
+
if (!hasUiDecorator(context, classNode)) return;
|
|
1993
|
+
const kind = classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, new Set(["Component"]))) ? "component" : "directive";
|
|
1994
|
+
context.report({
|
|
1995
|
+
node: classNode.superClass,
|
|
1996
|
+
messageId: "noUiInheritance",
|
|
1997
|
+
data: { kind }
|
|
1998
|
+
});
|
|
1999
|
+
} };
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
//#endregion
|
|
1685
2003
|
//#region src/rules/prefer-load-component-over-load-children/index.ts
|
|
1686
2004
|
const ROUTE_TYPE_NAMES = new Set(["Route"]);
|
|
1687
2005
|
const ROUTES_TYPE_NAMES = new Set(["Routes"]);
|
|
@@ -1966,7 +2284,7 @@ const preferStyleUrl = defineRule({
|
|
|
1966
2284
|
});
|
|
1967
2285
|
//#endregion
|
|
1968
2286
|
//#region src/rules/public-component-interface/index.ts
|
|
1969
|
-
const TARGET_DECORATORS = new Set(["Component", "Directive"]);
|
|
2287
|
+
const TARGET_DECORATORS$1 = new Set(["Component", "Directive"]);
|
|
1970
2288
|
const INPUT_MODEL_APIS = new Set(["input", "model"]);
|
|
1971
2289
|
const OUTPUT_APIS = new Set(["output", "outputFromObservable"]);
|
|
1972
2290
|
const INJECT_APIS = new Set(["inject"]);
|
|
@@ -1975,9 +2293,9 @@ const FIELD_NODE_TYPES = new Set([
|
|
|
1975
2293
|
"FieldDefinition",
|
|
1976
2294
|
"PropertyDefinition"
|
|
1977
2295
|
]);
|
|
1978
|
-
function hasTargetDecorator(context, classNode) {
|
|
2296
|
+
function hasTargetDecorator$1(context, classNode) {
|
|
1979
2297
|
if (!classNode || !Array.isArray(classNode.decorators)) return false;
|
|
1980
|
-
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS));
|
|
2298
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$1));
|
|
1981
2299
|
}
|
|
1982
2300
|
function isApiCallFromAngularCore(context, node, apiNames) {
|
|
1983
2301
|
if (!node || node.type !== "CallExpression") return false;
|
|
@@ -2036,7 +2354,7 @@ const publicComponentInterface = defineRule({
|
|
|
2036
2354
|
createOnce(context) {
|
|
2037
2355
|
return { ClassBody(node) {
|
|
2038
2356
|
const classNode = node.parent;
|
|
2039
|
-
if (!hasTargetDecorator(context, classNode)) return;
|
|
2357
|
+
if (!hasTargetDecorator$1(context, classNode)) return;
|
|
2040
2358
|
for (const member of node.body ?? []) {
|
|
2041
2359
|
if (!FIELD_NODE_TYPES.has(member.type)) continue;
|
|
2042
2360
|
const isInputModelMember = isApiCallFromAngularCore(context, member.value, INPUT_MODEL_APIS);
|
|
@@ -2119,7 +2437,7 @@ const restrictInjectableProvidedIn = defineRule({
|
|
|
2119
2437
|
//#endregion
|
|
2120
2438
|
//#region src/rules/rules-of-inject/index.ts
|
|
2121
2439
|
const DEFAULT_ALLOWED_FUNCTION_NAMES = [];
|
|
2122
|
-
const DEFAULT_INJECT_FUNCTION_PREFIXES = ["
|
|
2440
|
+
const DEFAULT_INJECT_FUNCTION_PREFIXES = ["inject"];
|
|
2123
2441
|
const DEFAULT_INJECT_FUNCTION_SUFFIXES = ["Guard"];
|
|
2124
2442
|
const DEFAULT_RUNS_IN_INJECTION_CONTEXT = [];
|
|
2125
2443
|
const ROUTER_CONTEXT_PROPERTY_NAMES = new Set([
|
|
@@ -2184,18 +2502,18 @@ const INJECTION_CONTEXT_FUNCTION_TYPE_NAMES = new Set([
|
|
|
2184
2502
|
"RedirectFunction",
|
|
2185
2503
|
"ResolveFn"
|
|
2186
2504
|
]);
|
|
2187
|
-
const FUNCTION_TYPES = new Set([
|
|
2505
|
+
const FUNCTION_TYPES$1 = new Set([
|
|
2188
2506
|
"ArrowFunctionExpression",
|
|
2189
2507
|
"FunctionDeclaration",
|
|
2190
2508
|
"FunctionExpression"
|
|
2191
2509
|
]);
|
|
2192
|
-
const CLASS_FIELD_TYPES = new Set([
|
|
2510
|
+
const CLASS_FIELD_TYPES$1 = new Set([
|
|
2193
2511
|
"AccessorProperty",
|
|
2194
2512
|
"FieldDefinition",
|
|
2195
2513
|
"PropertyDefinition"
|
|
2196
2514
|
]);
|
|
2197
2515
|
function isFunction(node) {
|
|
2198
|
-
return !!node && FUNCTION_TYPES.has(node.type);
|
|
2516
|
+
return !!node && FUNCTION_TYPES$1.has(node.type);
|
|
2199
2517
|
}
|
|
2200
2518
|
function getAncestors(context, node) {
|
|
2201
2519
|
return context.sourceCode.getAncestors(node);
|
|
@@ -2251,7 +2569,7 @@ function getFunctionContextTypeName(functionNode) {
|
|
|
2251
2569
|
const parent = skipTransparentExpressionParents(functionNode.parent);
|
|
2252
2570
|
if (parent?.type === "VariableDeclarator" && parent.id?.type === "Identifier") return getTypeName(parent.id.typeAnnotation?.typeAnnotation);
|
|
2253
2571
|
if (parent?.type === "Property" && parent.value === functionNode) return getTypeName(parent.typeAnnotation?.typeAnnotation);
|
|
2254
|
-
if (parent && CLASS_FIELD_TYPES.has(parent.type) && parent.value === functionNode) return getTypeName(parent.typeAnnotation?.typeAnnotation);
|
|
2572
|
+
if (parent && CLASS_FIELD_TYPES$1.has(parent.type) && parent.value === functionNode) return getTypeName(parent.typeAnnotation?.typeAnnotation);
|
|
2255
2573
|
return null;
|
|
2256
2574
|
}
|
|
2257
2575
|
function isTypedInjectionContextFunction(functionNode) {
|
|
@@ -2285,7 +2603,7 @@ function isConstructorFunction(functionNode) {
|
|
|
2285
2603
|
return functionNode.parent?.type === "MethodDefinition" && functionNode.parent.kind === "constructor" && functionNode.parent.value === functionNode;
|
|
2286
2604
|
}
|
|
2287
2605
|
function isDirectClassFieldInitializer(ancestors, nearestFunction) {
|
|
2288
|
-
return !!ancestors.findLast((ancestor) => CLASS_FIELD_TYPES.has(ancestor.type)) && !nearestFunction;
|
|
2606
|
+
return !!ancestors.findLast((ancestor) => CLASS_FIELD_TYPES$1.has(ancestor.type)) && !nearestFunction;
|
|
2289
2607
|
}
|
|
2290
2608
|
function hasSupportedAngularClassDecorator(classNode) {
|
|
2291
2609
|
if (!classNode) return false;
|
|
@@ -2329,6 +2647,494 @@ function isInjectLikeHelperCall(context, node, injectionContextApiLocalNames, in
|
|
|
2329
2647
|
if (runsInInjectionContextFunctionNames.has(callee.name)) return true;
|
|
2330
2648
|
return !injectionContextApiLocalNames.has(callee.name) && callee.name !== "inject" && (injectFunctionPrefixes.some((prefix) => callee.name.startsWith(prefix)) || injectFunctionSuffixes.some((suffix) => callee.name.endsWith(suffix)));
|
|
2331
2649
|
}
|
|
2650
|
+
const rulesOfInject = defineRule({
|
|
2651
|
+
meta: {
|
|
2652
|
+
type: "problem",
|
|
2653
|
+
docs: {
|
|
2654
|
+
description: "Require Angular APIs that depend on injection context to appear only in known injection contexts.",
|
|
2655
|
+
recommended: true
|
|
2656
|
+
},
|
|
2657
|
+
schema: [{
|
|
2658
|
+
type: "object",
|
|
2659
|
+
additionalProperties: false,
|
|
2660
|
+
properties: {
|
|
2661
|
+
allowedFunctionNames: {
|
|
2662
|
+
type: "array",
|
|
2663
|
+
items: { type: "string" },
|
|
2664
|
+
default: DEFAULT_ALLOWED_FUNCTION_NAMES
|
|
2665
|
+
},
|
|
2666
|
+
checkUnimportedInject: {
|
|
2667
|
+
type: "boolean",
|
|
2668
|
+
default: false
|
|
2669
|
+
},
|
|
2670
|
+
injectFunctionPrefixes: {
|
|
2671
|
+
type: "array",
|
|
2672
|
+
items: { type: "string" },
|
|
2673
|
+
default: DEFAULT_INJECT_FUNCTION_PREFIXES
|
|
2674
|
+
},
|
|
2675
|
+
injectFunctionSuffixes: {
|
|
2676
|
+
type: "array",
|
|
2677
|
+
items: { type: "string" },
|
|
2678
|
+
default: DEFAULT_INJECT_FUNCTION_SUFFIXES
|
|
2679
|
+
},
|
|
2680
|
+
runsInInjectionContext: {
|
|
2681
|
+
type: "array",
|
|
2682
|
+
items: {
|
|
2683
|
+
type: "object",
|
|
2684
|
+
additionalProperties: false,
|
|
2685
|
+
required: ["from", "imports"],
|
|
2686
|
+
properties: {
|
|
2687
|
+
from: { type: "string" },
|
|
2688
|
+
imports: { anyOf: [{ const: "all" }, {
|
|
2689
|
+
type: "array",
|
|
2690
|
+
items: { type: "string" }
|
|
2691
|
+
}] }
|
|
2692
|
+
}
|
|
2693
|
+
},
|
|
2694
|
+
default: DEFAULT_RUNS_IN_INJECTION_CONTEXT
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
}],
|
|
2698
|
+
messages: { disallowedInject: "Angular APIs that depend on injection context must be called from an injection context: a class field initializer or constructor in an Angular-decorated class, provider factory, InjectionToken factory, runInInjectionContext/runInContext callback, Angular route callback property (for example loadComponent/canActivate), an inject* or *Guard function, or configured allowed function." }
|
|
2699
|
+
},
|
|
2700
|
+
createOnce(context) {
|
|
2701
|
+
const injectionContextApiLocalNames = /* @__PURE__ */ new Set();
|
|
2702
|
+
const injectionContextApiNamespaceMembers = /* @__PURE__ */ new Map();
|
|
2703
|
+
const runsInInjectionContextFunctionNames = /* @__PURE__ */ new Set();
|
|
2704
|
+
let runsInInjectionContextRules = [];
|
|
2705
|
+
return {
|
|
2706
|
+
before() {
|
|
2707
|
+
injectionContextApiLocalNames.clear();
|
|
2708
|
+
injectionContextApiNamespaceMembers.clear();
|
|
2709
|
+
runsInInjectionContextFunctionNames.clear();
|
|
2710
|
+
runsInInjectionContextRules = (context.options[0] ?? {}).runsInInjectionContext ?? DEFAULT_RUNS_IN_INJECTION_CONTEXT;
|
|
2711
|
+
},
|
|
2712
|
+
ImportDeclaration(node) {
|
|
2713
|
+
const source = node.source?.value;
|
|
2714
|
+
const matchingRule = typeof source === "string" ? runsInInjectionContextRules.find((rule) => rule.from === source) : null;
|
|
2715
|
+
if (matchingRule) for (const specifier of node.specifiers ?? []) {
|
|
2716
|
+
if (specifier.type === "ImportSpecifier" && (matchingRule.imports === "all" || matchingRule.imports.includes(getPropertyName(specifier.imported) ?? ""))) runsInInjectionContextFunctionNames.add(specifier.local.name);
|
|
2717
|
+
if ((specifier.type === "ImportDefaultSpecifier" || specifier.type === "ImportNamespaceSpecifier") && matchingRule.imports === "all") runsInInjectionContextFunctionNames.add(specifier.local.name);
|
|
2718
|
+
}
|
|
2719
|
+
const knownApiImports = typeof source === "string" ? getKnownInjectionContextApiImports(source) : null;
|
|
2720
|
+
if (knownApiImports) for (const specifier of node.specifiers ?? []) {
|
|
2721
|
+
if (specifier.type === "ImportSpecifier" && knownApiImports.has(getPropertyName(specifier.imported) ?? "")) injectionContextApiLocalNames.add(specifier.local.name);
|
|
2722
|
+
if (specifier.type === "ImportNamespaceSpecifier") injectionContextApiNamespaceMembers.set(specifier.local.name, knownApiImports);
|
|
2723
|
+
}
|
|
2724
|
+
},
|
|
2725
|
+
CallExpression(node) {
|
|
2726
|
+
const options = context.options[0] ?? {};
|
|
2727
|
+
const allowedFunctionNames = new Set(options.allowedFunctionNames ?? DEFAULT_ALLOWED_FUNCTION_NAMES);
|
|
2728
|
+
const checkUnimportedInject = options.checkUnimportedInject ?? false;
|
|
2729
|
+
const injectFunctionPrefixes = options.injectFunctionPrefixes ?? DEFAULT_INJECT_FUNCTION_PREFIXES;
|
|
2730
|
+
const injectFunctionSuffixes = options.injectFunctionSuffixes ?? DEFAULT_INJECT_FUNCTION_SUFFIXES;
|
|
2731
|
+
const inAllowedContext = isAllowedInjectionContext(context, node, allowedFunctionNames, injectFunctionPrefixes, injectFunctionSuffixes);
|
|
2732
|
+
if (isInjectLikeHelperCall(context, node, injectionContextApiLocalNames, injectFunctionPrefixes, injectFunctionSuffixes, runsInInjectionContextFunctionNames) && !inAllowedContext) {
|
|
2733
|
+
context.report({
|
|
2734
|
+
node: node.callee,
|
|
2735
|
+
messageId: "disallowedInject"
|
|
2736
|
+
});
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
if (!isKnownInjectionContextApiCall(context, node, injectionContextApiLocalNames, injectionContextApiNamespaceMembers, checkUnimportedInject)) return;
|
|
2740
|
+
if (inAllowedContext) return;
|
|
2741
|
+
context.report({
|
|
2742
|
+
node: node.callee,
|
|
2743
|
+
messageId: "disallowedInject"
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
});
|
|
2749
|
+
//#endregion
|
|
2750
|
+
//#region src/utilities/tanstack-query.ts
|
|
2751
|
+
const DEFAULT_TANSTACK_QUERY_SOURCES = [
|
|
2752
|
+
"@tanstack/angular-query",
|
|
2753
|
+
"@benjavicente/angular-query",
|
|
2754
|
+
"@tanstack/angular-query-experimental"
|
|
2755
|
+
];
|
|
2756
|
+
const QUERY_CORE_SOURCE = "@tanstack/query-core";
|
|
2757
|
+
const QUERY_OPTIONS_BUILDERS = new Set(["queryOptions", "infiniteQueryOptions"]);
|
|
2758
|
+
function getTanstackQuerySources(options) {
|
|
2759
|
+
return new Set(options.tanstackQuerySources ?? DEFAULT_TANSTACK_QUERY_SOURCES);
|
|
2760
|
+
}
|
|
2761
|
+
function getTanstackQueryImportName(context, node, sources) {
|
|
2762
|
+
const expression = unwrapExpression(node);
|
|
2763
|
+
if (expression?.type === "Identifier") {
|
|
2764
|
+
for (const source of sources) {
|
|
2765
|
+
const importedName = getImportedName(context, expression, source);
|
|
2766
|
+
if (importedName) return importedName;
|
|
2767
|
+
}
|
|
2768
|
+
return null;
|
|
2769
|
+
}
|
|
2770
|
+
if (expression?.type !== "MemberExpression" || expression.object?.type !== "Identifier") return null;
|
|
2771
|
+
for (const source of sources) if (isNamespaceImport(context, expression.object, source)) return getPropertyName(expression.property);
|
|
2772
|
+
return null;
|
|
2773
|
+
}
|
|
2774
|
+
function isTanstackQueryImportedReference(context, node, sources, names) {
|
|
2775
|
+
for (const source of sources) if (isImportedReference(context, node, source, names)) return true;
|
|
2776
|
+
return false;
|
|
2777
|
+
}
|
|
2778
|
+
//#endregion
|
|
2779
|
+
//#region src/rules/tanstack-query-injects-only-in-component-body/index.ts
|
|
2780
|
+
const TARGET_DECORATORS = new Set(["Component", "Directive"]);
|
|
2781
|
+
const TANSTACK_QUERY_APIS = new Set(["injectQuery", "injectMutation"]);
|
|
2782
|
+
const CLASS_FIELD_TYPES = new Set([
|
|
2783
|
+
"AccessorProperty",
|
|
2784
|
+
"FieldDefinition",
|
|
2785
|
+
"PropertyDefinition"
|
|
2786
|
+
]);
|
|
2787
|
+
const FUNCTION_TYPES = new Set([
|
|
2788
|
+
"ArrowFunctionExpression",
|
|
2789
|
+
"FunctionDeclaration",
|
|
2790
|
+
"FunctionExpression"
|
|
2791
|
+
]);
|
|
2792
|
+
function hasTargetDecorator(context, classNode) {
|
|
2793
|
+
if (!classNode || !Array.isArray(classNode.decorators)) return false;
|
|
2794
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS));
|
|
2795
|
+
}
|
|
2796
|
+
function isTanstackQueryInjectCall(context, callNode, tanstackQuerySources) {
|
|
2797
|
+
return isTanstackQueryImportedReference(context, callNode.callee, tanstackQuerySources, TANSTACK_QUERY_APIS);
|
|
2798
|
+
}
|
|
2799
|
+
function isDirectComponentOrDirectiveFieldInitializer(context, callNode) {
|
|
2800
|
+
const ancestors = context.sourceCode.getAncestors(callNode);
|
|
2801
|
+
const classField = ancestors.findLast((ancestor) => CLASS_FIELD_TYPES.has(ancestor.type));
|
|
2802
|
+
if (!classField || unwrapExpression(classField.value) !== callNode) return false;
|
|
2803
|
+
const classBodyIndex = ancestors.findLastIndex((ancestor) => ancestor.type === "ClassBody");
|
|
2804
|
+
if (classBodyIndex === -1) return false;
|
|
2805
|
+
const classBody = ancestors[classBodyIndex];
|
|
2806
|
+
if (ancestors.slice(classBodyIndex + 1).some((ancestor) => FUNCTION_TYPES.has(ancestor.type))) return false;
|
|
2807
|
+
return hasTargetDecorator(context, classBody?.parent);
|
|
2808
|
+
}
|
|
2809
|
+
const tanstackQueryInjectsOnlyInComponentBody = defineRule({
|
|
2810
|
+
meta: {
|
|
2811
|
+
type: "problem",
|
|
2812
|
+
docs: {
|
|
2813
|
+
description: "Require Angular TanStack Query injectQuery/injectMutation calls to be direct component/directive class fields.",
|
|
2814
|
+
recommended: true
|
|
2815
|
+
},
|
|
2816
|
+
schema: [{
|
|
2817
|
+
type: "object",
|
|
2818
|
+
additionalProperties: false,
|
|
2819
|
+
properties: { tanstackQuerySources: {
|
|
2820
|
+
type: "array",
|
|
2821
|
+
items: { type: "string" },
|
|
2822
|
+
default: DEFAULT_TANSTACK_QUERY_SOURCES
|
|
2823
|
+
} }
|
|
2824
|
+
}],
|
|
2825
|
+
messages: { onlyInComponentBody: "Call {{name}} only as a direct class field initializer in an Angular component or directive." }
|
|
2826
|
+
},
|
|
2827
|
+
createOnce(context) {
|
|
2828
|
+
let tanstackQuerySources = new Set(DEFAULT_TANSTACK_QUERY_SOURCES);
|
|
2829
|
+
return {
|
|
2830
|
+
before() {
|
|
2831
|
+
tanstackQuerySources = getTanstackQuerySources(context.options?.[0] ?? {});
|
|
2832
|
+
},
|
|
2833
|
+
CallExpression(node) {
|
|
2834
|
+
const callNode = node;
|
|
2835
|
+
if (!isTanstackQueryInjectCall(context, callNode, tanstackQuerySources)) return;
|
|
2836
|
+
if (isDirectComponentOrDirectiveFieldInitializer(context, callNode)) return;
|
|
2837
|
+
context.report({
|
|
2838
|
+
node: callNode.callee ?? callNode,
|
|
2839
|
+
messageId: "onlyInComponentBody",
|
|
2840
|
+
data: { name: callNode.callee?.name ?? "this TanStack Query inject API" }
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
2845
|
+
});
|
|
2846
|
+
//#endregion
|
|
2847
|
+
//#region src/rules/tanstack-query-inlined-keys/index.ts
|
|
2848
|
+
function getProperty$1(node, name) {
|
|
2849
|
+
for (const property of node.properties ?? []) if (property.type === "Property" && getPropertyName(property.key) === name) return property;
|
|
2850
|
+
return null;
|
|
2851
|
+
}
|
|
2852
|
+
function isInlineArrayExpression$1(node) {
|
|
2853
|
+
return unwrapExpression(node)?.type === "ArrayExpression";
|
|
2854
|
+
}
|
|
2855
|
+
const tanstackQueryInlinedKeys = defineRule({
|
|
2856
|
+
meta: {
|
|
2857
|
+
type: "problem",
|
|
2858
|
+
docs: {
|
|
2859
|
+
description: "Require TanStack Query queryOptions() keys to be inline arrays.",
|
|
2860
|
+
recommended: true
|
|
2861
|
+
},
|
|
2862
|
+
schema: [{
|
|
2863
|
+
type: "object",
|
|
2864
|
+
additionalProperties: false,
|
|
2865
|
+
properties: { tanstackQuerySources: {
|
|
2866
|
+
type: "array",
|
|
2867
|
+
items: { type: "string" },
|
|
2868
|
+
default: DEFAULT_TANSTACK_QUERY_SOURCES
|
|
2869
|
+
} }
|
|
2870
|
+
}],
|
|
2871
|
+
messages: { inlinedKeys: "Inline queryKey as an array in queryOptions(). Query keys are implementation details and should be read through query options." }
|
|
2872
|
+
},
|
|
2873
|
+
createOnce(context) {
|
|
2874
|
+
let tanstackQuerySources = new Set(DEFAULT_TANSTACK_QUERY_SOURCES);
|
|
2875
|
+
return {
|
|
2876
|
+
before() {
|
|
2877
|
+
tanstackQuerySources = getTanstackQuerySources(context.options?.[0] ?? {});
|
|
2878
|
+
},
|
|
2879
|
+
CallExpression(node) {
|
|
2880
|
+
const callNode = node;
|
|
2881
|
+
const importName = getTanstackQueryImportName(context, callNode.callee, tanstackQuerySources);
|
|
2882
|
+
if (!importName || !QUERY_OPTIONS_BUILDERS.has(importName)) return;
|
|
2883
|
+
const options = unwrapExpression(callNode.arguments?.[0]);
|
|
2884
|
+
if (options?.type !== "ObjectExpression") return;
|
|
2885
|
+
const queryKey = getProperty$1(options, "queryKey");
|
|
2886
|
+
if (!queryKey || isInlineArrayExpression$1(queryKey.value)) return;
|
|
2887
|
+
context.report({
|
|
2888
|
+
node: queryKey.value ?? queryKey,
|
|
2889
|
+
messageId: "inlinedKeys"
|
|
2890
|
+
});
|
|
2891
|
+
}
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
});
|
|
2895
|
+
//#endregion
|
|
2896
|
+
//#region src/rules/tanstack-query-prefer-query-options/index.ts
|
|
2897
|
+
const ANGULAR_CORE_SOURCE = "@angular/core";
|
|
2898
|
+
const INJECT_NAMES = new Set(["inject"]);
|
|
2899
|
+
const QUERY_INJECT_APIS = new Set(["injectQuery", "injectInfiniteQuery"]);
|
|
2900
|
+
const QUERIES_INJECT_APIS = new Set(["injectQueries"]);
|
|
2901
|
+
const FILTER_INJECT_APIS = new Set(["injectIsFetching"]);
|
|
2902
|
+
const QUERY_CLIENT_OPTION_METHODS = new Set([
|
|
2903
|
+
"ensureInfiniteQueryData",
|
|
2904
|
+
"ensureQueryData",
|
|
2905
|
+
"fetchInfiniteQuery",
|
|
2906
|
+
"fetchQuery",
|
|
2907
|
+
"prefetchInfiniteQuery",
|
|
2908
|
+
"prefetchQuery"
|
|
2909
|
+
]);
|
|
2910
|
+
const QUERY_CLIENT_QUERY_KEY_METHODS = new Set([
|
|
2911
|
+
"getQueryData",
|
|
2912
|
+
"getQueryDefaults",
|
|
2913
|
+
"getQueryState",
|
|
2914
|
+
"setQueryData",
|
|
2915
|
+
"setQueryDefaults"
|
|
2916
|
+
]);
|
|
2917
|
+
const QUERY_CLIENT_FILTER_METHODS = new Set([
|
|
2918
|
+
"cancelQueries",
|
|
2919
|
+
"getQueriesData",
|
|
2920
|
+
"invalidateQueries",
|
|
2921
|
+
"isFetching",
|
|
2922
|
+
"refetchQueries",
|
|
2923
|
+
"removeQueries",
|
|
2924
|
+
"resetQueries",
|
|
2925
|
+
"setQueriesData"
|
|
2926
|
+
]);
|
|
2927
|
+
const QUERY_CLIENT_NAMES = new Set(["QueryClient"]);
|
|
2928
|
+
const SKIP_TOKEN_NAMES = new Set(["skipToken"]);
|
|
2929
|
+
function getProperty(node, name) {
|
|
2930
|
+
for (const property of node.properties ?? []) if (property.type === "Property" && getPropertyName(property.key) === name) return property;
|
|
2931
|
+
return null;
|
|
2932
|
+
}
|
|
2933
|
+
function isObjectExpression(node) {
|
|
2934
|
+
return unwrapExpression(node)?.type === "ObjectExpression";
|
|
2935
|
+
}
|
|
2936
|
+
function isInlineArrayExpression(node) {
|
|
2937
|
+
return unwrapExpression(node)?.type === "ArrayExpression";
|
|
2938
|
+
}
|
|
2939
|
+
function isSkipToken(context, node, sources) {
|
|
2940
|
+
const expression = unwrapExpression(node);
|
|
2941
|
+
if (!expression) return false;
|
|
2942
|
+
if (expression.type === "ConditionalExpression") return isSkipToken(context, expression.consequent, sources) || isSkipToken(context, expression.alternate, sources);
|
|
2943
|
+
if (expression.type === "LogicalExpression") return isSkipToken(context, expression.left, sources) || isSkipToken(context, expression.right, sources);
|
|
2944
|
+
for (const source of [...sources, QUERY_CORE_SOURCE]) if (isImportedReference(context, expression, source, SKIP_TOKEN_NAMES)) return true;
|
|
2945
|
+
return false;
|
|
2946
|
+
}
|
|
2947
|
+
function hasObjectSpread(node) {
|
|
2948
|
+
return (node.properties ?? []).some((property) => property.type === "SpreadElement");
|
|
2949
|
+
}
|
|
2950
|
+
function hasInlineQueryOptions(context, node, sources) {
|
|
2951
|
+
if (getProperty(node, "queryKey")) return true;
|
|
2952
|
+
const queryFn = getProperty(node, "queryFn");
|
|
2953
|
+
if (!queryFn) return false;
|
|
2954
|
+
return !(hasObjectSpread(node) && isSkipToken(context, queryFn.value, sources));
|
|
2955
|
+
}
|
|
2956
|
+
function hasInlineFilterQueryKey(node) {
|
|
2957
|
+
const queryKey = getProperty(node, "queryKey")?.value;
|
|
2958
|
+
return isInlineArrayExpression(queryKey);
|
|
2959
|
+
}
|
|
2960
|
+
function getReturnedObjectExpressions(node) {
|
|
2961
|
+
const expression = unwrapExpression(node);
|
|
2962
|
+
if (!expression) return [];
|
|
2963
|
+
if (expression.type === "ObjectExpression") return [expression];
|
|
2964
|
+
if (expression.type === "ArrowFunctionExpression" || expression.type === "FunctionExpression") return getReturnedObjectExpressions(expression.body);
|
|
2965
|
+
if (expression.type === "BlockStatement") return (expression.body ?? []).flatMap((statement) => statement.type === "ReturnStatement" ? getReturnedObjectExpressions(statement.argument) : []);
|
|
2966
|
+
if (expression.type === "ConditionalExpression") return [...getReturnedObjectExpressions(expression.consequent), ...getReturnedObjectExpressions(expression.alternate)];
|
|
2967
|
+
if (expression.type === "LogicalExpression") return [...getReturnedObjectExpressions(expression.left), ...getReturnedObjectExpressions(expression.right)];
|
|
2968
|
+
if (expression.type === "SequenceExpression") return (expression.expressions ?? []).flatMap((child) => getReturnedObjectExpressions(child));
|
|
2969
|
+
return [];
|
|
2970
|
+
}
|
|
2971
|
+
function getQueryObjects(node) {
|
|
2972
|
+
const expression = unwrapExpression(node);
|
|
2973
|
+
if (!expression) return [];
|
|
2974
|
+
if (expression.type === "ArrayExpression") return (expression.elements ?? []).filter(isObjectExpression).map((element) => unwrapExpression(element));
|
|
2975
|
+
if (expression.type === "CallExpression" && expression.callee?.type === "MemberExpression" && getPropertyName(expression.callee.property) === "map") {
|
|
2976
|
+
const mapper = expression.arguments?.[0];
|
|
2977
|
+
if (mapper?.type === "ArrowFunctionExpression" || mapper?.type === "FunctionExpression") return getReturnedObjectExpressions(mapper);
|
|
2978
|
+
}
|
|
2979
|
+
return [];
|
|
2980
|
+
}
|
|
2981
|
+
function getBindingInitializer(binding) {
|
|
2982
|
+
const parent = binding?.parent;
|
|
2983
|
+
if (!parent) return null;
|
|
2984
|
+
if (parent.type === "VariableDeclarator" && parent.id === binding) return parent.init ?? null;
|
|
2985
|
+
if (parent.type === "AssignmentPattern" && parent.left === binding) return parent.right ?? null;
|
|
2986
|
+
return null;
|
|
2987
|
+
}
|
|
2988
|
+
function getThisFieldInitializer(context, node, fieldName) {
|
|
2989
|
+
const classBody = context.sourceCode.getAncestors(node).findLast((ancestor) => ancestor.type === "ClassBody");
|
|
2990
|
+
if (!classBody) return null;
|
|
2991
|
+
for (const member of classBody.body ?? []) if (getPropertyName(member.key) === fieldName) return member.value ?? null;
|
|
2992
|
+
return null;
|
|
2993
|
+
}
|
|
2994
|
+
function isQueryClientReference(context, node, sources) {
|
|
2995
|
+
const expression = unwrapExpression(node);
|
|
2996
|
+
if (!expression) return false;
|
|
2997
|
+
return isTanstackQueryImportedReference(context, expression, sources, QUERY_CLIENT_NAMES) || isImportedReference(context, expression, "@tanstack/query-core", QUERY_CLIENT_NAMES);
|
|
2998
|
+
}
|
|
2999
|
+
function isInjectQueryClientCall(context, node, sources) {
|
|
3000
|
+
const expression = unwrapExpression(node);
|
|
3001
|
+
return expression?.type === "CallExpression" && isImportedReference(context, expression.callee, ANGULAR_CORE_SOURCE, INJECT_NAMES) && isQueryClientReference(context, expression.arguments?.[0], sources);
|
|
3002
|
+
}
|
|
3003
|
+
function isQueryClientSource(context, node, sources) {
|
|
3004
|
+
const expression = unwrapExpression(node);
|
|
3005
|
+
if (!expression) return false;
|
|
3006
|
+
if (expression.type === "NewExpression") return isQueryClientReference(context, expression.callee, sources);
|
|
3007
|
+
return isInjectQueryClientCall(context, expression, sources);
|
|
3008
|
+
}
|
|
3009
|
+
function resolveQueryClientSource(context, node, sources) {
|
|
3010
|
+
let current = unwrapExpression(node);
|
|
3011
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3012
|
+
while (current && !visited.has(current)) {
|
|
3013
|
+
visited.add(current);
|
|
3014
|
+
if (isQueryClientSource(context, current, sources)) return current;
|
|
3015
|
+
if (current.type === "Identifier") {
|
|
3016
|
+
const initializer = getBindingInitializer(findNearestBindingIdentifier(context, current));
|
|
3017
|
+
if (!initializer) return current;
|
|
3018
|
+
current = unwrapExpression(initializer);
|
|
3019
|
+
continue;
|
|
3020
|
+
}
|
|
3021
|
+
if (current.type === "MemberExpression" && current.object?.type === "ThisExpression") {
|
|
3022
|
+
const initializer = getThisFieldInitializer(context, current, getPropertyName(current.property) ?? "");
|
|
3023
|
+
if (!initializer) return current;
|
|
3024
|
+
current = unwrapExpression(initializer);
|
|
3025
|
+
continue;
|
|
3026
|
+
}
|
|
3027
|
+
return current;
|
|
3028
|
+
}
|
|
3029
|
+
return current ?? null;
|
|
3030
|
+
}
|
|
3031
|
+
function isTanstackQueryClient(context, node, sources) {
|
|
3032
|
+
return isQueryClientSource(context, resolveQueryClientSource(context, node, sources), sources);
|
|
3033
|
+
}
|
|
3034
|
+
function reportInlineQueryOptions(context, node, sources) {
|
|
3035
|
+
const expression = unwrapExpression(node);
|
|
3036
|
+
if (!expression || expression.type !== "ObjectExpression") return;
|
|
3037
|
+
if (!hasInlineQueryOptions(context, expression, sources)) return;
|
|
3038
|
+
context.report({
|
|
3039
|
+
node: expression,
|
|
3040
|
+
messageId: "preferQueryOptions"
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
function reportInlineFilterQueryKey(context, node) {
|
|
3044
|
+
const expression = unwrapExpression(node);
|
|
3045
|
+
if (!expression || expression.type !== "ObjectExpression") return;
|
|
3046
|
+
if (!hasInlineFilterQueryKey(expression)) return;
|
|
3047
|
+
context.report({
|
|
3048
|
+
node: expression,
|
|
3049
|
+
messageId: "preferQueryOptionsQueryKey"
|
|
3050
|
+
});
|
|
3051
|
+
}
|
|
3052
|
+
const tanstackQueryPreferQueryOptions = defineRule({
|
|
3053
|
+
meta: {
|
|
3054
|
+
type: "problem",
|
|
3055
|
+
docs: {
|
|
3056
|
+
description: "Prefer queryOptions() to co-locate TanStack Query queryKey and queryFn.",
|
|
3057
|
+
recommended: true
|
|
3058
|
+
},
|
|
3059
|
+
schema: [{
|
|
3060
|
+
type: "object",
|
|
3061
|
+
additionalProperties: false,
|
|
3062
|
+
properties: { tanstackQuerySources: {
|
|
3063
|
+
type: "array",
|
|
3064
|
+
items: { type: "string" },
|
|
3065
|
+
default: DEFAULT_TANSTACK_QUERY_SOURCES
|
|
3066
|
+
} }
|
|
3067
|
+
}],
|
|
3068
|
+
messages: {
|
|
3069
|
+
preferQueryOptions: "Prefer using queryOptions() or infiniteQueryOptions() to co-locate queryKey and queryFn.",
|
|
3070
|
+
preferQueryOptionsQueryKey: "Prefer referencing a queryKey from a queryOptions() result instead of typing it manually."
|
|
3071
|
+
}
|
|
3072
|
+
},
|
|
3073
|
+
createOnce(context) {
|
|
3074
|
+
let tanstackQuerySources = new Set(DEFAULT_TANSTACK_QUERY_SOURCES);
|
|
3075
|
+
return {
|
|
3076
|
+
before() {
|
|
3077
|
+
tanstackQuerySources = getTanstackQuerySources(context.options?.[0] ?? {});
|
|
3078
|
+
},
|
|
3079
|
+
CallExpression(node) {
|
|
3080
|
+
const callNode = node;
|
|
3081
|
+
const importName = getTanstackQueryImportName(context, callNode.callee, tanstackQuerySources);
|
|
3082
|
+
if (importName && QUERY_OPTIONS_BUILDERS.has(importName)) return;
|
|
3083
|
+
if (importName && QUERY_INJECT_APIS.has(importName)) {
|
|
3084
|
+
for (const objectExpression of getReturnedObjectExpressions(callNode.arguments?.[0])) reportInlineQueryOptions(context, objectExpression, tanstackQuerySources);
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
if (importName && QUERIES_INJECT_APIS.has(importName)) {
|
|
3088
|
+
for (const objectExpression of getReturnedObjectExpressions(callNode.arguments?.[0])) {
|
|
3089
|
+
const queries = getProperty(objectExpression, "queries")?.value;
|
|
3090
|
+
for (const query of getQueryObjects(queries)) reportInlineQueryOptions(context, query, tanstackQuerySources);
|
|
3091
|
+
}
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
if (importName && FILTER_INJECT_APIS.has(importName)) {
|
|
3095
|
+
reportInlineFilterQueryKey(context, callNode.arguments?.[0]);
|
|
3096
|
+
return;
|
|
3097
|
+
}
|
|
3098
|
+
const callee = unwrapExpression(callNode.callee);
|
|
3099
|
+
if (callee?.type !== "MemberExpression" || !isTanstackQueryClient(context, callee.object, tanstackQuerySources)) return;
|
|
3100
|
+
const method = getPropertyName(callee.property);
|
|
3101
|
+
const options = callNode.arguments?.[0];
|
|
3102
|
+
if (QUERY_CLIENT_OPTION_METHODS.has(method ?? "")) {
|
|
3103
|
+
reportInlineQueryOptions(context, options, tanstackQuerySources);
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
if (QUERY_CLIENT_QUERY_KEY_METHODS.has(method ?? "") && isInlineArrayExpression(options)) {
|
|
3107
|
+
context.report({
|
|
3108
|
+
node: unwrapExpression(options) ?? options,
|
|
3109
|
+
messageId: "preferQueryOptionsQueryKey"
|
|
3110
|
+
});
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
3113
|
+
if (QUERY_CLIENT_FILTER_METHODS.has(method ?? "")) reportInlineFilterQueryKey(context, options);
|
|
3114
|
+
}
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
});
|
|
3118
|
+
//#endregion
|
|
3119
|
+
//#region src/rules/vitest-no-incompatible-angular-testing-apis/index.ts
|
|
3120
|
+
const ANGULAR_TESTING_SOURCE = "@angular/core/testing";
|
|
3121
|
+
const VITEST_INCOMPATIBLE_APIS = new Set([
|
|
3122
|
+
"discardPeriodicTasks",
|
|
3123
|
+
"fakeAsync",
|
|
3124
|
+
"flush",
|
|
3125
|
+
"flushMicrotasks",
|
|
3126
|
+
"resetFakeAsyncZone",
|
|
3127
|
+
"tick",
|
|
3128
|
+
"waitForAsync"
|
|
3129
|
+
]);
|
|
3130
|
+
function getIncompatibleImportName(specifier) {
|
|
3131
|
+
if (specifier.type !== "ImportSpecifier") return null;
|
|
3132
|
+
const importedName = getPropertyName(specifier.imported);
|
|
3133
|
+
return importedName && VITEST_INCOMPATIBLE_APIS.has(importedName) ? importedName : null;
|
|
3134
|
+
}
|
|
3135
|
+
function isIncompatibleNamespaceCall(context, callNode) {
|
|
3136
|
+
return isImportedNamespaceMember(context, callNode.callee, ANGULAR_TESTING_SOURCE, VITEST_INCOMPATIBLE_APIS);
|
|
3137
|
+
}
|
|
2332
3138
|
//#endregion
|
|
2333
3139
|
//#region src/index.ts
|
|
2334
3140
|
const plugin = eslintCompatPlugin({
|
|
@@ -2336,6 +3142,7 @@ const plugin = eslintCompatPlugin({
|
|
|
2336
3142
|
rules: {
|
|
2337
3143
|
"avoid-explicit-injection-context": avoidExplicitInjectionContext,
|
|
2338
3144
|
"avoid-explicit-subscription-management": avoidExplicitSubscriptionManagement,
|
|
3145
|
+
"avoid-inappropriate-intimacy": avoidInappropriateIntimacy,
|
|
2339
3146
|
"avoid-ng-modules": avoidNgModules,
|
|
2340
3147
|
"avoid-rxjs-state-in-component": avoidRxjsStateInComponent,
|
|
2341
3148
|
"avoid-writing-signals-in-reactive-context": avoidWritingSignalsInReactiveContext,
|
|
@@ -2343,105 +3150,51 @@ const plugin = eslintCompatPlugin({
|
|
|
2343
3150
|
"class-matches-filename": classMatchesFilename,
|
|
2344
3151
|
"component-resource-filenames": componentResourceFilenames,
|
|
2345
3152
|
"decorator-filename-suffix": decoratorFilenameSuffix,
|
|
3153
|
+
"no-manual-change-detection": noManualChangeDetection,
|
|
3154
|
+
"no-resource-api": noResourceApi,
|
|
3155
|
+
"no-route-resolvers": noRouteResolvers,
|
|
3156
|
+
"no-ui-inheritance": noUiInheritance,
|
|
2346
3157
|
"prefer-load-component-over-load-children": preferLoadComponentOverLoadChildren,
|
|
2347
3158
|
"prefer-private-elements": preferPrivateElements,
|
|
2348
3159
|
"prefer-style-url": preferStyleUrl,
|
|
2349
3160
|
"public-component-interface": publicComponentInterface,
|
|
2350
3161
|
"restrict-injectable-provided-in": restrictInjectableProvidedIn,
|
|
2351
|
-
"rules-of-inject":
|
|
3162
|
+
"rules-of-inject": rulesOfInject,
|
|
3163
|
+
"tanstack-query-injects-only-in-component-body": tanstackQueryInjectsOnlyInComponentBody,
|
|
3164
|
+
"tanstack-query-inlined-keys": tanstackQueryInlinedKeys,
|
|
3165
|
+
"tanstack-query-prefer-query-options": tanstackQueryPreferQueryOptions,
|
|
3166
|
+
"vitest-no-incompatible-angular-testing-apis": defineRule({
|
|
2352
3167
|
meta: {
|
|
2353
3168
|
type: "problem",
|
|
2354
3169
|
docs: {
|
|
2355
|
-
description: "
|
|
3170
|
+
description: "Disallow Angular testing APIs that depend on Zone.js and are incompatible with Vitest.",
|
|
2356
3171
|
recommended: true
|
|
2357
3172
|
},
|
|
2358
|
-
schema: [
|
|
2359
|
-
|
|
2360
|
-
additionalProperties: false,
|
|
2361
|
-
properties: {
|
|
2362
|
-
allowedFunctionNames: {
|
|
2363
|
-
type: "array",
|
|
2364
|
-
items: { type: "string" },
|
|
2365
|
-
default: DEFAULT_ALLOWED_FUNCTION_NAMES
|
|
2366
|
-
},
|
|
2367
|
-
checkUnimportedInject: {
|
|
2368
|
-
type: "boolean",
|
|
2369
|
-
default: false
|
|
2370
|
-
},
|
|
2371
|
-
injectFunctionPrefixes: {
|
|
2372
|
-
type: "array",
|
|
2373
|
-
items: { type: "string" },
|
|
2374
|
-
default: DEFAULT_INJECT_FUNCTION_PREFIXES
|
|
2375
|
-
},
|
|
2376
|
-
injectFunctionSuffixes: {
|
|
2377
|
-
type: "array",
|
|
2378
|
-
items: { type: "string" },
|
|
2379
|
-
default: DEFAULT_INJECT_FUNCTION_SUFFIXES
|
|
2380
|
-
},
|
|
2381
|
-
runsInInjectionContext: {
|
|
2382
|
-
type: "array",
|
|
2383
|
-
items: {
|
|
2384
|
-
type: "object",
|
|
2385
|
-
additionalProperties: false,
|
|
2386
|
-
required: ["from", "imports"],
|
|
2387
|
-
properties: {
|
|
2388
|
-
from: { type: "string" },
|
|
2389
|
-
imports: { anyOf: [{ const: "all" }, {
|
|
2390
|
-
type: "array",
|
|
2391
|
-
items: { type: "string" }
|
|
2392
|
-
}] }
|
|
2393
|
-
}
|
|
2394
|
-
},
|
|
2395
|
-
default: DEFAULT_RUNS_IN_INJECTION_CONTEXT
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
}],
|
|
2399
|
-
messages: { disallowedInject: "Angular APIs that depend on injection context must be called from an injection context: a class field initializer or constructor in an Angular-decorated class, provider factory, InjectionToken factory, runInInjectionContext/runInContext callback, Angular route callback property (for example loadComponent/canActivate), an inject* or *Guard function, or configured allowed function." }
|
|
3173
|
+
schema: [],
|
|
3174
|
+
messages: { vitestNoIncompatibleAngularTestingApi: "Avoid Angular testing API '{{name}}'. It depends on Zone.js and is not compatible with Angular tests running on Vitest." }
|
|
2400
3175
|
},
|
|
2401
3176
|
createOnce(context) {
|
|
2402
|
-
const injectionContextApiLocalNames = /* @__PURE__ */ new Set();
|
|
2403
|
-
const injectionContextApiNamespaceMembers = /* @__PURE__ */ new Map();
|
|
2404
|
-
const runsInInjectionContextFunctionNames = /* @__PURE__ */ new Set();
|
|
2405
|
-
let runsInInjectionContextRules = [];
|
|
2406
3177
|
return {
|
|
2407
|
-
before() {
|
|
2408
|
-
injectionContextApiLocalNames.clear();
|
|
2409
|
-
injectionContextApiNamespaceMembers.clear();
|
|
2410
|
-
runsInInjectionContextFunctionNames.clear();
|
|
2411
|
-
runsInInjectionContextRules = (context.options[0] ?? {}).runsInInjectionContext ?? DEFAULT_RUNS_IN_INJECTION_CONTEXT;
|
|
2412
|
-
},
|
|
2413
3178
|
ImportDeclaration(node) {
|
|
2414
|
-
const
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
if (
|
|
2419
|
-
}
|
|
2420
|
-
const knownApiImports = typeof source === "string" ? getKnownInjectionContextApiImports(source) : null;
|
|
2421
|
-
if (knownApiImports) for (const specifier of node.specifiers ?? []) {
|
|
2422
|
-
if (specifier.type === "ImportSpecifier" && knownApiImports.has(getPropertyName(specifier.imported) ?? "")) injectionContextApiLocalNames.add(specifier.local.name);
|
|
2423
|
-
if (specifier.type === "ImportNamespaceSpecifier") injectionContextApiNamespaceMembers.set(specifier.local.name, knownApiImports);
|
|
2424
|
-
}
|
|
2425
|
-
},
|
|
2426
|
-
CallExpression(node) {
|
|
2427
|
-
const options = context.options[0] ?? {};
|
|
2428
|
-
const allowedFunctionNames = new Set(options.allowedFunctionNames ?? DEFAULT_ALLOWED_FUNCTION_NAMES);
|
|
2429
|
-
const checkUnimportedInject = options.checkUnimportedInject ?? false;
|
|
2430
|
-
const injectFunctionPrefixes = options.injectFunctionPrefixes ?? DEFAULT_INJECT_FUNCTION_PREFIXES;
|
|
2431
|
-
const injectFunctionSuffixes = options.injectFunctionSuffixes ?? DEFAULT_INJECT_FUNCTION_SUFFIXES;
|
|
2432
|
-
const inAllowedContext = isAllowedInjectionContext(context, node, allowedFunctionNames, injectFunctionPrefixes, injectFunctionSuffixes);
|
|
2433
|
-
if (isInjectLikeHelperCall(context, node, injectionContextApiLocalNames, injectFunctionPrefixes, injectFunctionSuffixes, runsInInjectionContextFunctionNames) && !inAllowedContext) {
|
|
3179
|
+
const importNode = node;
|
|
3180
|
+
if (importNode.source?.value !== ANGULAR_TESTING_SOURCE) return;
|
|
3181
|
+
for (const specifier of importNode.specifiers ?? []) {
|
|
3182
|
+
const name = getIncompatibleImportName(specifier);
|
|
3183
|
+
if (!name) continue;
|
|
2434
3184
|
context.report({
|
|
2435
|
-
node:
|
|
2436
|
-
messageId: "
|
|
3185
|
+
node: specifier,
|
|
3186
|
+
messageId: "vitestNoIncompatibleAngularTestingApi",
|
|
3187
|
+
data: { name }
|
|
2437
3188
|
});
|
|
2438
|
-
return;
|
|
2439
3189
|
}
|
|
2440
|
-
|
|
2441
|
-
|
|
3190
|
+
},
|
|
3191
|
+
CallExpression(node) {
|
|
3192
|
+
const callNode = node;
|
|
3193
|
+
if (!isIncompatibleNamespaceCall(context, callNode)) return;
|
|
2442
3194
|
context.report({
|
|
2443
|
-
node:
|
|
2444
|
-
messageId: "
|
|
3195
|
+
node: callNode.callee,
|
|
3196
|
+
messageId: "vitestNoIncompatibleAngularTestingApi",
|
|
3197
|
+
data: { name: getPropertyName(callNode.callee?.property) ?? "this API" }
|
|
2445
3198
|
});
|
|
2446
3199
|
}
|
|
2447
3200
|
};
|