oxlint-plugin-arch 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +47 -1
- package/dist/index.js +122 -122
- package/dist/presets/index.js +28 -9
- package/dist/presets/tanstack-start-react-modules-preset.js +28 -9
- package/dist/rules/dillon-anti-slop/index.d.ts +2 -0
- package/dist/rules/dillon-anti-slop/index.js +2 -2
- package/dist/rules/dillon-anti-slop/rules/no-chained-type-assertions.d.ts +1 -0
- package/dist/rules/dillon-anti-slop/rules/no-conditional-empty-object-spread.d.ts +1 -0
- package/dist/rules/dillon-anti-slop/rules/no-known-value-widening.d.ts +1 -0
- package/dist/rules/dillon-anti-slop/rules/no-module-mocking.d.ts +1 -0
- package/dist/rules/dillon-anti-slop/rules/no-object-parameters.d.ts +1 -0
- package/dist/rules/dillon-anti-slop/rules/no-reflect-apply.d.ts +1 -0
- package/dist/rules/dillon-anti-slop/rules/no-reflect-get.d.ts +1 -0
- package/dist/rules/dillon-anti-slop/rules/no-runtime-typeof.d.ts +1 -0
- package/dist/rules/dillon-anti-slop/rules/no-shape-in-symbol-names.d.ts +1 -0
- package/dist/rules/dillon-anti-slop/rules/no-unknown-parameters.d.ts +1 -0
- package/dist/rules/dillon-anti-slop/rules/no-unknown-returns.d.ts +1 -0
- package/dist/rules/dillon-anti-slop/rules/no-unknown-type-aliases.d.ts +1 -0
- package/dist/rules/dillon-anti-slop/rules/no-unsafe-dictionary-type.d.ts +1 -0
- package/dist/rules/dillon-anti-slop/rules/no-widen-then-assert.d.ts +1 -0
- package/dist/rules/dillon-anti-slop/rules/require-safety-comment-for-type-assertion.d.ts +1 -0
- package/dist/rules/dillon-anti-slop/shared/dictionary-types.d.ts +19 -0
- package/dist/rules/dillon-anti-slop/shared/lexical-type-parameters.d.ts +4 -0
- package/dist/rules/dillon-anti-slop/shared/reflect-method.d.ts +2 -0
- package/dist/rules/module-policy/domain-constants.d.ts +2 -0
- package/dist/rules/module-policy/import-boundaries.d.ts +2 -0
- package/dist/rules/module-policy/index.d.ts +2 -0
- package/dist/rules/module-policy/index.js +49 -49
- package/dist/rules/module-policy/memoization.d.ts +2 -0
- package/dist/rules/module-policy/reasoned-directives.d.ts +2 -0
- package/dist/rules/module-policy/service-functions.d.ts +2 -0
- package/dist/rules/module-policy/service-types.d.ts +2 -0
- package/dist/rules/module-policy/tanstack-runtime.d.ts +2 -0
- package/dist/rules/module-policy/test-assertions.d.ts +2 -0
- package/dist/rules/module-policy/test-modifiers.d.ts +2 -0
- package/dist/types/module-policy.types.d.ts +34 -0
- package/dist/types/test-analysis.types.d.ts +11 -0
- package/dist/utils/helpers/ast.d.ts +12 -0
- package/dist/utils/helpers/boundaries.d.ts +3 -0
- package/dist/utils/helpers/comments.d.ts +6 -0
- package/dist/utils/helpers/declarations.d.ts +35 -0
- package/dist/utils/helpers/exports.d.ts +18 -0
- package/dist/utils/helpers/factories.d.ts +9 -0
- package/dist/utils/helpers/guards.d.ts +11 -0
- package/dist/utils/helpers/index.d.ts +16 -0
- package/dist/utils/helpers/layout.d.ts +15 -0
- package/dist/utils/helpers/naming.d.ts +13 -0
- package/dist/utils/helpers/options.d.ts +22 -0
- package/dist/utils/helpers/orpc.d.ts +3 -0
- package/dist/utils/helpers/react-components.d.ts +13 -0
- package/dist/utils/helpers/routes.d.ts +5 -0
- package/dist/utils/helpers/sanitizers.d.ts +3 -0
- package/dist/utils/helpers/schemas.d.ts +10 -0
- package/dist/utils/helpers/syntax.d.ts +1 -0
- package/dist/utils/helpers/test-tables.d.ts +8 -0
- package/dist/utils/helpers/trivial.d.ts +6 -0
- package/dist/utils/index.d.ts +1 -0
- package/package.json +9 -2
package/dist/presets/index.js
CHANGED
|
@@ -1042,15 +1042,35 @@ function presetReactConfig(context) {
|
|
|
1042
1042
|
]))));
|
|
1043
1043
|
}
|
|
1044
1044
|
if (Object.keys(css).length && options.shadcn !== false) {
|
|
1045
|
+
const allow = ["layout", "opacity", "truncate"];
|
|
1046
|
+
const padding = ["p", "px", "py", "ps", "pe", "pt", "pr", "pb", "pl"];
|
|
1047
|
+
const gap = ["gap", "gap-x", "gap-y"];
|
|
1048
|
+
const safeArea = padding.flatMap((group) => ["top", "right", "bottom", "left"].flatMap((side) => [
|
|
1049
|
+
`${group}-[env(safe-area-inset-${side})]`,
|
|
1050
|
+
...group === "p" ? [] : [`${group}-[max(--spacing(*),env(safe-area-inset-${side}))]`]
|
|
1051
|
+
]));
|
|
1045
1052
|
policies.shadcn = [
|
|
1046
1053
|
presetOverride(webFiles, {
|
|
1047
|
-
"shadcn/no-restyle": [
|
|
1048
|
-
|
|
1054
|
+
"shadcn/no-restyle": [
|
|
1055
|
+
"error",
|
|
1056
|
+
{
|
|
1057
|
+
allow,
|
|
1058
|
+
contracts: [
|
|
1059
|
+
{ pattern: "^(CardContent|PopoverContent)$", allow: [...allow, ...padding] },
|
|
1060
|
+
{ pattern: "^TabsContent$", allow: [...allow, ...gap, ...padding] },
|
|
1061
|
+
{ pattern: "^(Tabs|HoverCardContent|SidebarHeader|BreadcrumbList)$", allow: [...allow, ...gap] }
|
|
1062
|
+
]
|
|
1063
|
+
}
|
|
1064
|
+
],
|
|
1065
|
+
"shadcn/no-arbitrary-values": ["error", { allow: ["layout", ...safeArea] }],
|
|
1049
1066
|
"shadcn/require-static-classes": "error",
|
|
1050
1067
|
"modules/dynamic-classes": "error"
|
|
1051
1068
|
}, ui),
|
|
1052
1069
|
presetOverride(webFiles, {
|
|
1053
1070
|
"shadcn/no-unknown-classes": ["error", { allow: [] }]
|
|
1071
|
+
}),
|
|
1072
|
+
presetOverride(presetScopes(web, "components/ui/sonner.tsx"), {
|
|
1073
|
+
"shadcn/no-unknown-classes": ["error", { allow: ["toaster", "toast"] }]
|
|
1054
1074
|
})
|
|
1055
1075
|
];
|
|
1056
1076
|
}
|
|
@@ -1119,7 +1139,6 @@ function presetBoundariesConfig(context) {
|
|
|
1119
1139
|
|
|
1120
1140
|
// src/configs/plugins.ts
|
|
1121
1141
|
import { createRequire } from "node:module";
|
|
1122
|
-
import { fileURLToPath } from "node:url";
|
|
1123
1142
|
var require2 = createRequire(import.meta.url);
|
|
1124
1143
|
function presetPluginsConfig(context) {
|
|
1125
1144
|
const { options, web, css } = context;
|
|
@@ -1136,15 +1155,15 @@ function presetPluginsConfig(context) {
|
|
|
1136
1155
|
require2.resolve("@eslint-community/eslint-plugin-eslint-comments"),
|
|
1137
1156
|
{
|
|
1138
1157
|
name: "arch",
|
|
1139
|
-
specifier:
|
|
1158
|
+
specifier: require2.resolve("oxlint-plugin-arch")
|
|
1140
1159
|
},
|
|
1141
1160
|
{
|
|
1142
1161
|
name: "dillon-anti-slop",
|
|
1143
|
-
specifier:
|
|
1162
|
+
specifier: require2.resolve("oxlint-plugin-arch/rules/dillon-anti-slop")
|
|
1144
1163
|
},
|
|
1145
1164
|
{
|
|
1146
1165
|
name: "modules",
|
|
1147
|
-
specifier:
|
|
1166
|
+
specifier: require2.resolve("oxlint-plugin-arch/rules/module-policy")
|
|
1148
1167
|
}
|
|
1149
1168
|
];
|
|
1150
1169
|
const settings = {
|
|
@@ -1213,9 +1232,9 @@ function presetResolveContext(options) {
|
|
|
1213
1232
|
throw new Error("Folder layout mode must be flat or domain.");
|
|
1214
1233
|
layout[folder] = mode;
|
|
1215
1234
|
}
|
|
1216
|
-
const
|
|
1217
|
-
for (const [index, folder] of
|
|
1218
|
-
const overlaps =
|
|
1235
|
+
const folders2 = Object.keys(layout);
|
|
1236
|
+
for (const [index, folder] of folders2.entries()) {
|
|
1237
|
+
const overlaps = folders2.slice(0, index).some((other) => folder.startsWith(other + "/") || other.startsWith(folder + "/"));
|
|
1219
1238
|
if (overlaps) {
|
|
1220
1239
|
throw new Error("Layout folders must not overlap.");
|
|
1221
1240
|
}
|
|
@@ -1042,15 +1042,35 @@ function presetReactConfig(context) {
|
|
|
1042
1042
|
]))));
|
|
1043
1043
|
}
|
|
1044
1044
|
if (Object.keys(css).length && options.shadcn !== false) {
|
|
1045
|
+
const allow = ["layout", "opacity", "truncate"];
|
|
1046
|
+
const padding = ["p", "px", "py", "ps", "pe", "pt", "pr", "pb", "pl"];
|
|
1047
|
+
const gap = ["gap", "gap-x", "gap-y"];
|
|
1048
|
+
const safeArea = padding.flatMap((group) => ["top", "right", "bottom", "left"].flatMap((side) => [
|
|
1049
|
+
`${group}-[env(safe-area-inset-${side})]`,
|
|
1050
|
+
...group === "p" ? [] : [`${group}-[max(--spacing(*),env(safe-area-inset-${side}))]`]
|
|
1051
|
+
]));
|
|
1045
1052
|
policies.shadcn = [
|
|
1046
1053
|
presetOverride(webFiles, {
|
|
1047
|
-
"shadcn/no-restyle": [
|
|
1048
|
-
|
|
1054
|
+
"shadcn/no-restyle": [
|
|
1055
|
+
"error",
|
|
1056
|
+
{
|
|
1057
|
+
allow,
|
|
1058
|
+
contracts: [
|
|
1059
|
+
{ pattern: "^(CardContent|PopoverContent)$", allow: [...allow, ...padding] },
|
|
1060
|
+
{ pattern: "^TabsContent$", allow: [...allow, ...gap, ...padding] },
|
|
1061
|
+
{ pattern: "^(Tabs|HoverCardContent|SidebarHeader|BreadcrumbList)$", allow: [...allow, ...gap] }
|
|
1062
|
+
]
|
|
1063
|
+
}
|
|
1064
|
+
],
|
|
1065
|
+
"shadcn/no-arbitrary-values": ["error", { allow: ["layout", ...safeArea] }],
|
|
1049
1066
|
"shadcn/require-static-classes": "error",
|
|
1050
1067
|
"modules/dynamic-classes": "error"
|
|
1051
1068
|
}, ui),
|
|
1052
1069
|
presetOverride(webFiles, {
|
|
1053
1070
|
"shadcn/no-unknown-classes": ["error", { allow: [] }]
|
|
1071
|
+
}),
|
|
1072
|
+
presetOverride(presetScopes(web, "components/ui/sonner.tsx"), {
|
|
1073
|
+
"shadcn/no-unknown-classes": ["error", { allow: ["toaster", "toast"] }]
|
|
1054
1074
|
})
|
|
1055
1075
|
];
|
|
1056
1076
|
}
|
|
@@ -1119,7 +1139,6 @@ function presetBoundariesConfig(context) {
|
|
|
1119
1139
|
|
|
1120
1140
|
// src/configs/plugins.ts
|
|
1121
1141
|
import { createRequire } from "node:module";
|
|
1122
|
-
import { fileURLToPath } from "node:url";
|
|
1123
1142
|
var require2 = createRequire(import.meta.url);
|
|
1124
1143
|
function presetPluginsConfig(context) {
|
|
1125
1144
|
const { options, web, css } = context;
|
|
@@ -1136,15 +1155,15 @@ function presetPluginsConfig(context) {
|
|
|
1136
1155
|
require2.resolve("@eslint-community/eslint-plugin-eslint-comments"),
|
|
1137
1156
|
{
|
|
1138
1157
|
name: "arch",
|
|
1139
|
-
specifier:
|
|
1158
|
+
specifier: require2.resolve("oxlint-plugin-arch")
|
|
1140
1159
|
},
|
|
1141
1160
|
{
|
|
1142
1161
|
name: "dillon-anti-slop",
|
|
1143
|
-
specifier:
|
|
1162
|
+
specifier: require2.resolve("oxlint-plugin-arch/rules/dillon-anti-slop")
|
|
1144
1163
|
},
|
|
1145
1164
|
{
|
|
1146
1165
|
name: "modules",
|
|
1147
|
-
specifier:
|
|
1166
|
+
specifier: require2.resolve("oxlint-plugin-arch/rules/module-policy")
|
|
1148
1167
|
}
|
|
1149
1168
|
];
|
|
1150
1169
|
const settings = {
|
|
@@ -1213,9 +1232,9 @@ function presetResolveContext(options) {
|
|
|
1213
1232
|
throw new Error("Folder layout mode must be flat or domain.");
|
|
1214
1233
|
layout[folder] = mode;
|
|
1215
1234
|
}
|
|
1216
|
-
const
|
|
1217
|
-
for (const [index, folder] of
|
|
1218
|
-
const overlaps =
|
|
1235
|
+
const folders2 = Object.keys(layout);
|
|
1236
|
+
for (const [index, folder] of folders2.entries()) {
|
|
1237
|
+
const overlaps = folders2.slice(0, index).some((other) => folder.startsWith(other + "/") || other.startsWith(folder + "/"));
|
|
1219
1238
|
if (overlaps) {
|
|
1220
1239
|
throw new Error("Layout folders must not overlap.");
|
|
1221
1240
|
}
|
|
@@ -349,8 +349,8 @@ function classifyWideningTarget(type, environment) {
|
|
|
349
349
|
if (alias === undefined)
|
|
350
350
|
return null;
|
|
351
351
|
if ((alias.typeParameters?.params.length ?? 0) > 0) {
|
|
352
|
-
const
|
|
353
|
-
return
|
|
352
|
+
const substitutions2 = aliasSubstitution(alias, unwrapped, new Map);
|
|
353
|
+
return substitutions2 !== null && resolvesToDictionary(alias.typeAnnotation, environment, substitutions2, new Set([name])) ? { kind: "generic container" } : null;
|
|
354
354
|
}
|
|
355
355
|
const substitutions = aliasSubstitution(alias, unwrapped, new Map);
|
|
356
356
|
if (substitutions === null)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noChainedTypeAssertionsRule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noConditionalEmptyObjectSpreadRule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noKnownValueWideningRule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noModuleMockingRule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noObjectParametersRule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noReflectApplyRule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noReflectGetRule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noRuntimeTypeofRule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noForbiddenTermInSymbolNamesRule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noUnknownParametersRule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noUnknownReturnsRule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noUnknownTypeAliasesRule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noUnsafeDictionaryTypeRule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noWidenThenAssertRule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const requireSafetyCommentForTypeAssertionRule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
2
|
+
export type UnsafeDictionary = {
|
|
3
|
+
readonly kind: "unsafe-dictionary";
|
|
4
|
+
readonly unsafeValue: "any" | "empty-object" | "object" | "union" | "unknown";
|
|
5
|
+
};
|
|
6
|
+
export type WideningTargetKind = "anonymous object" | "generic container" | "object" | "open dictionary" | "unknown";
|
|
7
|
+
export type WideningTarget = {
|
|
8
|
+
readonly kind: WideningTargetKind;
|
|
9
|
+
};
|
|
10
|
+
export type TypeEnvironment = {
|
|
11
|
+
readonly aliases: ReadonlyMap<string, ESTree.TSTypeAliasDeclaration>;
|
|
12
|
+
readonly interfaces: ReadonlyMap<string, readonly ESTree.TSInterfaceDeclaration[]>;
|
|
13
|
+
readonly shadowedBuiltIns: ReadonlySet<string>;
|
|
14
|
+
};
|
|
15
|
+
export declare function createTypeEnvironment(program: ESTree.Program): TypeEnvironment;
|
|
16
|
+
export declare function classifyUnsafeDictionaryValue(valueType: ESTree.TSType, environment: TypeEnvironment): UnsafeDictionary | null;
|
|
17
|
+
export declare function classifyUnsafeDictionary(type: ESTree.TSType, environment: TypeEnvironment): UnsafeDictionary | null;
|
|
18
|
+
export declare function classifyWideningTarget(type: ESTree.TSType, environment: TypeEnvironment): WideningTarget | null;
|
|
19
|
+
export declare function isKnownEvidenceExpression(expression: ESTree.Expression): boolean;
|
|
@@ -162,7 +162,7 @@ function trivialExpression(node) {
|
|
|
162
162
|
}
|
|
163
163
|
if (!node.body || node.body.type !== "BlockStatement")
|
|
164
164
|
return null;
|
|
165
|
-
const statements = node.body.body.filter((
|
|
165
|
+
const statements = node.body.body.filter((statement2) => statement2.type !== "EmptyStatement" && !(statement2.type === "ExpressionStatement" && statement2.directive));
|
|
166
166
|
if (statements.length !== 1)
|
|
167
167
|
return null;
|
|
168
168
|
const statement = statements[0];
|
|
@@ -305,7 +305,7 @@ function declarationsIsTrivialFunction(node) {
|
|
|
305
305
|
if (!node.body || node.body.type !== "BlockStatement") {
|
|
306
306
|
return false;
|
|
307
307
|
}
|
|
308
|
-
const statements = node.body.body.filter((
|
|
308
|
+
const statements = node.body.body.filter((statement2) => statement2.type !== "EmptyStatement" && !(statement2.type === "ExpressionStatement" && statement2.directive));
|
|
309
309
|
if (statements.length === 0) {
|
|
310
310
|
return true;
|
|
311
311
|
}
|
|
@@ -367,8 +367,8 @@ function declarationsInsideForLoop(node) {
|
|
|
367
367
|
}
|
|
368
368
|
function declarationsNamedFromNode(node) {
|
|
369
369
|
if (node.type === "VariableDeclarator") {
|
|
370
|
-
const
|
|
371
|
-
return
|
|
370
|
+
const kind2 = declarationsNamedKind(node, [node.parent]);
|
|
371
|
+
return kind2 && node.id.type === "Identifier" && !declarationsInsideForLoop(node) ? { name: node.id.name, node, kind: kind2 } : undefined;
|
|
372
372
|
}
|
|
373
373
|
const kind = declarationsNamedKind(node, []);
|
|
374
374
|
const name = "id" in node && node.id && "name" in node.id ? node.id.name : undefined;
|
|
@@ -1081,32 +1081,32 @@ var serviceFunctions = defineRule3({
|
|
|
1081
1081
|
}
|
|
1082
1082
|
return {
|
|
1083
1083
|
Program(program) {
|
|
1084
|
-
const
|
|
1084
|
+
const options2 = optionsFirst(context, {});
|
|
1085
1085
|
const candidates = declarationsFileLevelFunctionCandidates(program);
|
|
1086
1086
|
const exported = exportsCollectFunctions(program);
|
|
1087
1087
|
operation = exported.length === 1 ? exported[0].node : undefined;
|
|
1088
|
-
operations = new Set(
|
|
1089
|
-
if (
|
|
1088
|
+
operations = new Set(options2.frontend && !options2.singleExport ? exported.map((entry) => entry.node) : operation ? [operation] : []);
|
|
1089
|
+
if (options2.frontend && !options2.singleExport) {
|
|
1090
1090
|
for (const method of declarationsPublicServiceMethods(program))
|
|
1091
1091
|
operations.add(method.node);
|
|
1092
1092
|
}
|
|
1093
|
-
allowLocalHelpers =
|
|
1094
|
-
allowReturnedMethods =
|
|
1095
|
-
message =
|
|
1096
|
-
if (
|
|
1093
|
+
allowLocalHelpers = options2.frontend || (options2.allowLocalHelpers ?? false);
|
|
1094
|
+
allowReturnedMethods = options2.allowReturnedMethods ?? false;
|
|
1095
|
+
message = options2.message ?? (options2.frontend ? "Frontend service modules expose public domain operations. Keep operation-only helpers inside their public operation and genuinely shared helpers in domain .utils.ts; file-level private functions are forbidden." : allowLocalHelpers ? "Keep this helper inside the one exported operation; domain .utils.ts is only for genuinely shared logic." : "This service contract does not allow private helpers. Keep its existing factory/operation contract; domain .utils.ts is only for genuinely shared logic. Do not add a fake factory or public helper export.");
|
|
1096
|
+
if (options2.singleExport) {
|
|
1097
1097
|
const bindings = exportsCollect(program);
|
|
1098
1098
|
if (bindings.length !== 1 || !operation || bindings[0].default || bindings[0].reExport || bindings[0].typeOnly) {
|
|
1099
1099
|
context.report({
|
|
1100
1100
|
node: program,
|
|
1101
1101
|
messageId: "exports",
|
|
1102
1102
|
data: {
|
|
1103
|
-
message:
|
|
1103
|
+
message: options2.message ?? "Export exactly one named operation. Do not export helpers, types, constants or re-exports."
|
|
1104
1104
|
}
|
|
1105
1105
|
});
|
|
1106
1106
|
}
|
|
1107
1107
|
}
|
|
1108
1108
|
for (const candidate of candidates) {
|
|
1109
|
-
if ((
|
|
1109
|
+
if ((options2.frontend && !options2.singleExport || candidates.length === 1) && exported.some((entry) => entry.name === candidate.name)) {
|
|
1110
1110
|
continue;
|
|
1111
1111
|
}
|
|
1112
1112
|
context.report({ node: candidate.node, messageId: "helper", data: { message, name: candidate.name } });
|
|
@@ -1188,8 +1188,8 @@ var domainConstants = defineRule4({
|
|
|
1188
1188
|
const declaration = node.parent;
|
|
1189
1189
|
if (declaration.type !== "VariableDeclaration" || declaration.kind !== "const" || declaration.parent.type !== "Program" && declaration.parent.type !== "ExportNamedDeclaration")
|
|
1190
1190
|
return;
|
|
1191
|
-
const
|
|
1192
|
-
if (node.id.type === "Identifier" && /^[A-Z][A-Z0-9_]+$/.test(node.id.name) ||
|
|
1191
|
+
const options2 = optionsFirst(context, {});
|
|
1192
|
+
if (node.id.type === "Identifier" && /^[A-Z][A-Z0-9_]+$/.test(node.id.name) || options2.includeData && node.init && data(node.init)) {
|
|
1193
1193
|
const file = context.filename.replaceAll("\\", "/");
|
|
1194
1194
|
const owner = file.includes("/services/") && dirname2(file).split("/").at(-1) !== "services" ? basename(dirname2(file)) : basename(file).split(".")[0].replace(/-(?:action|query)$/, "");
|
|
1195
1195
|
context.report({
|
|
@@ -1285,9 +1285,9 @@ function testTablesRegistration(call, names, functions) {
|
|
|
1285
1285
|
if (callee.type === "CallExpression") {
|
|
1286
1286
|
const builder = astDottedName(callee.callee);
|
|
1287
1287
|
if (builder?.endsWith(".for") || builder?.endsWith(".each")) {
|
|
1288
|
-
const
|
|
1289
|
-
if (
|
|
1290
|
-
table =
|
|
1288
|
+
const argument2 = callee.arguments[0];
|
|
1289
|
+
if (argument2 && argument2.type !== "SpreadElement")
|
|
1290
|
+
table = argument2;
|
|
1291
1291
|
}
|
|
1292
1292
|
callee = callee.callee;
|
|
1293
1293
|
}
|
|
@@ -1408,20 +1408,20 @@ var testAssertions = defineRule6({
|
|
|
1408
1408
|
createOnce(context) {
|
|
1409
1409
|
return {
|
|
1410
1410
|
"Program:exit"(program) {
|
|
1411
|
-
const
|
|
1412
|
-
const tests = ["test", "it", ...
|
|
1411
|
+
const options2 = optionsFirst(context, {});
|
|
1412
|
+
const tests = ["test", "it", ...options2.additionalTestFunctions ?? []];
|
|
1413
1413
|
const functions = new Map;
|
|
1414
1414
|
const values = new Map;
|
|
1415
1415
|
const patterns = [
|
|
1416
1416
|
...testAssertionFunctions,
|
|
1417
1417
|
"assert.*",
|
|
1418
|
-
...
|
|
1418
|
+
...options2.additionalAssertionFunctions ?? []
|
|
1419
1419
|
].map(testTablesAssertionPattern);
|
|
1420
1420
|
const aliases = new Map;
|
|
1421
1421
|
const shadowed = new Set;
|
|
1422
1422
|
const calls = [];
|
|
1423
1423
|
const allowed = new Set;
|
|
1424
|
-
const assertionRoots = new Set([...testAssertionFunctions, ...
|
|
1424
|
+
const assertionRoots = new Set([...testAssertionFunctions, ...options2.additionalAssertionFunctions ?? []].map((name) => name.split(".")[0]));
|
|
1425
1425
|
astVisit(program, [], (node) => {
|
|
1426
1426
|
if (node.type === "CallExpression")
|
|
1427
1427
|
calls.push(node);
|
|
@@ -1617,14 +1617,14 @@ var tanstackRuntime = defineRule8({
|
|
|
1617
1617
|
}
|
|
1618
1618
|
},
|
|
1619
1619
|
createOnce(context) {
|
|
1620
|
-
let
|
|
1620
|
+
let options2;
|
|
1621
1621
|
let file = "";
|
|
1622
1622
|
let serverFile = false;
|
|
1623
1623
|
let rscFile = false;
|
|
1624
1624
|
let shared = false;
|
|
1625
1625
|
const runtimeIdentifiers = new Set;
|
|
1626
1626
|
const imports = new Map;
|
|
1627
|
-
const
|
|
1627
|
+
const boundaries2 = [];
|
|
1628
1628
|
function imported(node) {
|
|
1629
1629
|
const entry = imports.get(node.name);
|
|
1630
1630
|
if (!entry)
|
|
@@ -1652,12 +1652,12 @@ var tanstackRuntime = defineRule8({
|
|
|
1652
1652
|
return;
|
|
1653
1653
|
}
|
|
1654
1654
|
function boundary(position) {
|
|
1655
|
-
return
|
|
1655
|
+
return boundaries2.filter((entry) => position >= entry.start && position < entry.end).sort((a, b) => b.start - a.start)[0];
|
|
1656
1656
|
}
|
|
1657
1657
|
function serverPosition(position, staticImport = false) {
|
|
1658
1658
|
const entry = boundary(position);
|
|
1659
1659
|
if (entry)
|
|
1660
|
-
return entry.side === "server" && (!staticImport || entry.kind !== "createIsomorphicFn" ||
|
|
1660
|
+
return entry.side === "server" && (!staticImport || entry.kind !== "createIsomorphicFn" || options2.rpcClients.includes(file));
|
|
1661
1661
|
return serverFile;
|
|
1662
1662
|
}
|
|
1663
1663
|
function report(node, source, reason) {
|
|
@@ -1666,11 +1666,11 @@ var tanstackRuntime = defineRule8({
|
|
|
1666
1666
|
function check(node, source, typeOnly, reExport = false, renderer) {
|
|
1667
1667
|
if (typeOnly)
|
|
1668
1668
|
return;
|
|
1669
|
-
const target = boundaryResolveTarget(
|
|
1669
|
+
const target = boundaryResolveTarget(options2, file, source);
|
|
1670
1670
|
const matches = (values) => values?.some((value) => source === value || source.startsWith(value + "/"));
|
|
1671
|
-
const backend =
|
|
1672
|
-
const server = Boolean(renderer) || builtinModules.includes(source) || source === "@orpc/server" || backend || /\.server(?:\.[cm]?[jt]sx?)?$/.test(target) || /^(?:node:|bun(?::|$)|cloudflare:)/.test(source) || /^@tanstack\/(?:react|solid|vue)-start\/(?:server|server-only)(?:\/|$)/.test(source) || /^react-dom\/server(?:\.|\/|$)/.test(source) || matches(
|
|
1673
|
-
const client = /(?:\/hooks\/|\.client-only(?:\.|$))/.test(target) || /^(?:react-dom\/client|@tanstack\/(?:react|solid|vue)-start\/client-only)(?:\/|$)/.test(source) || matches(
|
|
1671
|
+
const backend = options2.backend.some((path) => target.startsWith(path + "/")) || options2.backendPackages.some((name) => source === name || source.startsWith(name + "/") && /\/(?:server|rpc)(?:\/|$)/.test(source.slice(name.length)));
|
|
1672
|
+
const server = Boolean(renderer) || builtinModules.includes(source) || source === "@orpc/server" || backend || /\.server(?:\.[cm]?[jt]sx?)?$/.test(target) || /^(?:node:|bun(?::|$)|cloudflare:)/.test(source) || /^@tanstack\/(?:react|solid|vue)-start\/(?:server|server-only)(?:\/|$)/.test(source) || /^react-dom\/server(?:\.|\/|$)/.test(source) || matches(options2.serverImports);
|
|
1673
|
+
const client = /(?:\/hooks\/|\.client-only(?:\.|$))/.test(target) || /^(?:react-dom\/client|@tanstack\/(?:react|solid|vue)-start\/client-only)(?:\/|$)/.test(source) || matches(options2.clientImports);
|
|
1674
1674
|
if (client && (serverFile || rscFile || shared) && boundary(node.start)?.side !== "client") {
|
|
1675
1675
|
const references = node.type === "ImportDeclaration" ? context.sourceCode.getDeclaredVariables(node).flatMap((variable) => variable.references.filter((reference) => runtimeIdentifiers.has(reference.identifier.range[0]))) : [];
|
|
1676
1676
|
if (!shared && references.length && references.every((reference) => boundary(reference.identifier.range[0])?.side === "client"))
|
|
@@ -1745,14 +1745,14 @@ var tanstackRuntime = defineRule8({
|
|
|
1745
1745
|
}
|
|
1746
1746
|
return {
|
|
1747
1747
|
Program(program) {
|
|
1748
|
-
|
|
1748
|
+
options2 = optionsFirst(context);
|
|
1749
1749
|
file = context.filename.replaceAll("\\", "/");
|
|
1750
1750
|
serverFile = /\.server\.[cm]?[jt]sx?$/.test(file);
|
|
1751
1751
|
rscFile = /\.rsc\.[cm]?[jt]sx?$/.test(file);
|
|
1752
|
-
shared = /\.(?:utils|constants)\.[cm]?[jt]sx?$/.test(file) ||
|
|
1752
|
+
shared = /\.(?:utils|constants)\.[cm]?[jt]sx?$/.test(file) || options2.web.some((path) => file.startsWith(path + "/lib/"));
|
|
1753
1753
|
runtimeIdentifiers.clear();
|
|
1754
1754
|
imports.clear();
|
|
1755
|
-
|
|
1755
|
+
boundaries2.length = 0;
|
|
1756
1756
|
for (const statement of program.body) {
|
|
1757
1757
|
if (statement.type !== "ImportDeclaration" || statement.importKind === "type")
|
|
1758
1758
|
continue;
|
|
@@ -1795,7 +1795,7 @@ var tanstackRuntime = defineRule8({
|
|
|
1795
1795
|
const side = kind === "createServerOnlyFn" || kind === "createServerFn" && method === "handler" || ["createIsomorphicFn", "createMiddleware"].includes(kind ?? "") && method === "server" ? "server" : kind === "createClientOnlyFn" || ["createIsomorphicFn", "createMiddleware"].includes(kind ?? "") && method === "client" ? "client" : undefined;
|
|
1796
1796
|
const callback = node.arguments[0];
|
|
1797
1797
|
if (kind && side && callback && (callback.type === "FunctionExpression" || callback.type === "ArrowFunctionExpression"))
|
|
1798
|
-
|
|
1798
|
+
boundaries2.push({ start: callback.start, end: callback.end, side, kind });
|
|
1799
1799
|
});
|
|
1800
1800
|
},
|
|
1801
1801
|
ImportDeclaration(node) {
|
|
@@ -1829,7 +1829,7 @@ var tanstackRuntime = defineRule8({
|
|
|
1829
1829
|
ImportExpression(node) {
|
|
1830
1830
|
if (node.source.type === "Literal" && typeof node.source.value === "string")
|
|
1831
1831
|
check(node, node.source.value, false);
|
|
1832
|
-
else if (!
|
|
1832
|
+
else if (!options2.allowComputedImportsIn?.includes(file))
|
|
1833
1833
|
context.report({ node, messageId: "computed" });
|
|
1834
1834
|
},
|
|
1835
1835
|
MemberExpression(node) {
|
|
@@ -1874,19 +1874,19 @@ var importBoundaries = defineRule9({
|
|
|
1874
1874
|
},
|
|
1875
1875
|
createOnce(context) {
|
|
1876
1876
|
function check(node, source, typeOnly, reExport = false) {
|
|
1877
|
-
const
|
|
1877
|
+
const options2 = optionsFirst(context);
|
|
1878
1878
|
const file = context.filename.replaceAll("\\", "/");
|
|
1879
1879
|
const contains = (directory, candidate) => candidate === directory || candidate.startsWith(`${directory}/`);
|
|
1880
|
-
const roots = [...
|
|
1880
|
+
const roots = [...options2.web, ...options2.backend].sort((a, b) => b.length - a.length);
|
|
1881
1881
|
const owner = roots.find((path) => contains(path, file));
|
|
1882
|
-
const web = owner &&
|
|
1883
|
-
const target = boundaryResolveTarget(
|
|
1884
|
-
const backendImport =
|
|
1882
|
+
const web = owner && options2.web.includes(owner) ? owner : undefined;
|
|
1883
|
+
const target = boundaryResolveTarget(options2, file, source);
|
|
1884
|
+
const backendImport = options2.backend.some((path) => contains(path, target)) || options2.backendPackages.some((name) => source === name || source.startsWith(`${name}/`));
|
|
1885
1885
|
let reason;
|
|
1886
|
-
if (web && backendImport && !
|
|
1886
|
+
if (web && backendImport && !options2.publicEntrypoints.includes(source)) {
|
|
1887
1887
|
reason = "frontend code uses only public backend entry points";
|
|
1888
1888
|
}
|
|
1889
|
-
if ((Array.isArray(
|
|
1889
|
+
if ((Array.isArray(options2.packages) ? options2.packages : [options2.packages]).some((path) => path && contains(path, file)) && (backendImport || options2.web.some((path) => contains(path, target)) || options2.appPackages.some((name) => source === name || source.startsWith(`${name}/`)))) {
|
|
1890
1890
|
reason = "shared packages do not import app internals";
|
|
1891
1891
|
}
|
|
1892
1892
|
if (file.includes("/services/") && /\/services\/service(?:\.ts)?$/.test(target) && !typeOnly) {
|
|
@@ -1898,7 +1898,7 @@ var importBoundaries = defineRule9({
|
|
|
1898
1898
|
if (file.endsWith(".client.ts") && !typeOnly && /^(react|react-dom)(\/|$)/.test(source)) {
|
|
1899
1899
|
reason = "client services are React-free; use hooks for React behavior";
|
|
1900
1900
|
}
|
|
1901
|
-
if (
|
|
1901
|
+
if (options2.fileRoles?.length) {
|
|
1902
1902
|
const targetOwner = roots.find((path) => contains(path, target));
|
|
1903
1903
|
const domain = (path, appRoot) => {
|
|
1904
1904
|
const relative = path.slice(appRoot.length + 1);
|
|
@@ -1911,7 +1911,7 @@ var importBoundaries = defineRule9({
|
|
|
1911
1911
|
};
|
|
1912
1912
|
const sameDomain = owner && targetOwner && owner === targetOwner && domain(file, owner) && domain(file, owner) === domain(target, targetOwner);
|
|
1913
1913
|
const targetStem = target.replace(/\.[cm]?[jt]sx?$/, "");
|
|
1914
|
-
const concept =
|
|
1914
|
+
const concept = options2.fileRoles.some((role) => targetStem.endsWith(`.${role}`));
|
|
1915
1915
|
const publicSurface = /\.(?:service|client|server|rsc|utils)$/.test(targetStem) && !/\/(?:[^/]+)-(?:action|query)\./.test(targetStem);
|
|
1916
1916
|
const privateTarget = concept || targetOwner && domain(target, targetOwner) && !publicSurface && !/\.(?:types|constants)$/.test(targetStem);
|
|
1917
1917
|
if (privateTarget && !sameDomain) {
|
|
@@ -1921,11 +1921,11 @@ var importBoundaries = defineRule9({
|
|
|
1921
1921
|
reason = "public service and utility surfaces must not re-export private domain internals";
|
|
1922
1922
|
}
|
|
1923
1923
|
}
|
|
1924
|
-
if (
|
|
1924
|
+
if (options2.portableLib && web && contains(`${web}/lib`, file)) {
|
|
1925
1925
|
const portableTarget = target.startsWith("/") ? resolve2(target).replaceAll("\\", "/") : target;
|
|
1926
1926
|
const appService = roots.some((path) => contains(`${path}/services`, portableTarget));
|
|
1927
|
-
const backendTarget =
|
|
1928
|
-
const appPackage =
|
|
1927
|
+
const backendTarget = options2.backend.some((path) => contains(path, portableTarget));
|
|
1928
|
+
const appPackage = options2.appPackages.some((name) => source === name || source.startsWith(`${name}/`));
|
|
1929
1929
|
const appType = roots.some((path) => contains(`${path}/types`, portableTarget));
|
|
1930
1930
|
const stem = basename2(file).replace(/\.[cm]?[jt]sx?$/, "").replace(/\.utils$/, "");
|
|
1931
1931
|
const ownType = typeOnly && !reExport && portableTarget.replace(/\.[cm]?[jt]sx?$/, "") === `${web}/types/${stem}.types`;
|
|
@@ -1972,9 +1972,9 @@ var importBoundaries = defineRule9({
|
|
|
1972
1972
|
}
|
|
1973
1973
|
},
|
|
1974
1974
|
TSImportType(node) {
|
|
1975
|
-
const
|
|
1975
|
+
const options2 = optionsFirst(context);
|
|
1976
1976
|
const file = context.filename.replaceAll("\\", "/");
|
|
1977
|
-
if (
|
|
1977
|
+
if (options2.portableLib && options2.web.some((path) => file.startsWith(`${path}/lib/`))) {
|
|
1978
1978
|
check(node, node.source.value, true);
|
|
1979
1979
|
}
|
|
1980
1980
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface ModuleBoundaryOptions {
|
|
2
|
+
web: string[];
|
|
3
|
+
backend: string[];
|
|
4
|
+
packages: string | string[];
|
|
5
|
+
fileRoles?: string[];
|
|
6
|
+
portableLib?: boolean;
|
|
7
|
+
backendPackages: string[];
|
|
8
|
+
appPackages: string[];
|
|
9
|
+
aliases: Record<string, string>;
|
|
10
|
+
publicEntrypoints: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface ModuleServiceFunctionsOptions {
|
|
13
|
+
frontend?: boolean;
|
|
14
|
+
allowLocalHelpers?: boolean;
|
|
15
|
+
allowReturnedMethods?: boolean;
|
|
16
|
+
singleExport?: boolean;
|
|
17
|
+
message?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ModuleDomainConstantsOptions {
|
|
20
|
+
allowServiceMethods?: boolean;
|
|
21
|
+
includeData?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface ModuleRuntimeOptions extends Pick<ModuleBoundaryOptions, 'web' | 'backend' | 'aliases' | 'backendPackages'> {
|
|
24
|
+
rpcClients: string[];
|
|
25
|
+
serverImports?: string[];
|
|
26
|
+
clientImports?: string[];
|
|
27
|
+
allowComputedImportsIn?: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface ModuleTestModifiersOptions {
|
|
30
|
+
additionalTestFunctions?: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface ModuleTestAssertionsOptions extends ModuleTestModifiersOptions {
|
|
33
|
+
additionalAssertionFunctions?: string[];
|
|
34
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ESTree } from '@oxlint/plugins';
|
|
2
|
+
import type { AstRuntimeFunction } from '../utils/helpers/ast.ts';
|
|
3
|
+
export interface TestRegistration {
|
|
4
|
+
call: ESTree.CallExpression;
|
|
5
|
+
callback: AstRuntimeFunction;
|
|
6
|
+
table?: ESTree.Expression;
|
|
7
|
+
}
|
|
8
|
+
export interface TestTableRow {
|
|
9
|
+
bindings: Map<ESTree.Node, AstRuntimeFunction>;
|
|
10
|
+
callbacks: AstRuntimeFunction[];
|
|
11
|
+
}
|