@benjavicente/lint-angular 0.0.3 → 0.0.4
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 +5 -0
- package/dist/index.mjs +253 -16
- package/package.json +12 -12
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Opinated Oxlint/ESLint-compatible plugin for Angular project rules.
|
|
|
10
10
|
| [`rules-of-inject`](./src/rules/rules-of-inject/) | ✅ | | Restrict `inject()` usage to valid Angular injection contexts. |
|
|
11
11
|
| [`avoid-explicit-injection-context`](./src/rules/avoid-explicit-injection-context/) | ✅ | | Avoid explicit injection-context APIs such as `inject(Injector)` and `runInInjectionContext`. |
|
|
12
12
|
| [`avoid-explicit-subscription-management`](./src/rules/avoid-explicit-subscription-management/) | ✅ | | Avoid storing and manually managing RxJS subscriptions in Angular classes. |
|
|
13
|
+
| [`avoid-inappropriate-intimacy`](./src/rules/avoid-inappropriate-intimacy/) | ✅ | | Avoid passing Angular component, directive, and service instances as function arguments. |
|
|
13
14
|
| [`avoid-ng-modules`](./src/rules/avoid-ng-modules/) | ✅ | | Avoid NgModules in favor of standalone Angular APIs. |
|
|
14
15
|
| [`avoid-rxjs-state-in-component`](./src/rules/avoid-rxjs-state-in-component/) | ✅ | | Avoid RxJS subjects for component and directive-local state. |
|
|
15
16
|
| [`avoid-writing-signals-in-reactive-context`](./src/rules/avoid-writing-signals-in-reactive-context/) | ✅ | | Avoid writing to signals from reactive Angular contexts. |
|
|
@@ -17,6 +18,10 @@ Opinated Oxlint/ESLint-compatible plugin for Angular project rules.
|
|
|
17
18
|
| [`class-matches-filename`](./src/rules/class-matches-filename/) | ✅ | | Require Angular class names to match component, directive, and service filenames. |
|
|
18
19
|
| [`component-resource-filenames`](./src/rules/component-resource-filenames/) | ✅ | | Require component resource filenames to match the component TypeScript filename. |
|
|
19
20
|
| [`decorator-filename-suffix`](./src/rules/decorator-filename-suffix/) | ✅ | | Require Angular decorators to be declared in files with matching filename suffixes. |
|
|
21
|
+
| [`injects-tanstack-query-only-in-component-body`](./src/rules/injects-tanstack-query-only-in-component-body/) | ✅ | | Require TanStack Query inject helpers to be direct component/directive class fields. |
|
|
22
|
+
| [`no-resource-api`](./src/rules/no-resource-api/) | ✅ | | Avoid Angular resource APIs for server state. |
|
|
23
|
+
| [`no-route-resolvers`](./src/rules/no-route-resolvers/) | ✅ | | Avoid Angular route resolvers for data loading. |
|
|
24
|
+
| [`no-ui-inheritance`](./src/rules/no-ui-inheritance/) | ✅ | | Avoid inheritance for Angular components and directives. |
|
|
20
25
|
| [`prefer-private-elements`](./src/rules/prefer-private-elements/) | ✅ | ✅ | Prefer ECMAScript private elements over TypeScript `private` members. |
|
|
21
26
|
| [`prefer-load-component-over-load-children`](./src/rules/prefer-load-component-over-load-children/) | ✅ | | Prefer `loadComponent` for lazy routes that load a standalone component. |
|
|
22
27
|
| [`prefer-style-url`](./src/rules/prefer-style-url/) | ✅ | ✅ | Prefer `styleUrl` when a component has exactly one stylesheet. |
|
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;
|
|
@@ -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;
|
|
@@ -485,7 +485,7 @@ 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"
|
|
@@ -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;
|
|
@@ -793,7 +793,7 @@ 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
799
|
walkNode$1(classBody, classNode, (current) => {
|
|
@@ -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,7 +1062,7 @@ 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",
|
|
@@ -1033,9 +1073,9 @@ const FIELD_NODE_TYPES$1 = new Set([
|
|
|
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");
|
|
@@ -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,198 @@ const decoratorFilenameSuffix = defineRule({
|
|
|
1682
1722
|
}
|
|
1683
1723
|
});
|
|
1684
1724
|
//#endregion
|
|
1725
|
+
//#region src/rules/injects-tanstack-query-only-in-component-body/index.ts
|
|
1726
|
+
const TARGET_DECORATORS$1 = new Set(["Component", "Directive"]);
|
|
1727
|
+
const TANSTACK_QUERY_APIS = new Set(["injectQuery", "injectMutation"]);
|
|
1728
|
+
const TANSTACK_QUERY_SOURCES = new Set(["@tanstack/angular-query", "@benjavicente/angular-query"]);
|
|
1729
|
+
const CLASS_FIELD_TYPES$1 = new Set([
|
|
1730
|
+
"AccessorProperty",
|
|
1731
|
+
"FieldDefinition",
|
|
1732
|
+
"PropertyDefinition"
|
|
1733
|
+
]);
|
|
1734
|
+
const FUNCTION_TYPES$1 = new Set([
|
|
1735
|
+
"ArrowFunctionExpression",
|
|
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));
|
|
1742
|
+
}
|
|
1743
|
+
function isTanstackQueryInjectCall(context, callNode) {
|
|
1744
|
+
return [...TANSTACK_QUERY_SOURCES].some((source) => isImportedReference(context, callNode.callee, source, TANSTACK_QUERY_APIS));
|
|
1745
|
+
}
|
|
1746
|
+
function isDirectComponentOrDirectiveFieldInitializer(context, callNode) {
|
|
1747
|
+
const ancestors = context.sourceCode.getAncestors(callNode);
|
|
1748
|
+
const classField = ancestors.findLast((ancestor) => CLASS_FIELD_TYPES$1.has(ancestor.type));
|
|
1749
|
+
if (!classField || unwrapExpression(classField.value) !== callNode) return false;
|
|
1750
|
+
const classBodyIndex = ancestors.findLastIndex((ancestor) => ancestor.type === "ClassBody");
|
|
1751
|
+
if (classBodyIndex === -1) return false;
|
|
1752
|
+
const classBody = ancestors[classBodyIndex];
|
|
1753
|
+
if (ancestors.slice(classBodyIndex + 1).some((ancestor) => FUNCTION_TYPES$1.has(ancestor.type))) return false;
|
|
1754
|
+
return hasTargetDecorator$1(context, classBody?.parent);
|
|
1755
|
+
}
|
|
1756
|
+
const injectsTanstackQueryOnlyInComponentBody = defineRule({
|
|
1757
|
+
meta: {
|
|
1758
|
+
type: "problem",
|
|
1759
|
+
docs: {
|
|
1760
|
+
description: "Require Angular TanStack Query injectQuery/injectMutation calls to be direct component/directive class fields.",
|
|
1761
|
+
recommended: true
|
|
1762
|
+
},
|
|
1763
|
+
schema: [],
|
|
1764
|
+
messages: { onlyInComponentBody: "Call {{name}} only as a direct class field initializer in an Angular component or directive." }
|
|
1765
|
+
},
|
|
1766
|
+
createOnce(context) {
|
|
1767
|
+
return { CallExpression(node) {
|
|
1768
|
+
const callNode = node;
|
|
1769
|
+
if (!isTanstackQueryInjectCall(context, callNode)) return;
|
|
1770
|
+
if (isDirectComponentOrDirectiveFieldInitializer(context, callNode)) return;
|
|
1771
|
+
context.report({
|
|
1772
|
+
node: callNode.callee ?? callNode,
|
|
1773
|
+
messageId: "onlyInComponentBody",
|
|
1774
|
+
data: { name: callNode.callee?.name ?? "this TanStack Query inject API" }
|
|
1775
|
+
});
|
|
1776
|
+
} };
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
//#endregion
|
|
1780
|
+
//#region src/rules/no-resource-api/index.ts
|
|
1781
|
+
const RESOURCE_APIS = new Set(["resource"]);
|
|
1782
|
+
const RXJS_RESOURCE_APIS = new Set(["rxResource"]);
|
|
1783
|
+
const HTTP_RESOURCE_APIS = new Set(["httpResource"]);
|
|
1784
|
+
function isResourceApiCall(context, callNode) {
|
|
1785
|
+
const callee = callNode.callee;
|
|
1786
|
+
if (isImportedReference(context, callee, "@angular/core", RESOURCE_APIS)) return true;
|
|
1787
|
+
if (isImportedNamespaceMember(context, callee, "@angular/core", RESOURCE_APIS)) return true;
|
|
1788
|
+
if (isImportedReference(context, callee, "@angular/core/rxjs-interop", RXJS_RESOURCE_APIS)) return true;
|
|
1789
|
+
if (isImportedNamespaceMember(context, callee, "@angular/core/rxjs-interop", RXJS_RESOURCE_APIS)) return true;
|
|
1790
|
+
if (isImportedReference(context, callee, "@angular/common/http", HTTP_RESOURCE_APIS)) return true;
|
|
1791
|
+
if (isImportedNamespaceMember(context, callee, "@angular/common/http", HTTP_RESOURCE_APIS)) return true;
|
|
1792
|
+
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;
|
|
1793
|
+
return callee?.type === "MemberExpression" && isImportedReference(context, callee.object, "@angular/common/http", HTTP_RESOURCE_APIS);
|
|
1794
|
+
}
|
|
1795
|
+
const noResourceApi = defineRule({
|
|
1796
|
+
meta: {
|
|
1797
|
+
type: "suggestion",
|
|
1798
|
+
docs: {
|
|
1799
|
+
description: "Disallow Angular resource APIs in favor of a dedicated server-state library.",
|
|
1800
|
+
recommended: true
|
|
1801
|
+
},
|
|
1802
|
+
schema: [],
|
|
1803
|
+
messages: { noResourceApi: "Avoid Angular resource APIs for server state. Prefer a dedicated server-state helper such as TanStack Query." }
|
|
1804
|
+
},
|
|
1805
|
+
createOnce(context) {
|
|
1806
|
+
return { CallExpression(node) {
|
|
1807
|
+
const callNode = node;
|
|
1808
|
+
if (!isResourceApiCall(context, callNode)) return;
|
|
1809
|
+
context.report({
|
|
1810
|
+
node: callNode.callee ?? callNode,
|
|
1811
|
+
messageId: "noResourceApi"
|
|
1812
|
+
});
|
|
1813
|
+
} };
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
//#endregion
|
|
1817
|
+
//#region src/rules/no-route-resolvers/index.ts
|
|
1818
|
+
const ROUTE_TYPE_NAMES$1 = new Set(["Route"]);
|
|
1819
|
+
const ROUTES_TYPE_NAMES$1 = new Set(["Routes"]);
|
|
1820
|
+
function isImportedTypeName$1(context, typeNode, importedNames) {
|
|
1821
|
+
if (typeNode?.type === "Identifier") {
|
|
1822
|
+
const importedName = getImportedName(context, typeNode, "@angular/router");
|
|
1823
|
+
return !!importedName && importedNames.has(importedName);
|
|
1824
|
+
}
|
|
1825
|
+
return typeNode?.type === "TSQualifiedName" && typeNode.left?.type === "Identifier" && isNamespaceImport(context, typeNode.left, "@angular/router") && importedNames.has(getPropertyName(typeNode.right) ?? "");
|
|
1826
|
+
}
|
|
1827
|
+
function getTypeParameterNodes$1(typeNode) {
|
|
1828
|
+
return typeNode.typeParameters?.params ?? typeNode.typeArguments?.params ?? [];
|
|
1829
|
+
}
|
|
1830
|
+
function isRouteType$1(context, typeNode) {
|
|
1831
|
+
return typeNode?.type === "TSTypeReference" && isImportedTypeName$1(context, typeNode.typeName, ROUTE_TYPE_NAMES$1);
|
|
1832
|
+
}
|
|
1833
|
+
function isRouteArrayType$1(context, typeNode) {
|
|
1834
|
+
if (!typeNode) return false;
|
|
1835
|
+
if (typeNode.type === "TSTypeReference" && isImportedTypeName$1(context, typeNode.typeName, ROUTES_TYPE_NAMES$1)) return true;
|
|
1836
|
+
if (typeNode.type === "TSArrayType") return isRouteType$1(context, typeNode.elementType);
|
|
1837
|
+
if (typeNode.type !== "TSTypeReference") return false;
|
|
1838
|
+
if (getPropertyName(typeNode.typeName) !== "Array" && getPropertyName(typeNode.typeName) !== "ReadonlyArray") return false;
|
|
1839
|
+
const [elementType] = getTypeParameterNodes$1(typeNode);
|
|
1840
|
+
return isRouteType$1(context, elementType);
|
|
1841
|
+
}
|
|
1842
|
+
function getTypeAnnotation(node) {
|
|
1843
|
+
const typeAnnotation = node?.typeAnnotation;
|
|
1844
|
+
return typeAnnotation?.type === "TSTypeAnnotation" ? typeAnnotation.typeAnnotation : null;
|
|
1845
|
+
}
|
|
1846
|
+
const noRouteResolvers = defineRule({
|
|
1847
|
+
meta: {
|
|
1848
|
+
type: "suggestion",
|
|
1849
|
+
docs: {
|
|
1850
|
+
description: "Disallow Angular route resolvers in typed Route/Routes declarations.",
|
|
1851
|
+
recommended: true
|
|
1852
|
+
},
|
|
1853
|
+
schema: [],
|
|
1854
|
+
messages: { noRouteResolvers: "Avoid Angular route resolvers for data loading. Prefer component-level loading with signals or server-state helpers." }
|
|
1855
|
+
},
|
|
1856
|
+
createOnce(context) {
|
|
1857
|
+
function reportResolveInRouteObject(routeObject) {
|
|
1858
|
+
for (const property of routeObject.properties ?? []) {
|
|
1859
|
+
if (property.type !== "Property" || property.computed) continue;
|
|
1860
|
+
const propertyName = getPropertyName(property.key);
|
|
1861
|
+
if (propertyName === "resolve") {
|
|
1862
|
+
context.report({
|
|
1863
|
+
node: property.key ?? property,
|
|
1864
|
+
messageId: "noRouteResolvers"
|
|
1865
|
+
});
|
|
1866
|
+
continue;
|
|
1867
|
+
}
|
|
1868
|
+
if (propertyName === "children" && property.value?.type === "ArrayExpression") reportResolveInRouteArray(property.value);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
function reportResolveInRouteArray(routeArray) {
|
|
1872
|
+
for (const element of routeArray.elements ?? []) if (element?.type === "ObjectExpression") reportResolveInRouteObject(element);
|
|
1873
|
+
}
|
|
1874
|
+
return { VariableDeclarator(node) {
|
|
1875
|
+
const declarator = node;
|
|
1876
|
+
const typeNode = getTypeAnnotation(declarator.id);
|
|
1877
|
+
if (isRouteArrayType$1(context, typeNode) && declarator.init?.type === "ArrayExpression") {
|
|
1878
|
+
reportResolveInRouteArray(declarator.init);
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
if (isRouteType$1(context, typeNode) && declarator.init?.type === "ObjectExpression") reportResolveInRouteObject(declarator.init);
|
|
1882
|
+
} };
|
|
1883
|
+
}
|
|
1884
|
+
});
|
|
1885
|
+
//#endregion
|
|
1886
|
+
//#region src/rules/no-ui-inheritance/index.ts
|
|
1887
|
+
const UI_DECORATORS = new Set(["Component", "Directive"]);
|
|
1888
|
+
function hasUiDecorator(context, classNode) {
|
|
1889
|
+
if (!Array.isArray(classNode.decorators)) return false;
|
|
1890
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, UI_DECORATORS));
|
|
1891
|
+
}
|
|
1892
|
+
const noUiInheritance = defineRule({
|
|
1893
|
+
meta: {
|
|
1894
|
+
type: "suggestion",
|
|
1895
|
+
docs: {
|
|
1896
|
+
description: "Disallow inheritance for Angular components and directives.",
|
|
1897
|
+
recommended: true
|
|
1898
|
+
},
|
|
1899
|
+
schema: [],
|
|
1900
|
+
messages: { noUiInheritance: "Avoid inheritance for Angular {{kind}} classes. Prefer composition with services or inject* helpers." }
|
|
1901
|
+
},
|
|
1902
|
+
createOnce(context) {
|
|
1903
|
+
return { "ClassDeclaration, ClassExpression"(node) {
|
|
1904
|
+
const classNode = node;
|
|
1905
|
+
if (!classNode.superClass) return;
|
|
1906
|
+
if (!hasUiDecorator(context, classNode)) return;
|
|
1907
|
+
const kind = classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, new Set(["Component"]))) ? "component" : "directive";
|
|
1908
|
+
context.report({
|
|
1909
|
+
node: classNode.superClass,
|
|
1910
|
+
messageId: "noUiInheritance",
|
|
1911
|
+
data: { kind }
|
|
1912
|
+
});
|
|
1913
|
+
} };
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
//#endregion
|
|
1685
1917
|
//#region src/rules/prefer-load-component-over-load-children/index.ts
|
|
1686
1918
|
const ROUTE_TYPE_NAMES = new Set(["Route"]);
|
|
1687
1919
|
const ROUTES_TYPE_NAMES = new Set(["Routes"]);
|
|
@@ -2119,7 +2351,7 @@ const restrictInjectableProvidedIn = defineRule({
|
|
|
2119
2351
|
//#endregion
|
|
2120
2352
|
//#region src/rules/rules-of-inject/index.ts
|
|
2121
2353
|
const DEFAULT_ALLOWED_FUNCTION_NAMES = [];
|
|
2122
|
-
const DEFAULT_INJECT_FUNCTION_PREFIXES = ["
|
|
2354
|
+
const DEFAULT_INJECT_FUNCTION_PREFIXES = ["inject"];
|
|
2123
2355
|
const DEFAULT_INJECT_FUNCTION_SUFFIXES = ["Guard"];
|
|
2124
2356
|
const DEFAULT_RUNS_IN_INJECTION_CONTEXT = [];
|
|
2125
2357
|
const ROUTER_CONTEXT_PROPERTY_NAMES = new Set([
|
|
@@ -2336,6 +2568,7 @@ const plugin = eslintCompatPlugin({
|
|
|
2336
2568
|
rules: {
|
|
2337
2569
|
"avoid-explicit-injection-context": avoidExplicitInjectionContext,
|
|
2338
2570
|
"avoid-explicit-subscription-management": avoidExplicitSubscriptionManagement,
|
|
2571
|
+
"avoid-inappropriate-intimacy": avoidInappropriateIntimacy,
|
|
2339
2572
|
"avoid-ng-modules": avoidNgModules,
|
|
2340
2573
|
"avoid-rxjs-state-in-component": avoidRxjsStateInComponent,
|
|
2341
2574
|
"avoid-writing-signals-in-reactive-context": avoidWritingSignalsInReactiveContext,
|
|
@@ -2343,6 +2576,10 @@ const plugin = eslintCompatPlugin({
|
|
|
2343
2576
|
"class-matches-filename": classMatchesFilename,
|
|
2344
2577
|
"component-resource-filenames": componentResourceFilenames,
|
|
2345
2578
|
"decorator-filename-suffix": decoratorFilenameSuffix,
|
|
2579
|
+
"injects-tanstack-query-only-in-component-body": injectsTanstackQueryOnlyInComponentBody,
|
|
2580
|
+
"no-resource-api": noResourceApi,
|
|
2581
|
+
"no-route-resolvers": noRouteResolvers,
|
|
2582
|
+
"no-ui-inheritance": noUiInheritance,
|
|
2346
2583
|
"prefer-load-component-over-load-children": preferLoadComponentOverLoadChildren,
|
|
2347
2584
|
"prefer-private-elements": preferPrivateElements,
|
|
2348
2585
|
"prefer-style-url": preferStyleUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@benjavicente/lint-angular",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Oxlint/ESLint-compatible rules for Angular.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"angular",
|
|
@@ -24,20 +24,20 @@
|
|
|
24
24
|
"publishConfig": {
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
|
-
"scripts": {
|
|
28
|
-
"build": "vp pack",
|
|
29
|
-
"fmt": "vp fmt src",
|
|
30
|
-
"lint": "vp lint src",
|
|
31
|
-
"test": "vp test run"
|
|
32
|
-
},
|
|
33
27
|
"dependencies": {
|
|
34
28
|
"@oxlint/plugins": "^1.63.0"
|
|
35
29
|
},
|
|
36
30
|
"devDependencies": {
|
|
37
31
|
"@types/estree": "^1.0.9",
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"vitest": "
|
|
32
|
+
"typescript": "^6.0.3",
|
|
33
|
+
"vite-plus": "^0.1.20",
|
|
34
|
+
"vitest": "npm:@voidzero-dev/vite-plus-test@^0.1.20",
|
|
35
|
+
"oxlint-vitest-rule-tester": "0.0.1"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "vp pack",
|
|
39
|
+
"fmt": "vp fmt src",
|
|
40
|
+
"lint": "vp lint src",
|
|
41
|
+
"test": "vp test run"
|
|
42
42
|
}
|
|
43
|
-
}
|
|
43
|
+
}
|