@benjavicente/lint-angular 0.0.4 → 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 -24
- package/dist/index.mjs +668 -152
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,30 +4,32 @@ Opinated Oxlint/ESLint-compatible plugin for Angular project rules.
|
|
|
4
4
|
|
|
5
5
|
## Rules
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
|
9
|
-
|
|
|
10
|
-
| [`
|
|
11
|
-
| [`avoid-explicit-
|
|
12
|
-
| [`avoid-
|
|
13
|
-
| [`avoid-
|
|
14
|
-
| [`avoid-
|
|
15
|
-
| [`avoid-
|
|
16
|
-
| [`
|
|
17
|
-
| [`class-
|
|
18
|
-
| [`
|
|
19
|
-
| [`
|
|
20
|
-
| [`
|
|
21
|
-
| [`
|
|
22
|
-
| [`no-
|
|
23
|
-
| [`no-
|
|
24
|
-
| [`
|
|
25
|
-
| [`prefer-
|
|
26
|
-
| [`prefer-
|
|
27
|
-
| [`
|
|
28
|
-
| [`
|
|
29
|
-
| [`
|
|
30
|
-
|
|
7
|
+
| Rule | Default | Fixable | Description |
|
|
8
|
+
| ------------------------------------------------------------------------------------------------------------- | ------- | ------- | --------------------------------------------------------------------------------------------- |
|
|
9
|
+
| [`rules-of-inject`](./src/rules/rules-of-inject/) | ✅ | | Restrict `inject()` usage to valid Angular injection contexts. |
|
|
10
|
+
| [`avoid-explicit-injection-context`](./src/rules/avoid-explicit-injection-context/) | ✅ | | Avoid explicit injection-context APIs such as `inject(Injector)` and `runInInjectionContext`. |
|
|
11
|
+
| [`avoid-explicit-subscription-management`](./src/rules/avoid-explicit-subscription-management/) | ✅ | | Avoid storing and manually managing RxJS subscriptions in Angular classes. |
|
|
12
|
+
| [`avoid-inappropriate-intimacy`](./src/rules/avoid-inappropriate-intimacy/) | ✅ | | Avoid passing Angular component, directive, and service instances as function arguments. |
|
|
13
|
+
| [`avoid-ng-modules`](./src/rules/avoid-ng-modules/) | ✅ | | Avoid NgModules in favor of standalone Angular APIs. |
|
|
14
|
+
| [`avoid-rxjs-state-in-component`](./src/rules/avoid-rxjs-state-in-component/) | ✅ | | Avoid RxJS subjects for component and directive-local state. |
|
|
15
|
+
| [`avoid-writing-signals-in-reactive-context`](./src/rules/avoid-writing-signals-in-reactive-context/) | ✅ | | Avoid writing to signals from reactive Angular contexts. |
|
|
16
|
+
| [`class-member-order`](./src/rules/class-member-order/) | ✅ | | Enforce a consistent member order for Angular classes. |
|
|
17
|
+
| [`class-matches-filename`](./src/rules/class-matches-filename/) | ✅ | | Require Angular class names to match component, directive, and service filenames. |
|
|
18
|
+
| [`component-resource-filenames`](./src/rules/component-resource-filenames/) | ✅ | | Require component resource filenames to match the component TypeScript filename. |
|
|
19
|
+
| [`decorator-filename-suffix`](./src/rules/decorator-filename-suffix/) | ✅ | | Require Angular decorators to be declared in files with matching filename suffixes. |
|
|
20
|
+
| [`no-manual-change-detection`](./src/rules/no-manual-change-detection/) | ✅ | | Avoid manual Angular change detection through `ChangeDetectorRef`. |
|
|
21
|
+
| [`no-resource-api`](./src/rules/no-resource-api/) | ✅ | | Avoid Angular resource APIs for server state. |
|
|
22
|
+
| [`no-route-resolvers`](./src/rules/no-route-resolvers/) | ✅ | | Avoid Angular route resolvers for data loading. |
|
|
23
|
+
| [`no-ui-inheritance`](./src/rules/no-ui-inheritance/) | ✅ | | Avoid inheritance for Angular components and directives. |
|
|
24
|
+
| [`prefer-private-elements`](./src/rules/prefer-private-elements/) | ✅ | ✅ | Prefer ECMAScript private elements over TypeScript `private` members. |
|
|
25
|
+
| [`prefer-load-component-over-load-children`](./src/rules/prefer-load-component-over-load-children/) | ✅ | | Prefer `loadComponent` for lazy routes that load a standalone component. |
|
|
26
|
+
| [`prefer-style-url`](./src/rules/prefer-style-url/) | ✅ | ✅ | Prefer `styleUrl` when a component has exactly one stylesheet. |
|
|
27
|
+
| [`public-component-interface`](./src/rules/public-component-interface/) | ✅ | ✅ | Keep component and directive signal interfaces public while hiding injected dependencies. |
|
|
28
|
+
| [`restrict-injectable-provided-in`](./src/rules/restrict-injectable-provided-in/) | ✅ | | Restrict `@Injectable` `providedIn` values to `root` or `platform`. |
|
|
29
|
+
| [`tanstack-query-injects-only-in-component-body`](./src/rules/tanstack-query-injects-only-in-component-body/) | ✅ | | Require TanStack Query inject helpers to be direct component/directive class fields. |
|
|
30
|
+
| [`tanstack-query-inlined-keys`](./src/rules/tanstack-query-inlined-keys/) | ✅ | | Require TanStack Query query option keys to be inline arrays. |
|
|
31
|
+
| [`tanstack-query-prefer-query-options`](./src/rules/tanstack-query-prefer-query-options/) | ✅ | | Prefer `queryOptions()` to co-locate TanStack Query query keys and functions. |
|
|
32
|
+
| [`vitest-no-incompatible-angular-testing-apis`](./src/rules/vitest-no-incompatible-angular-testing-apis/) | ✅ | | Disallow Angular testing APIs that are incompatible with Vitest. |
|
|
31
33
|
|
|
32
34
|
## Oxlint setup
|
|
33
35
|
|
package/dist/index.mjs
CHANGED
|
@@ -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;
|
|
@@ -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) {
|
|
@@ -490,7 +490,7 @@ const TARGET_DECORATORS$4 = new Set([
|
|
|
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"
|
|
@@ -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);
|
|
@@ -796,8 +796,8 @@ const avoidExplicitSubscriptionManagement = defineRule({
|
|
|
796
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"
|
|
@@ -1068,7 +1068,7 @@ const SUBJECT_NAMES = new Set([
|
|
|
1068
1068
|
"ReplaySubject",
|
|
1069
1069
|
"Subject"
|
|
1070
1070
|
]);
|
|
1071
|
-
const FIELD_NODE_TYPES$
|
|
1071
|
+
const FIELD_NODE_TYPES$2 = new Set([
|
|
1072
1072
|
"AccessorProperty",
|
|
1073
1073
|
"FieldDefinition",
|
|
1074
1074
|
"PropertyDefinition"
|
|
@@ -1101,7 +1101,7 @@ function getSubjectKindFromConstructor(context, node) {
|
|
|
1101
1101
|
const memberName = getPropertyName(callee.property);
|
|
1102
1102
|
return SUBJECT_NAMES.has(memberName) ? memberName : null;
|
|
1103
1103
|
}
|
|
1104
|
-
function getMemberName(node) {
|
|
1104
|
+
function getMemberName$1(node) {
|
|
1105
1105
|
const expression = unwrapExpression(node);
|
|
1106
1106
|
if (!expression) return null;
|
|
1107
1107
|
if (expression.type === "Identifier") return expression.name;
|
|
@@ -1131,17 +1131,17 @@ function isCallOnThisField(node, fields) {
|
|
|
1131
1131
|
function isNgOnDestroyMethod(member) {
|
|
1132
1132
|
return member.type === "MethodDefinition" && getPropertyName(member.key) === "ngOnDestroy" && !!member.value;
|
|
1133
1133
|
}
|
|
1134
|
-
function walkNode(node, visitor) {
|
|
1134
|
+
function walkNode$1(node, visitor) {
|
|
1135
1135
|
if (!node) return;
|
|
1136
1136
|
if (Array.isArray(node)) {
|
|
1137
|
-
for (const child of node) walkNode(child, visitor);
|
|
1137
|
+
for (const child of node) walkNode$1(child, visitor);
|
|
1138
1138
|
return;
|
|
1139
1139
|
}
|
|
1140
1140
|
visitor(node);
|
|
1141
1141
|
for (const [key, value] of Object.entries(node)) {
|
|
1142
1142
|
if (key === "parent") continue;
|
|
1143
1143
|
if (!value || typeof value !== "object") continue;
|
|
1144
|
-
walkNode(value, visitor);
|
|
1144
|
+
walkNode$1(value, visitor);
|
|
1145
1145
|
}
|
|
1146
1146
|
}
|
|
1147
1147
|
function getUsage(usages, fieldName) {
|
|
@@ -1159,8 +1159,8 @@ function getUsage(usages, fieldName) {
|
|
|
1159
1159
|
function collectSubjectFields(context, classBody) {
|
|
1160
1160
|
const fields = /* @__PURE__ */ new Map();
|
|
1161
1161
|
for (const member of classBody.body ?? []) {
|
|
1162
|
-
if (!FIELD_NODE_TYPES$
|
|
1163
|
-
const fieldName = getMemberName(member.key);
|
|
1162
|
+
if (!FIELD_NODE_TYPES$2.has(member.type)) continue;
|
|
1163
|
+
const fieldName = getMemberName$1(member.key);
|
|
1164
1164
|
if (!fieldName) continue;
|
|
1165
1165
|
const typeNode = member.typeAnnotation?.typeAnnotation;
|
|
1166
1166
|
const kind = getSubjectKindFromConstructor(context, member.value) ?? getSubjectKindFromType(context, typeNode);
|
|
@@ -1176,7 +1176,7 @@ function collectFieldUsages(classBody, fields) {
|
|
|
1176
1176
|
const usages = /* @__PURE__ */ new Map();
|
|
1177
1177
|
const ngOnDestroyMethods = /* @__PURE__ */ new Set();
|
|
1178
1178
|
for (const member of classBody.body ?? []) if (isNgOnDestroyMethod(member)) ngOnDestroyMethods.add(member.value);
|
|
1179
|
-
walkNode(classBody, (node) => {
|
|
1179
|
+
walkNode$1(classBody, (node) => {
|
|
1180
1180
|
const call = isCallOnThisField(node, fields);
|
|
1181
1181
|
if (!call) return;
|
|
1182
1182
|
const usage = getUsage(usages, call.fieldName);
|
|
@@ -1722,56 +1722,142 @@ const decoratorFilenameSuffix = defineRule({
|
|
|
1722
1722
|
}
|
|
1723
1723
|
});
|
|
1724
1724
|
//#endregion
|
|
1725
|
-
//#region src/rules/
|
|
1726
|
-
const
|
|
1727
|
-
const
|
|
1728
|
-
const
|
|
1729
|
-
const
|
|
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([
|
|
1730
1737
|
"AccessorProperty",
|
|
1731
1738
|
"FieldDefinition",
|
|
1732
1739
|
"PropertyDefinition"
|
|
1733
1740
|
]);
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
"FunctionDeclaration",
|
|
1737
|
-
"FunctionExpression"
|
|
1738
|
-
]);
|
|
1739
|
-
function hasTargetDecorator$1(context, classNode) {
|
|
1740
|
-
if (!classNode || !Array.isArray(classNode.decorators)) return false;
|
|
1741
|
-
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$1));
|
|
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);
|
|
1742
1743
|
}
|
|
1743
|
-
function
|
|
1744
|
-
|
|
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]);
|
|
1745
1748
|
}
|
|
1746
|
-
function
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
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;
|
|
1755
1824
|
}
|
|
1756
|
-
|
|
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({
|
|
1757
1839
|
meta: {
|
|
1758
|
-
type: "
|
|
1840
|
+
type: "suggestion",
|
|
1759
1841
|
docs: {
|
|
1760
|
-
description: "
|
|
1842
|
+
description: "Disallow manual Angular change detection through ChangeDetectorRef APIs.",
|
|
1761
1843
|
recommended: true
|
|
1762
1844
|
},
|
|
1763
1845
|
schema: [],
|
|
1764
|
-
messages: {
|
|
1846
|
+
messages: { noManualChangeDetection: "Avoid manual change detection with ChangeDetectorRef. Prefer Angular's normal change detection triggers, signals, async bindings, or input updates." }
|
|
1765
1847
|
},
|
|
1766
1848
|
createOnce(context) {
|
|
1767
|
-
return {
|
|
1768
|
-
const
|
|
1769
|
-
|
|
1770
|
-
if (
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
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
|
+
});
|
|
1775
1861
|
});
|
|
1776
1862
|
} };
|
|
1777
1863
|
}
|
|
@@ -2198,7 +2284,7 @@ const preferStyleUrl = defineRule({
|
|
|
2198
2284
|
});
|
|
2199
2285
|
//#endregion
|
|
2200
2286
|
//#region src/rules/public-component-interface/index.ts
|
|
2201
|
-
const TARGET_DECORATORS = new Set(["Component", "Directive"]);
|
|
2287
|
+
const TARGET_DECORATORS$1 = new Set(["Component", "Directive"]);
|
|
2202
2288
|
const INPUT_MODEL_APIS = new Set(["input", "model"]);
|
|
2203
2289
|
const OUTPUT_APIS = new Set(["output", "outputFromObservable"]);
|
|
2204
2290
|
const INJECT_APIS = new Set(["inject"]);
|
|
@@ -2207,9 +2293,9 @@ const FIELD_NODE_TYPES = new Set([
|
|
|
2207
2293
|
"FieldDefinition",
|
|
2208
2294
|
"PropertyDefinition"
|
|
2209
2295
|
]);
|
|
2210
|
-
function hasTargetDecorator(context, classNode) {
|
|
2296
|
+
function hasTargetDecorator$1(context, classNode) {
|
|
2211
2297
|
if (!classNode || !Array.isArray(classNode.decorators)) return false;
|
|
2212
|
-
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS));
|
|
2298
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$1));
|
|
2213
2299
|
}
|
|
2214
2300
|
function isApiCallFromAngularCore(context, node, apiNames) {
|
|
2215
2301
|
if (!node || node.type !== "CallExpression") return false;
|
|
@@ -2268,7 +2354,7 @@ const publicComponentInterface = defineRule({
|
|
|
2268
2354
|
createOnce(context) {
|
|
2269
2355
|
return { ClassBody(node) {
|
|
2270
2356
|
const classNode = node.parent;
|
|
2271
|
-
if (!hasTargetDecorator(context, classNode)) return;
|
|
2357
|
+
if (!hasTargetDecorator$1(context, classNode)) return;
|
|
2272
2358
|
for (const member of node.body ?? []) {
|
|
2273
2359
|
if (!FIELD_NODE_TYPES.has(member.type)) continue;
|
|
2274
2360
|
const isInputModelMember = isApiCallFromAngularCore(context, member.value, INPUT_MODEL_APIS);
|
|
@@ -2416,18 +2502,18 @@ const INJECTION_CONTEXT_FUNCTION_TYPE_NAMES = new Set([
|
|
|
2416
2502
|
"RedirectFunction",
|
|
2417
2503
|
"ResolveFn"
|
|
2418
2504
|
]);
|
|
2419
|
-
const FUNCTION_TYPES = new Set([
|
|
2505
|
+
const FUNCTION_TYPES$1 = new Set([
|
|
2420
2506
|
"ArrowFunctionExpression",
|
|
2421
2507
|
"FunctionDeclaration",
|
|
2422
2508
|
"FunctionExpression"
|
|
2423
2509
|
]);
|
|
2424
|
-
const CLASS_FIELD_TYPES = new Set([
|
|
2510
|
+
const CLASS_FIELD_TYPES$1 = new Set([
|
|
2425
2511
|
"AccessorProperty",
|
|
2426
2512
|
"FieldDefinition",
|
|
2427
2513
|
"PropertyDefinition"
|
|
2428
2514
|
]);
|
|
2429
2515
|
function isFunction(node) {
|
|
2430
|
-
return !!node && FUNCTION_TYPES.has(node.type);
|
|
2516
|
+
return !!node && FUNCTION_TYPES$1.has(node.type);
|
|
2431
2517
|
}
|
|
2432
2518
|
function getAncestors(context, node) {
|
|
2433
2519
|
return context.sourceCode.getAncestors(node);
|
|
@@ -2483,7 +2569,7 @@ function getFunctionContextTypeName(functionNode) {
|
|
|
2483
2569
|
const parent = skipTransparentExpressionParents(functionNode.parent);
|
|
2484
2570
|
if (parent?.type === "VariableDeclarator" && parent.id?.type === "Identifier") return getTypeName(parent.id.typeAnnotation?.typeAnnotation);
|
|
2485
2571
|
if (parent?.type === "Property" && parent.value === functionNode) return getTypeName(parent.typeAnnotation?.typeAnnotation);
|
|
2486
|
-
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);
|
|
2487
2573
|
return null;
|
|
2488
2574
|
}
|
|
2489
2575
|
function isTypedInjectionContextFunction(functionNode) {
|
|
@@ -2517,7 +2603,7 @@ function isConstructorFunction(functionNode) {
|
|
|
2517
2603
|
return functionNode.parent?.type === "MethodDefinition" && functionNode.parent.kind === "constructor" && functionNode.parent.value === functionNode;
|
|
2518
2604
|
}
|
|
2519
2605
|
function isDirectClassFieldInitializer(ancestors, nearestFunction) {
|
|
2520
|
-
return !!ancestors.findLast((ancestor) => CLASS_FIELD_TYPES.has(ancestor.type)) && !nearestFunction;
|
|
2606
|
+
return !!ancestors.findLast((ancestor) => CLASS_FIELD_TYPES$1.has(ancestor.type)) && !nearestFunction;
|
|
2521
2607
|
}
|
|
2522
2608
|
function hasSupportedAngularClassDecorator(classNode) {
|
|
2523
2609
|
if (!classNode) return false;
|
|
@@ -2561,6 +2647,494 @@ function isInjectLikeHelperCall(context, node, injectionContextApiLocalNames, in
|
|
|
2561
2647
|
if (runsInInjectionContextFunctionNames.has(callee.name)) return true;
|
|
2562
2648
|
return !injectionContextApiLocalNames.has(callee.name) && callee.name !== "inject" && (injectFunctionPrefixes.some((prefix) => callee.name.startsWith(prefix)) || injectFunctionSuffixes.some((suffix) => callee.name.endsWith(suffix)));
|
|
2563
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
|
+
}
|
|
2564
3138
|
//#endregion
|
|
2565
3139
|
//#region src/index.ts
|
|
2566
3140
|
const plugin = eslintCompatPlugin({
|
|
@@ -2576,7 +3150,7 @@ const plugin = eslintCompatPlugin({
|
|
|
2576
3150
|
"class-matches-filename": classMatchesFilename,
|
|
2577
3151
|
"component-resource-filenames": componentResourceFilenames,
|
|
2578
3152
|
"decorator-filename-suffix": decoratorFilenameSuffix,
|
|
2579
|
-
"
|
|
3153
|
+
"no-manual-change-detection": noManualChangeDetection,
|
|
2580
3154
|
"no-resource-api": noResourceApi,
|
|
2581
3155
|
"no-route-resolvers": noRouteResolvers,
|
|
2582
3156
|
"no-ui-inheritance": noUiInheritance,
|
|
@@ -2585,100 +3159,42 @@ const plugin = eslintCompatPlugin({
|
|
|
2585
3159
|
"prefer-style-url": preferStyleUrl,
|
|
2586
3160
|
"public-component-interface": publicComponentInterface,
|
|
2587
3161
|
"restrict-injectable-provided-in": restrictInjectableProvidedIn,
|
|
2588
|
-
"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({
|
|
2589
3167
|
meta: {
|
|
2590
3168
|
type: "problem",
|
|
2591
3169
|
docs: {
|
|
2592
|
-
description: "
|
|
3170
|
+
description: "Disallow Angular testing APIs that depend on Zone.js and are incompatible with Vitest.",
|
|
2593
3171
|
recommended: true
|
|
2594
3172
|
},
|
|
2595
|
-
schema: [
|
|
2596
|
-
|
|
2597
|
-
additionalProperties: false,
|
|
2598
|
-
properties: {
|
|
2599
|
-
allowedFunctionNames: {
|
|
2600
|
-
type: "array",
|
|
2601
|
-
items: { type: "string" },
|
|
2602
|
-
default: DEFAULT_ALLOWED_FUNCTION_NAMES
|
|
2603
|
-
},
|
|
2604
|
-
checkUnimportedInject: {
|
|
2605
|
-
type: "boolean",
|
|
2606
|
-
default: false
|
|
2607
|
-
},
|
|
2608
|
-
injectFunctionPrefixes: {
|
|
2609
|
-
type: "array",
|
|
2610
|
-
items: { type: "string" },
|
|
2611
|
-
default: DEFAULT_INJECT_FUNCTION_PREFIXES
|
|
2612
|
-
},
|
|
2613
|
-
injectFunctionSuffixes: {
|
|
2614
|
-
type: "array",
|
|
2615
|
-
items: { type: "string" },
|
|
2616
|
-
default: DEFAULT_INJECT_FUNCTION_SUFFIXES
|
|
2617
|
-
},
|
|
2618
|
-
runsInInjectionContext: {
|
|
2619
|
-
type: "array",
|
|
2620
|
-
items: {
|
|
2621
|
-
type: "object",
|
|
2622
|
-
additionalProperties: false,
|
|
2623
|
-
required: ["from", "imports"],
|
|
2624
|
-
properties: {
|
|
2625
|
-
from: { type: "string" },
|
|
2626
|
-
imports: { anyOf: [{ const: "all" }, {
|
|
2627
|
-
type: "array",
|
|
2628
|
-
items: { type: "string" }
|
|
2629
|
-
}] }
|
|
2630
|
-
}
|
|
2631
|
-
},
|
|
2632
|
-
default: DEFAULT_RUNS_IN_INJECTION_CONTEXT
|
|
2633
|
-
}
|
|
2634
|
-
}
|
|
2635
|
-
}],
|
|
2636
|
-
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." }
|
|
2637
3175
|
},
|
|
2638
3176
|
createOnce(context) {
|
|
2639
|
-
const injectionContextApiLocalNames = /* @__PURE__ */ new Set();
|
|
2640
|
-
const injectionContextApiNamespaceMembers = /* @__PURE__ */ new Map();
|
|
2641
|
-
const runsInInjectionContextFunctionNames = /* @__PURE__ */ new Set();
|
|
2642
|
-
let runsInInjectionContextRules = [];
|
|
2643
3177
|
return {
|
|
2644
|
-
before() {
|
|
2645
|
-
injectionContextApiLocalNames.clear();
|
|
2646
|
-
injectionContextApiNamespaceMembers.clear();
|
|
2647
|
-
runsInInjectionContextFunctionNames.clear();
|
|
2648
|
-
runsInInjectionContextRules = (context.options[0] ?? {}).runsInInjectionContext ?? DEFAULT_RUNS_IN_INJECTION_CONTEXT;
|
|
2649
|
-
},
|
|
2650
3178
|
ImportDeclaration(node) {
|
|
2651
|
-
const
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
if (
|
|
2656
|
-
}
|
|
2657
|
-
const knownApiImports = typeof source === "string" ? getKnownInjectionContextApiImports(source) : null;
|
|
2658
|
-
if (knownApiImports) for (const specifier of node.specifiers ?? []) {
|
|
2659
|
-
if (specifier.type === "ImportSpecifier" && knownApiImports.has(getPropertyName(specifier.imported) ?? "")) injectionContextApiLocalNames.add(specifier.local.name);
|
|
2660
|
-
if (specifier.type === "ImportNamespaceSpecifier") injectionContextApiNamespaceMembers.set(specifier.local.name, knownApiImports);
|
|
2661
|
-
}
|
|
2662
|
-
},
|
|
2663
|
-
CallExpression(node) {
|
|
2664
|
-
const options = context.options[0] ?? {};
|
|
2665
|
-
const allowedFunctionNames = new Set(options.allowedFunctionNames ?? DEFAULT_ALLOWED_FUNCTION_NAMES);
|
|
2666
|
-
const checkUnimportedInject = options.checkUnimportedInject ?? false;
|
|
2667
|
-
const injectFunctionPrefixes = options.injectFunctionPrefixes ?? DEFAULT_INJECT_FUNCTION_PREFIXES;
|
|
2668
|
-
const injectFunctionSuffixes = options.injectFunctionSuffixes ?? DEFAULT_INJECT_FUNCTION_SUFFIXES;
|
|
2669
|
-
const inAllowedContext = isAllowedInjectionContext(context, node, allowedFunctionNames, injectFunctionPrefixes, injectFunctionSuffixes);
|
|
2670
|
-
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;
|
|
2671
3184
|
context.report({
|
|
2672
|
-
node:
|
|
2673
|
-
messageId: "
|
|
3185
|
+
node: specifier,
|
|
3186
|
+
messageId: "vitestNoIncompatibleAngularTestingApi",
|
|
3187
|
+
data: { name }
|
|
2674
3188
|
});
|
|
2675
|
-
return;
|
|
2676
3189
|
}
|
|
2677
|
-
|
|
2678
|
-
|
|
3190
|
+
},
|
|
3191
|
+
CallExpression(node) {
|
|
3192
|
+
const callNode = node;
|
|
3193
|
+
if (!isIncompatibleNamespaceCall(context, callNode)) return;
|
|
2679
3194
|
context.report({
|
|
2680
|
-
node:
|
|
2681
|
-
messageId: "
|
|
3195
|
+
node: callNode.callee,
|
|
3196
|
+
messageId: "vitestNoIncompatibleAngularTestingApi",
|
|
3197
|
+
data: { name: getPropertyName(callNode.callee?.property) ?? "this API" }
|
|
2682
3198
|
});
|
|
2683
3199
|
}
|
|
2684
3200
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@benjavicente/lint-angular",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Oxlint/ESLint-compatible rules for Angular.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"angular",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/estree": "^1.0.9",
|
|
32
32
|
"typescript": "^6.0.3",
|
|
33
|
-
"vite-plus": "^0.1.
|
|
33
|
+
"vite-plus": "^0.1.22",
|
|
34
34
|
"vitest": "npm:@voidzero-dev/vite-plus-test@^0.1.20",
|
|
35
35
|
"oxlint-vitest-rule-tester": "0.0.1"
|
|
36
36
|
},
|