oxlint-plugin-arch 0.3.4 → 0.5.0
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 +48 -0
- package/README.md +232 -10
- package/dist/configs/boundaries.d.ts +2 -0
- package/dist/configs/modules.d.ts +2 -0
- package/dist/configs/plugins.d.ts +3 -0
- package/dist/configs/react.d.ts +2 -0
- package/dist/configs/source.d.ts +2 -0
- package/dist/configs/tests.d.ts +2 -0
- package/dist/configs/types.d.ts +2 -0
- package/dist/constants/preset.constants.d.ts +28 -0
- package/dist/index.js +1182 -633
- package/dist/presets/index.d.ts +2 -0
- package/dist/presets/index.js +1354 -0
- package/dist/presets/tanstack-start-react-modules-preset.d.ts +3 -0
- package/dist/presets/tanstack-start-react-modules-preset.js +1354 -0
- package/dist/rules/dillon-anti-slop/index.js +1588 -0
- package/dist/rules/module-policy/index.js +1743 -0
- package/dist/services/preset-compose.d.ts +3 -0
- package/dist/services/preset-context.d.ts +2 -0
- package/dist/types/preset.types.d.ts +119 -0
- package/dist/utils/helpers/preset.d.ts +6 -0
- package/package.json +45 -10
- package/src/rules/dillon-anti-slop/LICENSE +21 -0
- package/src/rules/dillon-anti-slop/UPSTREAM.md +19 -0
|
@@ -0,0 +1,1743 @@
|
|
|
1
|
+
// src/rules/module-policy/index.ts
|
|
2
|
+
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
3
|
+
|
|
4
|
+
// src/utils/helpers/syntax.ts
|
|
5
|
+
import { defineRule } from "@oxlint/plugins";
|
|
6
|
+
function syntaxRule(selector, message) {
|
|
7
|
+
return defineRule({
|
|
8
|
+
meta: { type: "problem", schema: [], messages: { forbidden: message } },
|
|
9
|
+
createOnce(context) {
|
|
10
|
+
return {
|
|
11
|
+
[selector]: (node) => context.report({ node, messageId: "forbidden" })
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/rules/module-policy/service-types.ts
|
|
18
|
+
import { defineRule as defineRule2 } from "@oxlint/plugins";
|
|
19
|
+
var serviceTypes = defineRule2({
|
|
20
|
+
meta: {
|
|
21
|
+
type: "problem",
|
|
22
|
+
schema: [],
|
|
23
|
+
messages: {
|
|
24
|
+
domain: "Move this type to its domain types file; only the matching factory-derived Service alias belongs here."
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
createOnce(context) {
|
|
28
|
+
return {
|
|
29
|
+
TSInterfaceDeclaration(node) {
|
|
30
|
+
context.report({ node, messageId: "domain" });
|
|
31
|
+
},
|
|
32
|
+
TSTypeAliasDeclaration(node) {
|
|
33
|
+
const type = node.typeAnnotation;
|
|
34
|
+
const argument = type.type === "TSTypeReference" ? type.typeArguments?.params[0] : undefined;
|
|
35
|
+
const factory = argument?.type === "TSTypeQuery" && argument.exprName.type === "Identifier" ? argument.exprName.name : undefined;
|
|
36
|
+
if (node.id.name.endsWith("Service") && type.type === "TSTypeReference" && type.typeName.type === "Identifier" && type.typeName.name === "ReturnType" && type.typeArguments?.params.length === 1 && factory === `make${node.id.name}`) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
context.report({ node, messageId: "domain" });
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// src/rules/module-policy/service-functions.ts
|
|
46
|
+
import { defineRule as defineRule3 } from "@oxlint/plugins";
|
|
47
|
+
|
|
48
|
+
// src/utils/helpers/ast.ts
|
|
49
|
+
var astVisitSkip = new Set(["parent", "type", "start", "end", "range", "loc", "tokens", "comments"]);
|
|
50
|
+
function astIsNode(value) {
|
|
51
|
+
return value !== null && typeof value === "object" && typeof Reflect.get(value, "type") === "string";
|
|
52
|
+
}
|
|
53
|
+
function astVisit(node, ancestors, callback) {
|
|
54
|
+
if (Array.isArray(node)) {
|
|
55
|
+
for (const child of node) {
|
|
56
|
+
astVisit(child, ancestors, callback);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!astIsNode(node)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
callback(node, ancestors);
|
|
64
|
+
ancestors.push(node);
|
|
65
|
+
for (const key in node) {
|
|
66
|
+
if (Object.hasOwn(node, key) && !astVisitSkip.has(key)) {
|
|
67
|
+
astVisit(Reflect.get(node, key), ancestors, callback);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
ancestors.pop();
|
|
71
|
+
}
|
|
72
|
+
function astContainsJsx(node) {
|
|
73
|
+
let found = false;
|
|
74
|
+
astVisit(node, [], (candidate) => {
|
|
75
|
+
if (candidate.type === "JSXElement" || candidate.type === "JSXFragment") {
|
|
76
|
+
found = true;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
81
|
+
function astNearestFunction(ancestors) {
|
|
82
|
+
return [...ancestors].reverse().find((node) => node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression");
|
|
83
|
+
}
|
|
84
|
+
function astDirectReturnExpressions(node) {
|
|
85
|
+
if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") {
|
|
86
|
+
return [node.body];
|
|
87
|
+
}
|
|
88
|
+
const expressions = [];
|
|
89
|
+
astVisit(node.body, [node], (candidate, ancestors) => {
|
|
90
|
+
if (candidate.type === "ReturnStatement" && candidate.argument && astNearestFunction(ancestors) === node) {
|
|
91
|
+
expressions.push(candidate.argument);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return expressions;
|
|
95
|
+
}
|
|
96
|
+
function astStaticMemberName(node) {
|
|
97
|
+
return !node.computed && node.property.type === "Identifier" ? node.property.name : undefined;
|
|
98
|
+
}
|
|
99
|
+
function astDottedName(node) {
|
|
100
|
+
if (node.type === "Identifier") {
|
|
101
|
+
return node.name;
|
|
102
|
+
}
|
|
103
|
+
if (node.type !== "MemberExpression") {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const object = astDottedName(node.object);
|
|
107
|
+
const property = astStaticMemberName(node);
|
|
108
|
+
return object && property ? `${object}.${property}` : undefined;
|
|
109
|
+
}
|
|
110
|
+
function astCallName(node) {
|
|
111
|
+
if (node.callee.type === "Identifier") {
|
|
112
|
+
return node.callee.name;
|
|
113
|
+
}
|
|
114
|
+
return node.callee.type === "MemberExpression" ? astStaticMemberName(node.callee) : undefined;
|
|
115
|
+
}
|
|
116
|
+
function astImportedCallAliases(program) {
|
|
117
|
+
const aliases = new Map;
|
|
118
|
+
for (const statement of program.body) {
|
|
119
|
+
if (statement.type !== "ImportDeclaration") {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
for (const specifier of statement.specifiers) {
|
|
123
|
+
if (specifier.type !== "ImportSpecifier") {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const imported = specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
|
|
127
|
+
if (imported !== specifier.local.name) {
|
|
128
|
+
aliases.set(specifier.local.name, imported);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return aliases;
|
|
133
|
+
}
|
|
134
|
+
function astResolvedCallName(node, aliases) {
|
|
135
|
+
const name = astCallName(node);
|
|
136
|
+
return name ? aliases.get(name) ?? name : undefined;
|
|
137
|
+
}
|
|
138
|
+
// src/utils/helpers/comments.ts
|
|
139
|
+
var COMMENTS_DIRECTIVE = /^\s*(?:\/\s*<(?:reference\b|amd-[\w-]+\b)|@ts-(?:check|ignore|expect-error|nocheck)\b|(?:#|@)__(?:PURE|NO_SIDE_EFFECTS)__\b|@jsx(?:ImportSource|Runtime|Frag)?\b|@vite-ignore\b|(?:[#@]\s*)?sourceMappingURL=|(?:es|ox)lint-(?:disable|enable)|prettier-ignore|biome-ignore|[cv]8 ignore|istanbul ignore|webpack[A-Z])/;
|
|
140
|
+
var COMMENTS_DISABLE_NEXT_LINE = /^(?:eslint|oxlint)-disable-next-line\b/;
|
|
141
|
+
var COMMENTS_LEGAL_BLOCK = "\x00legal-block\x00";
|
|
142
|
+
function commentsNormalizedValue(comment) {
|
|
143
|
+
const value = (comment.type === "Block" ? comment.value.replace(/^\s*\* ?/gm, "") : comment.value).trimStart();
|
|
144
|
+
return comment.type === "Block" && comment.value.startsWith("!") ? COMMENTS_LEGAL_BLOCK + value : value;
|
|
145
|
+
}
|
|
146
|
+
function commentsIsDirective(value) {
|
|
147
|
+
return value.startsWith(COMMENTS_LEGAL_BLOCK) || COMMENTS_DIRECTIVE.test(value);
|
|
148
|
+
}
|
|
149
|
+
function commentsIsDisableNextLine(value) {
|
|
150
|
+
return COMMENTS_DISABLE_NEXT_LINE.test(value);
|
|
151
|
+
}
|
|
152
|
+
function commentsIsWhy(value) {
|
|
153
|
+
return value.startsWith("Why: ");
|
|
154
|
+
}
|
|
155
|
+
function commentsIsJsdoc(source, comment) {
|
|
156
|
+
return comment.type === "Block" && source.startsWith("/**", comment.range[0]);
|
|
157
|
+
}
|
|
158
|
+
// src/utils/helpers/trivial.ts
|
|
159
|
+
function trivialExpression(node) {
|
|
160
|
+
if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") {
|
|
161
|
+
return node.body;
|
|
162
|
+
}
|
|
163
|
+
if (!node.body || node.body.type !== "BlockStatement")
|
|
164
|
+
return null;
|
|
165
|
+
const statements = node.body.body.filter((statement) => statement.type !== "EmptyStatement" && !(statement.type === "ExpressionStatement" && statement.directive));
|
|
166
|
+
if (statements.length !== 1)
|
|
167
|
+
return null;
|
|
168
|
+
const statement = statements[0];
|
|
169
|
+
if (statement.type === "ReturnStatement")
|
|
170
|
+
return statement.argument;
|
|
171
|
+
return statement.type === "ExpressionStatement" ? statement.expression : null;
|
|
172
|
+
}
|
|
173
|
+
function trivialUnwrap(expression) {
|
|
174
|
+
if (expression.type === "ParenthesizedExpression" || expression.type === "TSAsExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSTypeAssertion" || expression.type === "TSNonNullExpression" || expression.type === "ChainExpression") {
|
|
175
|
+
return trivialUnwrap(expression.expression);
|
|
176
|
+
}
|
|
177
|
+
return expression.type === "AwaitExpression" ? trivialUnwrap(expression.argument) : expression;
|
|
178
|
+
}
|
|
179
|
+
function trivialCallRoot(expression) {
|
|
180
|
+
if (expression.type === "Identifier")
|
|
181
|
+
return expression.name;
|
|
182
|
+
if (expression.type === "MemberExpression" && !expression.computed && !expression.optional) {
|
|
183
|
+
return trivialCallRoot(expression.object);
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
function trivialIsForwarder(node, expression) {
|
|
188
|
+
const value = trivialUnwrap(expression);
|
|
189
|
+
if (value.type === "Literal")
|
|
190
|
+
return !("regex" in value);
|
|
191
|
+
if (value.type === "TemplateLiteral")
|
|
192
|
+
return value.expressions.length === 0;
|
|
193
|
+
if (value.type === "Identifier")
|
|
194
|
+
return true;
|
|
195
|
+
if (value.type !== "CallExpression" || value.optional)
|
|
196
|
+
return false;
|
|
197
|
+
const root = trivialCallRoot(value.callee);
|
|
198
|
+
if (!root || node.params.some((param) => param.type !== "Identifier"))
|
|
199
|
+
return false;
|
|
200
|
+
const parameters = node.params.flatMap((param) => param.type === "Identifier" ? [param.name] : []);
|
|
201
|
+
const arguments_ = value.arguments;
|
|
202
|
+
const receiver = value.callee.type === "MemberExpression" ? root : undefined;
|
|
203
|
+
const forwarded = parameters.filter((name) => name !== receiver);
|
|
204
|
+
return arguments_.length === forwarded.length && arguments_.every((argument, index) => argument.type === "Identifier" && argument.name === forwarded[index]);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/utils/helpers/declarations.ts
|
|
208
|
+
function declarationsTopLevelUnexportedFunctions(program) {
|
|
209
|
+
return program.body.flatMap((statement) => {
|
|
210
|
+
if (statement.type === "FunctionDeclaration") {
|
|
211
|
+
return [{ name: statement.id?.name ?? "<anonymous>", node: statement }];
|
|
212
|
+
}
|
|
213
|
+
if (statement.type !== "VariableDeclaration") {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
return statement.declarations.flatMap((item) => item.init?.type === "FunctionExpression" || item.init?.type === "ArrowFunctionExpression" ? [{ name: item.id.type === "Identifier" ? item.id.name : "<anonymous>", node: item.init }] : []);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function declarationsFileLevelFunctionCandidates(program) {
|
|
220
|
+
const result = [];
|
|
221
|
+
for (const statement of program.body) {
|
|
222
|
+
const declaration = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration : statement;
|
|
223
|
+
if (declaration?.type === "FunctionDeclaration") {
|
|
224
|
+
result.push({ name: declaration.id?.name ?? "<anonymous>", node: declaration });
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (declaration?.type !== "VariableDeclaration" || declaration.kind !== "const") {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
for (const item of declaration.declarations) {
|
|
231
|
+
if (item.init?.type === "FunctionExpression" || item.init?.type === "ArrowFunctionExpression") {
|
|
232
|
+
result.push({
|
|
233
|
+
name: item.id.type === "Identifier" ? item.id.name : "<anonymous>",
|
|
234
|
+
node: declaration,
|
|
235
|
+
initializer: item.init
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
function declarationsIsObjectParam(node) {
|
|
243
|
+
if (node.type === "ObjectPattern") {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
if (node.type === "TSParameterProperty") {
|
|
247
|
+
return declarationsIsObjectParam(node.parameter);
|
|
248
|
+
}
|
|
249
|
+
const annotation = node.typeAnnotation?.typeAnnotation;
|
|
250
|
+
return !annotation || annotation.type === "TSTypeReference" || annotation.type === "TSTypeLiteral" || annotation.type === "TSIntersectionType";
|
|
251
|
+
}
|
|
252
|
+
function declarationsFunctionName(node) {
|
|
253
|
+
if (node.type === "VariableDeclarator") {
|
|
254
|
+
return node.id.type === "Identifier" ? node.id.name : "<anonymous>";
|
|
255
|
+
}
|
|
256
|
+
return node.id?.name ?? "<anonymous>";
|
|
257
|
+
}
|
|
258
|
+
function declarationsIsRuntime(node) {
|
|
259
|
+
return node.type === "VariableDeclaration" || node.type === "FunctionDeclaration" || node.type === "ClassDeclaration" || node.type === "TSEnumDeclaration";
|
|
260
|
+
}
|
|
261
|
+
function declarationsRuntimeName(node) {
|
|
262
|
+
if (node.type === "VariableDeclaration") {
|
|
263
|
+
return node.declarations.map((item) => item.id.type === "Identifier" ? item.id.name : "<anonymous>").join(", ");
|
|
264
|
+
}
|
|
265
|
+
return node.id?.name ?? "<anonymous>";
|
|
266
|
+
}
|
|
267
|
+
function declarationsRuntimeKind(node) {
|
|
268
|
+
if (node.type === "ClassDeclaration") {
|
|
269
|
+
return "class";
|
|
270
|
+
}
|
|
271
|
+
if (node.type === "TSEnumDeclaration") {
|
|
272
|
+
return "enum";
|
|
273
|
+
}
|
|
274
|
+
if (node.type === "FunctionDeclaration") {
|
|
275
|
+
return "function";
|
|
276
|
+
}
|
|
277
|
+
return "variable";
|
|
278
|
+
}
|
|
279
|
+
function declarationsUnwrapExpression(node) {
|
|
280
|
+
if (node.type === "ParenthesizedExpression" || node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSTypeAssertion" || node.type === "TSNonNullExpression") {
|
|
281
|
+
return declarationsUnwrapExpression(node.expression);
|
|
282
|
+
}
|
|
283
|
+
if (node.type === "ChainExpression") {
|
|
284
|
+
return declarationsUnwrapExpression(node.expression);
|
|
285
|
+
}
|
|
286
|
+
return node.type === "AwaitExpression" && node.argument ? declarationsUnwrapExpression(node.argument) : node;
|
|
287
|
+
}
|
|
288
|
+
function declarationsIsTrivialExpression(node) {
|
|
289
|
+
const value = declarationsUnwrapExpression(node);
|
|
290
|
+
if (value.type === "Identifier" || value.type === "ThisExpression" || value.type === "MetaProperty" || value.type.endsWith("Literal")) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
if (value.type === "TemplateLiteral") {
|
|
294
|
+
return value.expressions.length === 0;
|
|
295
|
+
}
|
|
296
|
+
if (value.type === "MemberExpression" || value.type === "CallExpression" || value.type === "NewExpression" || value.type === "TaggedTemplateExpression") {
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
return value.type === "LogicalExpression" && declarationsIsTrivialExpression(value.left) && declarationsIsTrivialExpression(value.right);
|
|
300
|
+
}
|
|
301
|
+
function declarationsIsTrivialFunction(node) {
|
|
302
|
+
if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") {
|
|
303
|
+
return declarationsIsTrivialExpression(node.body);
|
|
304
|
+
}
|
|
305
|
+
if (!node.body || node.body.type !== "BlockStatement") {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
const statements = node.body.body.filter((statement) => statement.type !== "EmptyStatement" && !(statement.type === "ExpressionStatement" && statement.directive));
|
|
309
|
+
if (statements.length === 0) {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
if (statements.length > 1) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
const statement = statements[0];
|
|
316
|
+
if (statement.type === "ReturnStatement") {
|
|
317
|
+
return !statement.argument || declarationsIsTrivialExpression(statement.argument);
|
|
318
|
+
}
|
|
319
|
+
return statement.type === "ExpressionStatement" && declarationsIsTrivialExpression(statement.expression);
|
|
320
|
+
}
|
|
321
|
+
var declarationsKinds = ["function", "const", "let", "var", "class", "type", "interface", "enum"];
|
|
322
|
+
function declarationsNamedKind(node, ancestors) {
|
|
323
|
+
if (node.type === "FunctionDeclaration") {
|
|
324
|
+
return "function";
|
|
325
|
+
}
|
|
326
|
+
if (node.type === "ClassDeclaration") {
|
|
327
|
+
return "class";
|
|
328
|
+
}
|
|
329
|
+
if (node.type === "TSTypeAliasDeclaration") {
|
|
330
|
+
return "type";
|
|
331
|
+
}
|
|
332
|
+
if (node.type === "TSInterfaceDeclaration") {
|
|
333
|
+
return "interface";
|
|
334
|
+
}
|
|
335
|
+
if (node.type === "TSEnumDeclaration") {
|
|
336
|
+
return "enum";
|
|
337
|
+
}
|
|
338
|
+
if (node.type !== "VariableDeclarator") {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const declaration = ancestors.findLast((ancestor) => ancestor.type === "VariableDeclaration");
|
|
342
|
+
return declaration?.type === "VariableDeclaration" && (declaration.kind === "const" || declaration.kind === "let" || declaration.kind === "var") ? declaration.kind : undefined;
|
|
343
|
+
}
|
|
344
|
+
function declarationsCollectNamed(root, kinds) {
|
|
345
|
+
const allowed = kinds && kinds.length > 0 ? new Set(kinds) : null;
|
|
346
|
+
const result = [];
|
|
347
|
+
astVisit(root, [], (node, ancestors) => {
|
|
348
|
+
const kind = declarationsNamedKind(node, ancestors);
|
|
349
|
+
const name = node.type === "VariableDeclarator" && node.id.type === "Identifier" ? node.id.name : ("id" in node) && node.id && ("name" in node.id) ? node.id.name : undefined;
|
|
350
|
+
if (!kind || !name || allowed && !allowed.has(kind)) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (node.type === "VariableDeclarator" && ancestors.some((ancestor) => ancestor.type === "ForStatement" || ancestor.type === "ForInStatement" || ancestor.type === "ForOfStatement")) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
result.push({ name, node, kind });
|
|
357
|
+
});
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
function declarationsInsideForLoop(node) {
|
|
361
|
+
for (let current = node.parent;current; current = current.parent) {
|
|
362
|
+
if (current.type === "ForStatement" || current.type === "ForInStatement" || current.type === "ForOfStatement") {
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
function declarationsNamedFromNode(node) {
|
|
369
|
+
if (node.type === "VariableDeclarator") {
|
|
370
|
+
const kind = declarationsNamedKind(node, [node.parent]);
|
|
371
|
+
return kind && node.id.type === "Identifier" && !declarationsInsideForLoop(node) ? { name: node.id.name, node, kind } : undefined;
|
|
372
|
+
}
|
|
373
|
+
const kind = declarationsNamedKind(node, []);
|
|
374
|
+
const name = "id" in node && node.id && "name" in node.id ? node.id.name : undefined;
|
|
375
|
+
return kind && name ? { name, node, kind } : undefined;
|
|
376
|
+
}
|
|
377
|
+
function declarationsCollectFunctions(root) {
|
|
378
|
+
return declarationsCollectNamed(root).flatMap((item) => {
|
|
379
|
+
if (item.node.type === "FunctionDeclaration") {
|
|
380
|
+
return [{ name: item.name, node: item.node }];
|
|
381
|
+
}
|
|
382
|
+
if (item.node.type === "VariableDeclarator" && (item.node.init?.type === "FunctionExpression" || item.node.init?.type === "ArrowFunctionExpression")) {
|
|
383
|
+
return [{ name: item.name, node: item.node.init }];
|
|
384
|
+
}
|
|
385
|
+
return [];
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
function declarationsPublicServiceMethods(program) {
|
|
389
|
+
return program.body.flatMap((statement) => {
|
|
390
|
+
if (statement.type !== "ExportNamedDeclaration" || statement.declaration?.type !== "VariableDeclaration" || statement.declaration.kind !== "const")
|
|
391
|
+
return [];
|
|
392
|
+
return statement.declaration.declarations.flatMap((binding) => {
|
|
393
|
+
if (binding.id.type !== "Identifier" || !binding.init)
|
|
394
|
+
return [];
|
|
395
|
+
const value = trivialUnwrap(binding.init);
|
|
396
|
+
if (value.type !== "ObjectExpression" || value.properties.length === 0)
|
|
397
|
+
return [];
|
|
398
|
+
const methods = value.properties.flatMap((property) => property.type === "Property" && property.method && property.kind === "init" && !property.computed && property.key.type === "Identifier" && property.value.type === "FunctionExpression" ? [{ name: property.key.name, node: property.value, binding }] : []);
|
|
399
|
+
return methods.length === value.properties.length ? methods : [];
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
// src/utils/helpers/exports.ts
|
|
404
|
+
function moduleExportName(node) {
|
|
405
|
+
return node.type === "Identifier" ? node.name : node.value;
|
|
406
|
+
}
|
|
407
|
+
function declarationBindings(declaration) {
|
|
408
|
+
if (!declaration || declaration.type === "TSModuleDeclaration") {
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
if (declaration.type === "VariableDeclaration") {
|
|
412
|
+
return declaration.declarations.map((item) => ({
|
|
413
|
+
exportedName: item.id.type === "Identifier" ? item.id.name : "<anonymous>",
|
|
414
|
+
localName: item.id.type === "Identifier" ? item.id.name : undefined,
|
|
415
|
+
node: declaration,
|
|
416
|
+
declaration,
|
|
417
|
+
initializer: item.init,
|
|
418
|
+
typeOnly: false,
|
|
419
|
+
default: false,
|
|
420
|
+
reExport: false
|
|
421
|
+
}));
|
|
422
|
+
}
|
|
423
|
+
const name = declaration.id?.name ?? "<anonymous>";
|
|
424
|
+
return [
|
|
425
|
+
{
|
|
426
|
+
exportedName: name,
|
|
427
|
+
localName: declaration.id?.name,
|
|
428
|
+
node: declaration,
|
|
429
|
+
declaration,
|
|
430
|
+
typeOnly: declaration.type === "TSInterfaceDeclaration" || declaration.type === "TSTypeAliasDeclaration" || declaration.type === "TSDeclareFunction",
|
|
431
|
+
default: false,
|
|
432
|
+
reExport: false
|
|
433
|
+
}
|
|
434
|
+
];
|
|
435
|
+
}
|
|
436
|
+
function localBindings(program) {
|
|
437
|
+
const result = new Map;
|
|
438
|
+
for (const statement of program.body) {
|
|
439
|
+
if (statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration") {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (statement.type.endsWith("Declaration")) {
|
|
443
|
+
for (const binding of declarationBindings(statement)) {
|
|
444
|
+
if (binding.localName) {
|
|
445
|
+
result.set(binding.localName, binding);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
function exportsCollect(program) {
|
|
453
|
+
const locals = localBindings(program);
|
|
454
|
+
return program.body.flatMap((statement) => {
|
|
455
|
+
if (statement.type === "ExportAllDeclaration") {
|
|
456
|
+
return [
|
|
457
|
+
{
|
|
458
|
+
exportedName: "*",
|
|
459
|
+
node: statement,
|
|
460
|
+
typeOnly: statement.exportKind === "type",
|
|
461
|
+
default: false,
|
|
462
|
+
reExport: true
|
|
463
|
+
}
|
|
464
|
+
];
|
|
465
|
+
}
|
|
466
|
+
if (statement.type === "ExportDefaultDeclaration") {
|
|
467
|
+
if (statement.declaration.type === "Identifier") {
|
|
468
|
+
const local = locals.get(statement.declaration.name);
|
|
469
|
+
return [
|
|
470
|
+
{
|
|
471
|
+
...local ?? {
|
|
472
|
+
exportedName: "default",
|
|
473
|
+
node: statement,
|
|
474
|
+
typeOnly: false,
|
|
475
|
+
default: true,
|
|
476
|
+
reExport: false
|
|
477
|
+
},
|
|
478
|
+
exportedName: "default",
|
|
479
|
+
localName: local?.localName ?? statement.declaration.name,
|
|
480
|
+
default: true
|
|
481
|
+
}
|
|
482
|
+
];
|
|
483
|
+
}
|
|
484
|
+
if (statement.declaration.type.endsWith("Declaration")) {
|
|
485
|
+
const bindings = declarationBindings(statement.declaration);
|
|
486
|
+
return bindings.length > 0 ? bindings.map((binding) => ({ ...binding, exportedName: "default", default: true })) : [{ exportedName: "default", node: statement, typeOnly: false, default: true, reExport: false }];
|
|
487
|
+
}
|
|
488
|
+
return [{ exportedName: "default", node: statement, typeOnly: false, default: true, reExport: false }];
|
|
489
|
+
}
|
|
490
|
+
if (statement.type !== "ExportNamedDeclaration") {
|
|
491
|
+
return [];
|
|
492
|
+
}
|
|
493
|
+
return [
|
|
494
|
+
...declarationBindings(statement.declaration),
|
|
495
|
+
...statement.specifiers.map((specifier) => {
|
|
496
|
+
const localName = moduleExportName(specifier.local);
|
|
497
|
+
const local = locals.get(localName);
|
|
498
|
+
return {
|
|
499
|
+
...local ?? {
|
|
500
|
+
exportedName: moduleExportName(specifier.exported),
|
|
501
|
+
node: specifier,
|
|
502
|
+
typeOnly: false,
|
|
503
|
+
default: false,
|
|
504
|
+
reExport: Boolean(statement.source)
|
|
505
|
+
},
|
|
506
|
+
exportedName: moduleExportName(specifier.exported),
|
|
507
|
+
localName,
|
|
508
|
+
node: local?.node ?? specifier,
|
|
509
|
+
typeOnly: specifier.exportKind === "type" || statement.exportKind === "type" || Boolean(local?.typeOnly),
|
|
510
|
+
default: moduleExportName(specifier.exported) === "default",
|
|
511
|
+
reExport: Boolean(statement.source)
|
|
512
|
+
};
|
|
513
|
+
})
|
|
514
|
+
];
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
function exportsCollectFunctions(program) {
|
|
518
|
+
return exportsCollect(program).flatMap((binding) => {
|
|
519
|
+
const name = binding.localName ?? binding.exportedName;
|
|
520
|
+
if (binding.declaration?.type === "FunctionDeclaration") {
|
|
521
|
+
return [{ name, node: binding.declaration }];
|
|
522
|
+
}
|
|
523
|
+
return binding.initializer?.type === "FunctionExpression" || binding.initializer?.type === "ArrowFunctionExpression" ? [{ name, node: binding.initializer }] : [];
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
// src/utils/helpers/naming.ts
|
|
527
|
+
function namingPosixPath(filePath) {
|
|
528
|
+
return filePath.replaceAll("\\", "/");
|
|
529
|
+
}
|
|
530
|
+
function namingFileBasename(filePath) {
|
|
531
|
+
const normalized = namingPosixPath(filePath);
|
|
532
|
+
const slash = normalized.lastIndexOf("/");
|
|
533
|
+
return slash === -1 ? normalized : normalized.slice(slash + 1);
|
|
534
|
+
}
|
|
535
|
+
function namingDirSegments(filePath) {
|
|
536
|
+
const parts = namingPosixPath(filePath).split("/").filter(Boolean);
|
|
537
|
+
return parts.slice(0, -1);
|
|
538
|
+
}
|
|
539
|
+
function namingSegmentsAfter(filePath, after) {
|
|
540
|
+
const dirs = namingDirSegments(filePath);
|
|
541
|
+
const marker = after.split("/").filter(Boolean);
|
|
542
|
+
if (marker.length === 0) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
for (let index = dirs.length - marker.length;index >= 0; index -= 1) {
|
|
546
|
+
if (marker.every((part, offset) => dirs[index + offset] === part)) {
|
|
547
|
+
return dirs.slice(index + marker.length);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function namingFolderPrefixes(folders, separator, singularize) {
|
|
552
|
+
const joined = folders.join(separator);
|
|
553
|
+
const last = folders.at(-1) ?? "";
|
|
554
|
+
const prefixes = [joined];
|
|
555
|
+
if (singularize === "trailing-s" && last.endsWith("s") && last.length > 1) {
|
|
556
|
+
prefixes.push([...folders.slice(0, -1), last.slice(0, -1)].join(separator));
|
|
557
|
+
}
|
|
558
|
+
return prefixes.filter(Boolean);
|
|
559
|
+
}
|
|
560
|
+
function namingPascalCase(value) {
|
|
561
|
+
return value.split(/[-_\s.]+/).filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("");
|
|
562
|
+
}
|
|
563
|
+
function namingCamelCase(value) {
|
|
564
|
+
const next = namingPascalCase(value);
|
|
565
|
+
return `${next.charAt(0).toLowerCase()}${next.slice(1)}`;
|
|
566
|
+
}
|
|
567
|
+
function namingMatchTemplate(basename, template, placeholderPattern = "[a-z0-9-]+") {
|
|
568
|
+
let pattern = "";
|
|
569
|
+
let index = 0;
|
|
570
|
+
while (index < template.length) {
|
|
571
|
+
const char = template[index];
|
|
572
|
+
if (char === "{") {
|
|
573
|
+
const end = template.indexOf("}", index);
|
|
574
|
+
if (end === -1) {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
const key = template.slice(index + 1, end);
|
|
578
|
+
pattern += `(?<${key}>${placeholderPattern})`;
|
|
579
|
+
index = end + 1;
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
pattern += /[.+^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
|
|
583
|
+
index += 1;
|
|
584
|
+
}
|
|
585
|
+
return new RegExp(`^${pattern}$`).exec(basename)?.groups ?? null;
|
|
586
|
+
}
|
|
587
|
+
var namingStemModes = ["before-first-dot", "full-basename"];
|
|
588
|
+
function namingNormalized(value, normalize) {
|
|
589
|
+
return normalize === "none" ? value : value.replaceAll(/[-_.]/g, "").toLowerCase();
|
|
590
|
+
}
|
|
591
|
+
function namingFileStem(basename, mode = "before-first-dot", trailingRoles = [], roleSeparators = [".", "-"]) {
|
|
592
|
+
const stem = mode === "full-basename" ? basename : basename.split(".")[0] ?? "";
|
|
593
|
+
const roles = [...new Set(trailingRoles.filter(Boolean))].sort((left, right) => right.length - left.length);
|
|
594
|
+
const separators = [...new Set(roleSeparators.filter(Boolean))];
|
|
595
|
+
if (roles.length === 0 || separators.length === 0) {
|
|
596
|
+
return stem;
|
|
597
|
+
}
|
|
598
|
+
let remaining = stem;
|
|
599
|
+
let changed = true;
|
|
600
|
+
while (changed) {
|
|
601
|
+
changed = false;
|
|
602
|
+
for (const role of roles) {
|
|
603
|
+
for (const separator of separators) {
|
|
604
|
+
const suffix = `${separator}${role}`;
|
|
605
|
+
if (remaining.length > suffix.length && remaining.endsWith(suffix)) {
|
|
606
|
+
remaining = remaining.slice(0, -suffix.length);
|
|
607
|
+
changed = true;
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (changed) {
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return remaining;
|
|
617
|
+
}
|
|
618
|
+
function namingFilePrefixes(stem, normalize = "remove-separators", singularize = "none") {
|
|
619
|
+
const prefixes = [namingNormalized(stem, normalize)];
|
|
620
|
+
const parts = stem.split(/[-_]+/).filter(Boolean);
|
|
621
|
+
const last = parts.at(-1) ?? "";
|
|
622
|
+
if (singularize === "trailing-s" && last.endsWith("s") && last.length > 1) {
|
|
623
|
+
prefixes.push(namingNormalized([...parts.slice(0, -1), last.slice(0, -1)].join("-"), normalize));
|
|
624
|
+
}
|
|
625
|
+
return [...new Set(prefixes.filter(Boolean))];
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/utils/helpers/factories.ts
|
|
629
|
+
function factoriesExpandTemplate(template, groups, camelCase = false) {
|
|
630
|
+
return template.replaceAll(/\{([^}]+)\}/g, (placeholder, key) => {
|
|
631
|
+
const group = groups[key.toLowerCase()];
|
|
632
|
+
if (!group) {
|
|
633
|
+
return placeholder;
|
|
634
|
+
}
|
|
635
|
+
if (/^[A-Z]/.test(key)) {
|
|
636
|
+
return namingPascalCase(group);
|
|
637
|
+
}
|
|
638
|
+
return camelCase ? namingCamelCase(group) : group;
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
function factoriesCollectTopLevel(program, pattern) {
|
|
642
|
+
return program.body.flatMap((statement) => {
|
|
643
|
+
const declaration = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration : statement;
|
|
644
|
+
if (declaration?.type === "FunctionDeclaration") {
|
|
645
|
+
const name = declaration.id?.name ?? "<anonymous>";
|
|
646
|
+
return pattern.test(name) ? [{ name, node: declaration }] : [];
|
|
647
|
+
}
|
|
648
|
+
if (declaration?.type !== "VariableDeclaration") {
|
|
649
|
+
return [];
|
|
650
|
+
}
|
|
651
|
+
return declaration.declarations.flatMap((item) => item.id.type === "Identifier" && pattern.test(item.id.name) && (item.init?.type === "FunctionExpression" || item.init?.type === "ArrowFunctionExpression") ? [{ name: item.id.name, node: item.init }] : []);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
function factoriesDirectlyReturnedObjects(factory) {
|
|
655
|
+
if (factory.node.type === "ArrowFunctionExpression" && factory.node.body.type === "ObjectExpression") {
|
|
656
|
+
return [factory.node.body];
|
|
657
|
+
}
|
|
658
|
+
const result = [];
|
|
659
|
+
astVisit(factory.node.body, [factory.node], (node, ancestors) => {
|
|
660
|
+
if (node.type === "ReturnStatement" && node.argument?.type === "ObjectExpression" && astNearestFunction(ancestors) === factory.node) {
|
|
661
|
+
result.push(node.argument);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
return result;
|
|
665
|
+
}
|
|
666
|
+
function factoriesObjectPropertyName(property) {
|
|
667
|
+
if (property.type === "SpreadElement" || property.computed) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (property.key.type === "Identifier") {
|
|
671
|
+
return property.key.name;
|
|
672
|
+
}
|
|
673
|
+
return property.key.type === "Literal" && typeof property.key.value === "string" ? property.key.value : undefined;
|
|
674
|
+
}
|
|
675
|
+
// src/utils/helpers/guards.ts
|
|
676
|
+
var guardsPrimitiveNames = new Set([
|
|
677
|
+
"string",
|
|
678
|
+
"number",
|
|
679
|
+
"boolean",
|
|
680
|
+
"object",
|
|
681
|
+
"undefined",
|
|
682
|
+
"bigint",
|
|
683
|
+
"symbol",
|
|
684
|
+
"function"
|
|
685
|
+
]);
|
|
686
|
+
function guardsValue(node, state) {
|
|
687
|
+
const value = trivialUnwrap(node);
|
|
688
|
+
if (value.type === "Identifier")
|
|
689
|
+
return state.values.has(value.name);
|
|
690
|
+
return value.type === "MemberExpression" && value.computed && value.object.type === "Identifier" && state.values.has(value.object.name) && value.property.type === "Identifier" && state.values.has(value.property.name);
|
|
691
|
+
}
|
|
692
|
+
function guardsFallback(node) {
|
|
693
|
+
return node.type === "Literal" && !("regex" in node) || node.type === "Identifier" && node.name === "undefined" || node.type === "ArrayExpression" && node.elements.length === 0 || node.type === "ObjectExpression" && node.properties.length === 0;
|
|
694
|
+
}
|
|
695
|
+
function guardsPredicate(expression, state) {
|
|
696
|
+
const node = trivialUnwrap(expression);
|
|
697
|
+
if (node.type === "Identifier")
|
|
698
|
+
return state.predicates.has(node.name);
|
|
699
|
+
if (node.type === "UnaryExpression" && node.operator === "!")
|
|
700
|
+
return guardsPredicate(node.argument, state);
|
|
701
|
+
if (node.type === "LogicalExpression" && (node.operator === "&&" || node.operator === "||")) {
|
|
702
|
+
return guardsPredicate(node.left, state) && guardsPredicate(node.right, state);
|
|
703
|
+
}
|
|
704
|
+
if (node.type === "BinaryExpression" && ["===", "!==", "==", "!="].includes(node.operator)) {
|
|
705
|
+
for (const [left, right] of [
|
|
706
|
+
[node.left, node.right],
|
|
707
|
+
[node.right, node.left]
|
|
708
|
+
]) {
|
|
709
|
+
if (left.type === "UnaryExpression" && left.operator === "typeof" && right.type === "Literal" && typeof right.value === "string" && guardsPrimitiveNames.has(right.value) && guardsValue(left.argument, state)) {
|
|
710
|
+
state.primitive = true;
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
if (left.type !== "PrivateIdentifier" && guardsValue(left, state) && (right.type === "Literal" && right.value === null || right.type === "Identifier" && right.name === "undefined"))
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (node.type === "CallExpression" && !node.optional && node.arguments.length === 1 && node.arguments[0].type !== "SpreadElement" && guardsValue(node.arguments[0], state) && node.callee.type === "MemberExpression" && !node.callee.computed && !node.callee.optional && node.callee.object.type === "Identifier" && node.callee.object.name === "Array" && node.callee.property.type === "Identifier" && node.callee.property.name === "isArray" && state.globalArray(node.callee.object)) {
|
|
718
|
+
state.primitive = true;
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
function guardsResult(node, state) {
|
|
724
|
+
const value = trivialUnwrap(node);
|
|
725
|
+
if (guardsValue(value, state) || guardsFallback(value) || guardsPredicate(value, state))
|
|
726
|
+
return true;
|
|
727
|
+
return value.type === "ConditionalExpression" && guardsPredicate(value.test, state) && guardsResult(value.consequent, state) && guardsResult(value.alternate, state);
|
|
728
|
+
}
|
|
729
|
+
function guardsStatement(node, state) {
|
|
730
|
+
if (node.type === "EmptyStatement" || node.type === "ExpressionStatement" && Boolean(node.directive))
|
|
731
|
+
return true;
|
|
732
|
+
if (node.type === "ReturnStatement" && node.argument) {
|
|
733
|
+
state.returned = true;
|
|
734
|
+
return guardsResult(node.argument, state);
|
|
735
|
+
}
|
|
736
|
+
if (node.type === "BlockStatement")
|
|
737
|
+
return node.body.every((statement) => guardsStatement(statement, state));
|
|
738
|
+
if (node.type === "IfStatement") {
|
|
739
|
+
return guardsPredicate(node.test, state) && guardsStatement(node.consequent, state) && (!node.alternate || guardsStatement(node.alternate, state));
|
|
740
|
+
}
|
|
741
|
+
if (node.type === "VariableDeclaration" && node.kind === "const") {
|
|
742
|
+
return node.declarations.every((declaration) => {
|
|
743
|
+
if (declaration.id.type !== "Identifier" || !declaration.init)
|
|
744
|
+
return false;
|
|
745
|
+
if (guardsValue(declaration.init, state)) {
|
|
746
|
+
state.values.add(declaration.id.name);
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
if (guardsPredicate(declaration.init, state)) {
|
|
750
|
+
state.predicates.add(declaration.id.name);
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
return guardsFallback(declaration.init);
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
function guardsIsGenericHelper(node, globalArray) {
|
|
759
|
+
if (!node.body || node.params.length === 0 || node.params.some((param) => param.type !== "Identifier"))
|
|
760
|
+
return false;
|
|
761
|
+
const state = {
|
|
762
|
+
values: new Set(node.params.flatMap((param) => param.type === "Identifier" ? [param.name] : [])),
|
|
763
|
+
predicates: new Set,
|
|
764
|
+
primitive: false,
|
|
765
|
+
returned: false,
|
|
766
|
+
globalArray
|
|
767
|
+
};
|
|
768
|
+
if (node.body.type !== "BlockStatement")
|
|
769
|
+
return guardsResult(node.body, state) && state.primitive;
|
|
770
|
+
return node.body.body.every((statement) => guardsStatement(statement, state)) && state.primitive && state.returned;
|
|
771
|
+
}
|
|
772
|
+
// src/utils/helpers/layout.ts
|
|
773
|
+
var layoutIndentSchema = {
|
|
774
|
+
anyOf: [
|
|
775
|
+
{ type: "integer", minimum: 0 },
|
|
776
|
+
{ type: "string", enum: ["tab"] }
|
|
777
|
+
]
|
|
778
|
+
};
|
|
779
|
+
function layoutLineIndent(text, offset) {
|
|
780
|
+
const lineStart = text.lastIndexOf(`
|
|
781
|
+
`, offset - 1) + 1;
|
|
782
|
+
return /^[\t ]*/.exec(text.slice(lineStart, offset))?.[0] ?? "";
|
|
783
|
+
}
|
|
784
|
+
function layoutIndentUnit(indent = 2) {
|
|
785
|
+
return indent === "tab" ? "\t" : " ".repeat(indent);
|
|
786
|
+
}
|
|
787
|
+
function layoutNewline(text) {
|
|
788
|
+
return text.includes(`\r
|
|
789
|
+
`) ? `\r
|
|
790
|
+
` : `
|
|
791
|
+
`;
|
|
792
|
+
}
|
|
793
|
+
// src/utils/helpers/options.ts
|
|
794
|
+
function optionsFirst(context, fallback) {
|
|
795
|
+
return context.options[0] ?? fallback;
|
|
796
|
+
}
|
|
797
|
+
var optionsPatternSchema = {
|
|
798
|
+
anyOf: [{ type: "string" }, { type: "array", items: { type: "string" }, minItems: 1 }]
|
|
799
|
+
};
|
|
800
|
+
function optionsPatterns(pattern, flags) {
|
|
801
|
+
return pattern === undefined ? [] : [pattern].flat().map((source) => new RegExp(source, flags));
|
|
802
|
+
}
|
|
803
|
+
function optionsOptionalPatterns(pattern, flags) {
|
|
804
|
+
return optionsPatterns(pattern === undefined ? undefined : [pattern].flat().filter((source) => source !== ""), flags);
|
|
805
|
+
}
|
|
806
|
+
function optionsPatternLabel(pattern) {
|
|
807
|
+
return pattern === undefined ? "" : [pattern].flat().join(" | ");
|
|
808
|
+
}
|
|
809
|
+
function optionsPatternsTest(patterns, value) {
|
|
810
|
+
return patterns.some((pattern) => {
|
|
811
|
+
pattern.lastIndex = 0;
|
|
812
|
+
return pattern.test(value);
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
// src/utils/helpers/orpc.ts
|
|
816
|
+
function orpcFindChainCall(expression, method) {
|
|
817
|
+
if (expression.type === "CallExpression") {
|
|
818
|
+
if (expression.callee.type === "MemberExpression" && astStaticMemberName(expression.callee) === method) {
|
|
819
|
+
return expression;
|
|
820
|
+
}
|
|
821
|
+
if (expression.callee.type === "MemberExpression") {
|
|
822
|
+
return orpcFindChainCall(expression.callee.object, method);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (expression.type === "MemberExpression") {
|
|
826
|
+
return orpcFindChainCall(expression.object, method);
|
|
827
|
+
}
|
|
828
|
+
if (expression.type === "ChainExpression") {
|
|
829
|
+
return orpcFindChainCall(expression.expression, method);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function orpcIsNamedOrComposedArgument(argument, composers) {
|
|
833
|
+
if (argument?.type === "Identifier") {
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
return argument?.type === "CallExpression" && argument.callee.type === "Identifier" && composers.has(argument.callee.name) && argument.arguments[0]?.type === "Identifier";
|
|
837
|
+
}
|
|
838
|
+
// src/utils/helpers/react-components.ts
|
|
839
|
+
function reactComponentsExportCandidates(program) {
|
|
840
|
+
return exportsCollect(program).map((binding) => ({
|
|
841
|
+
...binding,
|
|
842
|
+
name: binding.localName ?? binding.exportedName
|
|
843
|
+
}));
|
|
844
|
+
}
|
|
845
|
+
function importedPropsWithChildren(program) {
|
|
846
|
+
return new Set(program.body.flatMap((statement) => statement.type === "ImportDeclaration" && statement.source.value === "react" ? statement.specifiers.flatMap((specifier) => specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === "PropsWithChildren" ? [specifier.local.name] : []) : []));
|
|
847
|
+
}
|
|
848
|
+
function containsNamedTypeReference(node, names) {
|
|
849
|
+
let found = false;
|
|
850
|
+
astVisit(node, [], (candidate) => {
|
|
851
|
+
if (candidate.type === "TSTypeReference" && candidate.typeName.type === "Identifier" && names.has(candidate.typeName.name)) {
|
|
852
|
+
found = true;
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
return found;
|
|
856
|
+
}
|
|
857
|
+
function returnsReactChildren(program, node, expressions) {
|
|
858
|
+
const returnedNames = new Set(expressions.flatMap((expression) => expression.type === "Identifier" ? [expression.name] : []));
|
|
859
|
+
const propsWithChildren = importedPropsWithChildren(program);
|
|
860
|
+
return node.params.some((param) => {
|
|
861
|
+
if (param.type !== "ObjectPattern") {
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
const returnsChild = param.properties.some((property) => property.type === "Property" && property.value.type === "Identifier" && returnedNames.has(property.value.name));
|
|
865
|
+
return returnsChild && containsNamedTypeReference(param.typeAnnotation, propsWithChildren);
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
function reactComponentsFunctionReturnsOutput(program, node) {
|
|
869
|
+
const expressions = astDirectReturnExpressions(node);
|
|
870
|
+
return expressions.some((expression) => astContainsJsx(expression) || expression.type === "Literal" && expression.value === null) || returnsReactChildren(program, node, expressions);
|
|
871
|
+
}
|
|
872
|
+
function functionReturnsLocalComponent(program, node) {
|
|
873
|
+
const componentNames = new Set;
|
|
874
|
+
astVisit(node.body, [node], (candidate, ancestors) => {
|
|
875
|
+
if (candidate.type === "FunctionDeclaration" && candidate !== node && candidate.id?.name && /^[A-Z]/.test(candidate.id.name) && astNearestFunction(ancestors) === node && reactComponentsFunctionReturnsOutput(program, candidate)) {
|
|
876
|
+
componentNames.add(candidate.id.name);
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
return astDirectReturnExpressions(node).some((expression) => expression.type === "Identifier" && componentNames.has(expression.name));
|
|
880
|
+
}
|
|
881
|
+
function componentFactoryNames(program) {
|
|
882
|
+
return new Set(program.body.flatMap((statement) => statement.type === "FunctionDeclaration" && statement.id?.name && functionReturnsLocalComponent(program, statement) ? [statement.id.name] : []));
|
|
883
|
+
}
|
|
884
|
+
function isComponentInitializer(program, initializer) {
|
|
885
|
+
if (!initializer) {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
if (initializer.type === "ArrowFunctionExpression" || initializer.type === "FunctionExpression") {
|
|
889
|
+
return reactComponentsFunctionReturnsOutput(program, initializer);
|
|
890
|
+
}
|
|
891
|
+
if (initializer.type !== "CallExpression") {
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
const name = astCallName(initializer);
|
|
895
|
+
if (name === "forwardRef" || name === "memo") {
|
|
896
|
+
return initializer.arguments.some((argument) => argument.type !== "SpreadElement" && isComponentInitializer(program, argument));
|
|
897
|
+
}
|
|
898
|
+
if (name === "lazy") {
|
|
899
|
+
return initializer.arguments.some((argument) => argument.type === "ArrowFunctionExpression" || argument.type === "FunctionExpression");
|
|
900
|
+
}
|
|
901
|
+
return Boolean(name?.startsWith("create") && componentFactoryNames(program).has(name));
|
|
902
|
+
}
|
|
903
|
+
function reactComponentsIsLike(program, candidate) {
|
|
904
|
+
if (!/^[A-Z]/.test(candidate.name)) {
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
const declaration = candidate.declaration ?? candidate.node;
|
|
908
|
+
if (declaration.type === "FunctionDeclaration") {
|
|
909
|
+
return reactComponentsFunctionReturnsOutput(program, declaration);
|
|
910
|
+
}
|
|
911
|
+
return declaration.type === "VariableDeclaration" && isComponentInitializer(program, candidate.initializer);
|
|
912
|
+
}
|
|
913
|
+
function reactComponentsPascalFromBasename(basename) {
|
|
914
|
+
return basename.replace(/\.(tsx?|jsx?)$/, "").split(/[-.]/).filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("");
|
|
915
|
+
}
|
|
916
|
+
// src/utils/helpers/routes.ts
|
|
917
|
+
function routesInspectFunction(context, item, bannedHooks, banIntrinsicJsx) {
|
|
918
|
+
astVisit(item.node.body, [item.node], (node) => {
|
|
919
|
+
if (banIntrinsicJsx && node.type === "JSXOpeningElement" && node.name.type === "JSXIdentifier" && /^[a-z]/.test(node.name.name)) {
|
|
920
|
+
context.report({
|
|
921
|
+
node,
|
|
922
|
+
messageId: "hostJsx",
|
|
923
|
+
data: { name: item.name }
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
if (node.type === "CallExpression") {
|
|
927
|
+
const name = astCallName(node);
|
|
928
|
+
if (name && bannedHooks.has(name)) {
|
|
929
|
+
context.report({
|
|
930
|
+
node,
|
|
931
|
+
messageId: "bannedHook",
|
|
932
|
+
data: { hook: name, name: item.name }
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
// src/utils/helpers/sanitizers.ts
|
|
939
|
+
function sanitizersCallsConfigured(node, names) {
|
|
940
|
+
let found = false;
|
|
941
|
+
astVisit(node, [], (candidate) => {
|
|
942
|
+
if (candidate.type === "CallExpression" && candidate.callee.type === "Identifier" && names.has(candidate.callee.name)) {
|
|
943
|
+
found = true;
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
return found;
|
|
947
|
+
}
|
|
948
|
+
function sanitizersConfiguredBindings(program, names) {
|
|
949
|
+
const result = new Set;
|
|
950
|
+
for (const statement of program.body) {
|
|
951
|
+
const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
952
|
+
if (declaration?.type !== "VariableDeclaration") {
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
for (const item of declaration.declarations) {
|
|
956
|
+
if (item.id.type === "Identifier" && sanitizersCallsConfigured(item.init, names)) {
|
|
957
|
+
result.add(item.id.name);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return result;
|
|
962
|
+
}
|
|
963
|
+
// src/utils/helpers/schemas.ts
|
|
964
|
+
function schemasIsRuntimeSpecifier(specifier) {
|
|
965
|
+
return specifier.type !== "ImportSpecifier" || specifier.importKind !== "type";
|
|
966
|
+
}
|
|
967
|
+
function schemasMemberRootName(node) {
|
|
968
|
+
if (node.object.type === "Identifier") {
|
|
969
|
+
return node.object.name;
|
|
970
|
+
}
|
|
971
|
+
return node.object.type === "MemberExpression" ? schemasMemberRootName(node.object) : undefined;
|
|
972
|
+
}
|
|
973
|
+
function schemasIsPackage(source, packages) {
|
|
974
|
+
return packages.has(source) || [...packages].some((pkg) => source.startsWith(`${pkg}/`));
|
|
975
|
+
}
|
|
976
|
+
function schemasIsCall(node, namespaces) {
|
|
977
|
+
return node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && namespaces.has(node.callee.object.name);
|
|
978
|
+
}
|
|
979
|
+
function schemasRuntimeImportLocals(node) {
|
|
980
|
+
return node.importKind === "type" ? [] : node.specifiers.flatMap((specifier) => schemasIsRuntimeSpecifier(specifier) ? [specifier.local.name] : []);
|
|
981
|
+
}
|
|
982
|
+
function schemasIsRuntimeImport(node, packages) {
|
|
983
|
+
return schemasIsPackage(node.source.value, packages) && (node.specifiers.length === 0 ? node.importKind !== "type" : schemasRuntimeImportLocals(node).length > 0);
|
|
984
|
+
}
|
|
985
|
+
function schemasIsRuntimeReexport(node, packages) {
|
|
986
|
+
if (!node.source || !schemasIsPackage(node.source.value, packages) || node.exportKind === "type") {
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
return node.type === "ExportAllDeclaration" || node.specifiers.length === 0 || node.specifiers.some((specifier) => specifier.exportKind !== "type");
|
|
990
|
+
}
|
|
991
|
+
function schemasIsConstruction(node, constructors) {
|
|
992
|
+
if (node.callee.type === "Identifier") {
|
|
993
|
+
return constructors.has(node.callee.name);
|
|
994
|
+
}
|
|
995
|
+
if (node.callee.type !== "MemberExpression") {
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
const root = schemasMemberRootName(node.callee);
|
|
999
|
+
return Boolean(root && constructors.has(root));
|
|
1000
|
+
}
|
|
1001
|
+
function schemasElements(node) {
|
|
1002
|
+
return node.arguments.flatMap((argument) => argument.type === "ArrayExpression" ? argument.elements.filter((element) => element !== null) : [argument]);
|
|
1003
|
+
}
|
|
1004
|
+
function schemasIsAllowedElement(node, allowScalars, namespaces, structuralMethods) {
|
|
1005
|
+
if (node.type === "Identifier") {
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
if (!allowScalars || node.type !== "CallExpression" || !schemasIsCall(node, namespaces)) {
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
const name = node.callee.type === "MemberExpression" ? astStaticMemberName(node.callee) : undefined;
|
|
1012
|
+
return Boolean(name && !structuralMethods.has(name));
|
|
1013
|
+
}
|
|
1014
|
+
function schemasTypeOperator(node, namespaces, operators) {
|
|
1015
|
+
return node.typeName.type === "TSQualifiedName" && node.typeName.left.type === "Identifier" && namespaces.has(node.typeName.left.name) && operators.has(node.typeName.right.name) ? node.typeName.right.name : undefined;
|
|
1016
|
+
}
|
|
1017
|
+
// src/utils/helpers/boundaries.ts
|
|
1018
|
+
import { resolve, dirname } from "node:path";
|
|
1019
|
+
function boundaryResolveTarget(options, file, source) {
|
|
1020
|
+
const owner = [...options.web, ...options.backend].sort((a, b) => b.length - a.length).find((root) => file.startsWith(root + "/"));
|
|
1021
|
+
const web = owner && options.web.includes(owner) ? owner : undefined;
|
|
1022
|
+
const alias = Object.entries(options.aliases).sort(([a], [b]) => b.length - a.length).find(([prefix]) => source === prefix || source.startsWith(`${prefix}/`));
|
|
1023
|
+
return alias ? resolve(alias[1], source.slice(alias[0].length).replace(/^\//, "")).replaceAll("\\", "/") : source.startsWith(".") ? resolve(dirname(file), source).replaceAll("\\", "/") : source.startsWith("@/") && web ? `${web}/${source.slice(2)}` : source.startsWith("#/") && owner ? `${owner}/${source.slice(2)}` : source.startsWith("#services/") && owner ? `${owner}/services/${source.slice(10)}` : source;
|
|
1024
|
+
}
|
|
1025
|
+
// src/rules/module-policy/service-functions.ts
|
|
1026
|
+
var serviceFunctions = defineRule3({
|
|
1027
|
+
meta: {
|
|
1028
|
+
type: "problem",
|
|
1029
|
+
schema: [
|
|
1030
|
+
{
|
|
1031
|
+
type: "object",
|
|
1032
|
+
additionalProperties: false,
|
|
1033
|
+
properties: {
|
|
1034
|
+
frontend: { type: "boolean" },
|
|
1035
|
+
allowLocalHelpers: { type: "boolean" },
|
|
1036
|
+
allowReturnedMethods: { type: "boolean" },
|
|
1037
|
+
singleExport: { type: "boolean" },
|
|
1038
|
+
message: { type: "string" }
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
],
|
|
1042
|
+
messages: {
|
|
1043
|
+
helper: "{{name}}: {{message}}",
|
|
1044
|
+
exports: "{{message}}"
|
|
1045
|
+
}
|
|
1046
|
+
},
|
|
1047
|
+
createOnce(context) {
|
|
1048
|
+
let operation;
|
|
1049
|
+
let operations = new Set;
|
|
1050
|
+
let allowLocalHelpers = false;
|
|
1051
|
+
let allowReturnedMethods = false;
|
|
1052
|
+
let message = "";
|
|
1053
|
+
function owner(node) {
|
|
1054
|
+
for (let parent = node.parent;parent; parent = parent.parent) {
|
|
1055
|
+
if (parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression" || parent.type === "ArrowFunctionExpression")
|
|
1056
|
+
return parent;
|
|
1057
|
+
if (parent.type === "Program")
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
function insideOperation(node) {
|
|
1063
|
+
for (let parent = node.parent;parent; parent = parent.parent) {
|
|
1064
|
+
if (operations.has(parent))
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
function returnedMethod(node) {
|
|
1070
|
+
if (!allowReturnedMethods || !operation || owner(node) !== operation)
|
|
1071
|
+
return false;
|
|
1072
|
+
return context.sourceCode.getDeclaredVariables(node).some((variable) => variable.references.some((reference) => {
|
|
1073
|
+
const property = reference.identifier.parent;
|
|
1074
|
+
if (property.type !== "Property" || property.value !== reference.identifier || property.parent.type !== "ObjectExpression")
|
|
1075
|
+
return false;
|
|
1076
|
+
const object = property.parent;
|
|
1077
|
+
if (object.parent.type !== "ReturnStatement" || object.parent.argument !== object)
|
|
1078
|
+
return false;
|
|
1079
|
+
return owner(object) === operation;
|
|
1080
|
+
}));
|
|
1081
|
+
}
|
|
1082
|
+
return {
|
|
1083
|
+
Program(program) {
|
|
1084
|
+
const options = optionsFirst(context, {});
|
|
1085
|
+
const candidates = declarationsFileLevelFunctionCandidates(program);
|
|
1086
|
+
const exported = exportsCollectFunctions(program);
|
|
1087
|
+
operation = exported.length === 1 ? exported[0].node : undefined;
|
|
1088
|
+
operations = new Set(options.frontend && !options.singleExport ? exported.map((entry) => entry.node) : operation ? [operation] : []);
|
|
1089
|
+
if (options.frontend && !options.singleExport) {
|
|
1090
|
+
for (const method of declarationsPublicServiceMethods(program))
|
|
1091
|
+
operations.add(method.node);
|
|
1092
|
+
}
|
|
1093
|
+
allowLocalHelpers = options.frontend || (options.allowLocalHelpers ?? false);
|
|
1094
|
+
allowReturnedMethods = options.allowReturnedMethods ?? false;
|
|
1095
|
+
message = options.message ?? (options.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 (options.singleExport) {
|
|
1097
|
+
const bindings = exportsCollect(program);
|
|
1098
|
+
if (bindings.length !== 1 || !operation || bindings[0].default || bindings[0].reExport || bindings[0].typeOnly) {
|
|
1099
|
+
context.report({
|
|
1100
|
+
node: program,
|
|
1101
|
+
messageId: "exports",
|
|
1102
|
+
data: {
|
|
1103
|
+
message: options.message ?? "Export exactly one named operation. Do not export helpers, types, constants or re-exports."
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
for (const candidate of candidates) {
|
|
1109
|
+
if ((options.frontend && !options.singleExport || candidates.length === 1) && exported.some((entry) => entry.name === candidate.name)) {
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
context.report({ node: candidate.node, messageId: "helper", data: { message, name: candidate.name } });
|
|
1113
|
+
}
|
|
1114
|
+
},
|
|
1115
|
+
FunctionDeclaration(node) {
|
|
1116
|
+
if (node.parent.type === "Program" || node.parent.type === "ExportNamedDeclaration") {
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
if (returnedMethod(node) || allowLocalHelpers && insideOperation(node)) {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
context.report({
|
|
1123
|
+
node,
|
|
1124
|
+
messageId: "helper",
|
|
1125
|
+
data: { message, name: node.id?.type === "Identifier" ? node.id.name : "<anonymous>" }
|
|
1126
|
+
});
|
|
1127
|
+
},
|
|
1128
|
+
VariableDeclarator(node) {
|
|
1129
|
+
if (node.init?.type !== "ArrowFunctionExpression" && node.init?.type !== "FunctionExpression") {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (node.parent.parent?.type === "Program" || node.parent.parent?.type === "ExportNamedDeclaration") {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
if (returnedMethod(node))
|
|
1136
|
+
return;
|
|
1137
|
+
if (allowLocalHelpers && insideOperation(node)) {
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
context.report({
|
|
1141
|
+
node,
|
|
1142
|
+
messageId: "helper",
|
|
1143
|
+
data: { message, name: node.id.type === "Identifier" ? node.id.name : "<anonymous>" }
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
// src/rules/module-policy/domain-constants.ts
|
|
1151
|
+
import { basename, dirname as dirname2 } from "node:path";
|
|
1152
|
+
import { defineRule as defineRule4 } from "@oxlint/plugins";
|
|
1153
|
+
var domainConstants = defineRule4({
|
|
1154
|
+
meta: {
|
|
1155
|
+
type: "problem",
|
|
1156
|
+
schema: [
|
|
1157
|
+
{
|
|
1158
|
+
type: "object",
|
|
1159
|
+
additionalProperties: false,
|
|
1160
|
+
properties: { includeData: { type: "boolean" }, allowServiceMethods: { type: "boolean" } }
|
|
1161
|
+
}
|
|
1162
|
+
],
|
|
1163
|
+
messages: {
|
|
1164
|
+
forbidden: "'{{name}}' belongs in '{{file}}' under this module-data policy. Use a domain-prefixed name; do not invent a factory to silence the rule."
|
|
1165
|
+
}
|
|
1166
|
+
},
|
|
1167
|
+
createOnce(context) {
|
|
1168
|
+
let serviceObjects = new Set;
|
|
1169
|
+
function data(expression) {
|
|
1170
|
+
const value = trivialUnwrap(expression);
|
|
1171
|
+
if (value.type === "Literal" || value.type === "ArrayExpression" || value.type === "ObjectExpression")
|
|
1172
|
+
return true;
|
|
1173
|
+
if (value.type === "TemplateLiteral")
|
|
1174
|
+
return value.expressions.every(data);
|
|
1175
|
+
if (value.type === "UnaryExpression")
|
|
1176
|
+
return data(value.argument);
|
|
1177
|
+
if (value.type === "BinaryExpression" || value.type === "LogicalExpression")
|
|
1178
|
+
return value.left.type !== "PrivateIdentifier" && data(value.left) && data(value.right);
|
|
1179
|
+
return value.type === "NewExpression" && value.callee.type === "Identifier" && ["Map", "Set"].includes(value.callee.name) && value.arguments[0]?.type === "ArrayExpression" && value.arguments[0].elements.length > 0;
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
Program(program) {
|
|
1183
|
+
serviceObjects = new Set(optionsFirst(context, {}).allowServiceMethods ? declarationsPublicServiceMethods(program).map((method) => method.binding) : []);
|
|
1184
|
+
},
|
|
1185
|
+
VariableDeclarator(node) {
|
|
1186
|
+
if (serviceObjects.has(node))
|
|
1187
|
+
return;
|
|
1188
|
+
const declaration = node.parent;
|
|
1189
|
+
if (declaration.type !== "VariableDeclaration" || declaration.kind !== "const" || declaration.parent.type !== "Program" && declaration.parent.type !== "ExportNamedDeclaration")
|
|
1190
|
+
return;
|
|
1191
|
+
const options = optionsFirst(context, {});
|
|
1192
|
+
if (node.id.type === "Identifier" && /^[A-Z][A-Z0-9_]+$/.test(node.id.name) || options.includeData && node.init && data(node.init)) {
|
|
1193
|
+
const file = context.filename.replaceAll("\\", "/");
|
|
1194
|
+
const owner = file.includes("/services/") && dirname2(file).split("/").at(-1) !== "services" ? basename(dirname2(file)) : basename(file).split(".")[0].replace(/-(?:action|query)$/, "");
|
|
1195
|
+
context.report({
|
|
1196
|
+
node,
|
|
1197
|
+
messageId: "forbidden",
|
|
1198
|
+
data: {
|
|
1199
|
+
name: node.id.type === "Identifier" ? node.id.name : "<destructured binding>",
|
|
1200
|
+
file: `${owner}.constants.ts`
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
// src/rules/module-policy/test-modifiers.ts
|
|
1210
|
+
import { defineRule as defineRule5 } from "@oxlint/plugins";
|
|
1211
|
+
var testModifiers = defineRule5({
|
|
1212
|
+
meta: {
|
|
1213
|
+
type: "problem",
|
|
1214
|
+
schema: [],
|
|
1215
|
+
messages: {
|
|
1216
|
+
modifier: "Focused, skipped and placeholder tests are not allowed."
|
|
1217
|
+
}
|
|
1218
|
+
},
|
|
1219
|
+
createOnce(context) {
|
|
1220
|
+
let names = new Set;
|
|
1221
|
+
return {
|
|
1222
|
+
before() {
|
|
1223
|
+
names = new Set(["test", "it", "describe", "suite"]);
|
|
1224
|
+
},
|
|
1225
|
+
ImportDeclaration(node) {
|
|
1226
|
+
if (!["vitest", "bun:test", "@playwright/test", "@jest/globals"].includes(node.source.value)) {
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
for (const specifier of node.specifiers) {
|
|
1230
|
+
if (specifier.type !== "ImportSpecifier") {
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
const imported = specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
|
|
1234
|
+
if (["test", "it", "describe", "suite"].includes(imported)) {
|
|
1235
|
+
names.add(specifier.local.name);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
},
|
|
1239
|
+
MemberExpression(node) {
|
|
1240
|
+
const name = astDottedName(node);
|
|
1241
|
+
if (!name) {
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
const parts = name.split(".");
|
|
1245
|
+
if (names.has(parts[0]) && ["only", "skip", "todo"].includes(parts.at(-1) ?? "")) {
|
|
1246
|
+
context.report({ node, messageId: "modifier" });
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
// src/rules/module-policy/reasoned-directives.ts
|
|
1254
|
+
import { defineRule as defineRule6 } from "@oxlint/plugins";
|
|
1255
|
+
var directives = defineRule6({
|
|
1256
|
+
meta: {
|
|
1257
|
+
type: "problem",
|
|
1258
|
+
schema: [],
|
|
1259
|
+
messages: {
|
|
1260
|
+
directive: "Lint disables must name specific rules and explain why after --."
|
|
1261
|
+
}
|
|
1262
|
+
},
|
|
1263
|
+
createOnce(context) {
|
|
1264
|
+
return {
|
|
1265
|
+
Program() {
|
|
1266
|
+
for (const comment of context.sourceCode.getAllComments()) {
|
|
1267
|
+
const match = /^\s*(?:oxlint|eslint)-disable(?:-next-line|-line)?\b(.*)$/s.exec(comment.value);
|
|
1268
|
+
if (!match) {
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
const [rules, ...reason] = match[1].split("--");
|
|
1272
|
+
if (rules.trim() && reason.join("--").trim()) {
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
context.report({ loc: comment.loc, messageId: "directive" });
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
// src/rules/module-policy/tanstack-runtime.ts
|
|
1283
|
+
import { builtinModules } from "node:module";
|
|
1284
|
+
import { defineRule as defineRule7 } from "@oxlint/plugins";
|
|
1285
|
+
var tanstackRuntime = defineRule7({
|
|
1286
|
+
meta: {
|
|
1287
|
+
type: "problem",
|
|
1288
|
+
schema: [
|
|
1289
|
+
{
|
|
1290
|
+
type: "object",
|
|
1291
|
+
additionalProperties: false,
|
|
1292
|
+
properties: {
|
|
1293
|
+
web: { type: "array", items: { type: "string" } },
|
|
1294
|
+
backend: { type: "array", items: { type: "string" } },
|
|
1295
|
+
aliases: { type: "object", additionalProperties: { type: "string" } },
|
|
1296
|
+
backendPackages: { type: "array", items: { type: "string" } },
|
|
1297
|
+
rpcClients: { type: "array", items: { type: "string" } },
|
|
1298
|
+
serverImports: { type: "array", items: { type: "string" } },
|
|
1299
|
+
clientImports: { type: "array", items: { type: "string" } },
|
|
1300
|
+
allowComputedImportsIn: { type: "array", items: { type: "string" } }
|
|
1301
|
+
},
|
|
1302
|
+
required: ["web", "backend", "aliases", "backendPackages", "rpcClients"]
|
|
1303
|
+
}
|
|
1304
|
+
],
|
|
1305
|
+
messages: {
|
|
1306
|
+
runtime: "{{source}} is not valid here: {{reason}}.",
|
|
1307
|
+
computed: "Use a literal dynamic import so runtime ownership can be checked, or declare this exact file in allowComputedImportsIn after reviewing its targets."
|
|
1308
|
+
}
|
|
1309
|
+
},
|
|
1310
|
+
createOnce(context) {
|
|
1311
|
+
let options;
|
|
1312
|
+
let file = "";
|
|
1313
|
+
let serverFile = false;
|
|
1314
|
+
let rscFile = false;
|
|
1315
|
+
let shared = false;
|
|
1316
|
+
const runtimeIdentifiers = new Set;
|
|
1317
|
+
const imports = new Map;
|
|
1318
|
+
const boundaries = [];
|
|
1319
|
+
function imported(node) {
|
|
1320
|
+
const entry = imports.get(node.name);
|
|
1321
|
+
if (!entry)
|
|
1322
|
+
return;
|
|
1323
|
+
let scope = context.sourceCode.getScope(node);
|
|
1324
|
+
while (scope) {
|
|
1325
|
+
const variable = scope.set.get(node.name);
|
|
1326
|
+
if (variable)
|
|
1327
|
+
return variable.defs.some((definition) => definition.name.range[0] === entry.node.start) ? entry.name : undefined;
|
|
1328
|
+
scope = scope.upper;
|
|
1329
|
+
}
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
function factory(node) {
|
|
1333
|
+
if (node.type === "Identifier")
|
|
1334
|
+
return imported(node);
|
|
1335
|
+
if (node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier") {
|
|
1336
|
+
if (node.object.type === "Identifier" && imported(node.object) === "*")
|
|
1337
|
+
return node.property.name;
|
|
1338
|
+
if (["handler", "server", "client", "middleware", "validator", "inputValidator"].includes(node.property.name))
|
|
1339
|
+
return factory(node.object);
|
|
1340
|
+
}
|
|
1341
|
+
if (node.type === "CallExpression")
|
|
1342
|
+
return factory(node.callee);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
function boundary(position) {
|
|
1346
|
+
return boundaries.filter((entry) => position >= entry.start && position < entry.end).sort((a, b) => b.start - a.start)[0];
|
|
1347
|
+
}
|
|
1348
|
+
function serverPosition(position, staticImport = false) {
|
|
1349
|
+
const entry = boundary(position);
|
|
1350
|
+
if (entry)
|
|
1351
|
+
return entry.side === "server" && (!staticImport || entry.kind !== "createIsomorphicFn" || options.rpcClients.includes(file));
|
|
1352
|
+
return serverFile;
|
|
1353
|
+
}
|
|
1354
|
+
function report(node, source, reason) {
|
|
1355
|
+
context.report({ node, messageId: "runtime", data: { source, reason } });
|
|
1356
|
+
}
|
|
1357
|
+
function check(node, source, typeOnly, reExport = false, renderer) {
|
|
1358
|
+
if (typeOnly)
|
|
1359
|
+
return;
|
|
1360
|
+
const target = boundaryResolveTarget(options, file, source);
|
|
1361
|
+
const matches = (values) => values?.some((value) => source === value || source.startsWith(value + "/"));
|
|
1362
|
+
const backend = options.backend.some((path) => target.startsWith(path + "/")) || options.backendPackages.some((name) => source === name || source.startsWith(name + "/") && /\/(?:server|rpc)(?:\/|$)/.test(source.slice(name.length)));
|
|
1363
|
+
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(options.serverImports);
|
|
1364
|
+
const client = /(?:\/hooks\/|\.client-only(?:\.|$))/.test(target) || /^(?:react-dom\/client|@tanstack\/(?:react|solid|vue)-start\/client-only)(?:\/|$)/.test(source) || matches(options.clientImports);
|
|
1365
|
+
if (client && (serverFile || rscFile || shared) && boundary(node.start)?.side !== "client") {
|
|
1366
|
+
const references = node.type === "ImportDeclaration" ? context.sourceCode.getDeclaredVariables(node).flatMap((variable) => variable.references.filter((reference) => runtimeIdentifiers.has(reference.identifier.range[0]))) : [];
|
|
1367
|
+
if (!shared && references.length && references.every((reference) => boundary(reference.identifier.range[0])?.side === "client"))
|
|
1368
|
+
return;
|
|
1369
|
+
report(node, source, "browser-only dependencies do not belong in server/RSC or shared modules");
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
if (!server)
|
|
1373
|
+
return;
|
|
1374
|
+
if (shared) {
|
|
1375
|
+
report(node, source, "shared utilities/constants/lib cannot import runtime server dependencies");
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
if (reExport && !serverFile) {
|
|
1379
|
+
report(node, source, "server dependencies must not be re-exported as a public runtime API");
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
if (node.type === "ImportDeclaration") {
|
|
1383
|
+
if (serverFile)
|
|
1384
|
+
return;
|
|
1385
|
+
const references = context.sourceCode.getDeclaredVariables(node).filter((variable) => !renderer || variable.name === renderer.local.name).flatMap((variable) => variable.references.filter((reference) => runtimeIdentifiers.has(reference.identifier.range[0])));
|
|
1386
|
+
if (references.length && references.every((reference) => serverPosition(reference.identifier.range[0], true)))
|
|
1387
|
+
return;
|
|
1388
|
+
} else if (serverPosition(node.start))
|
|
1389
|
+
return;
|
|
1390
|
+
report(node, source, "use an inline imported TanStack server boundary; static isomorphic server imports belong only to the configured RPC owner");
|
|
1391
|
+
}
|
|
1392
|
+
function global(node) {
|
|
1393
|
+
const serverNames = ["process", "Bun", "Deno", "Buffer", "__dirname", "__filename", "require"];
|
|
1394
|
+
const browserNames = [
|
|
1395
|
+
"window",
|
|
1396
|
+
"document",
|
|
1397
|
+
"navigator",
|
|
1398
|
+
"localStorage",
|
|
1399
|
+
"sessionStorage",
|
|
1400
|
+
"location",
|
|
1401
|
+
"history",
|
|
1402
|
+
"screen",
|
|
1403
|
+
"HTMLElement",
|
|
1404
|
+
"Element",
|
|
1405
|
+
"HTMLInputElement",
|
|
1406
|
+
"ResizeObserver",
|
|
1407
|
+
"MutationObserver",
|
|
1408
|
+
"IntersectionObserver",
|
|
1409
|
+
"FileReader",
|
|
1410
|
+
"matchMedia",
|
|
1411
|
+
"getComputedStyle",
|
|
1412
|
+
"requestAnimationFrame",
|
|
1413
|
+
"cancelAnimationFrame",
|
|
1414
|
+
"alert",
|
|
1415
|
+
"confirm",
|
|
1416
|
+
"prompt"
|
|
1417
|
+
];
|
|
1418
|
+
const member = node.parent.type === "MemberExpression" && node.parent.object === node && node.name === "globalThis" ? node.parent : undefined;
|
|
1419
|
+
const name = member && !member.computed && member.property.type === "Identifier" ? member.property.name : member?.computed && member.property.type === "Literal" && typeof member.property.value === "string" ? member.property.value : node.name;
|
|
1420
|
+
if (!serverNames.includes(name) && !browserNames.includes(name))
|
|
1421
|
+
return;
|
|
1422
|
+
let scope = context.sourceCode.getScope(node);
|
|
1423
|
+
while (scope) {
|
|
1424
|
+
const variable = scope.set.get(node.name);
|
|
1425
|
+
if (variable?.defs.length)
|
|
1426
|
+
return;
|
|
1427
|
+
scope = scope.upper;
|
|
1428
|
+
}
|
|
1429
|
+
const references = context.sourceCode.getScope(node).references;
|
|
1430
|
+
if (!references.some((reference) => reference.identifier.range[0] === node.start && runtimeIdentifiers.has(reference.identifier.range[0])))
|
|
1431
|
+
return;
|
|
1432
|
+
if (serverNames.includes(name) && (shared || !serverPosition(node.start)))
|
|
1433
|
+
report(node, name, "server globals require a server execution boundary");
|
|
1434
|
+
if (browserNames.includes(name) && (serverFile || rscFile) && boundary(node.start)?.side !== "client")
|
|
1435
|
+
report(node, name, "browser globals are unavailable during server execution");
|
|
1436
|
+
}
|
|
1437
|
+
return {
|
|
1438
|
+
Program(program) {
|
|
1439
|
+
options = optionsFirst(context);
|
|
1440
|
+
file = context.filename.replaceAll("\\", "/");
|
|
1441
|
+
serverFile = /\.server\.[cm]?[jt]sx?$/.test(file);
|
|
1442
|
+
rscFile = /\.rsc\.[cm]?[jt]sx?$/.test(file);
|
|
1443
|
+
shared = /\.(?:utils|constants)\.[cm]?[jt]sx?$/.test(file) || options.web.some((path) => file.startsWith(path + "/lib/"));
|
|
1444
|
+
runtimeIdentifiers.clear();
|
|
1445
|
+
imports.clear();
|
|
1446
|
+
boundaries.length = 0;
|
|
1447
|
+
for (const statement of program.body) {
|
|
1448
|
+
if (statement.type !== "ImportDeclaration" || statement.importKind === "type")
|
|
1449
|
+
continue;
|
|
1450
|
+
if (/^(?:react|@tanstack\/react-(?:query|router))$/.test(statement.source.value)) {
|
|
1451
|
+
for (const specifier of statement.specifiers) {
|
|
1452
|
+
if (specifier.type === "ImportNamespaceSpecifier" || specifier.type === "ImportDefaultSpecifier")
|
|
1453
|
+
imports.set(specifier.local.name, { node: specifier.local, name: "client-hooks" });
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
if (["@tanstack/react-start/rsc", "@tanstack/react-start-rsc"].includes(statement.source.value)) {
|
|
1457
|
+
for (const specifier of statement.specifiers) {
|
|
1458
|
+
if (specifier.type === "ImportNamespaceSpecifier")
|
|
1459
|
+
imports.set(specifier.local.name, { node: specifier.local, name: "rsc-renderers" });
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (statement.source.value !== "@tanstack/react-start")
|
|
1463
|
+
continue;
|
|
1464
|
+
for (const specifier of statement.specifiers) {
|
|
1465
|
+
if (specifier.type === "ImportSpecifier" && specifier.importKind !== "type")
|
|
1466
|
+
imports.set(specifier.local.name, {
|
|
1467
|
+
node: specifier.local,
|
|
1468
|
+
name: specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value
|
|
1469
|
+
});
|
|
1470
|
+
if (specifier.type === "ImportNamespaceSpecifier")
|
|
1471
|
+
imports.set(specifier.local.name, { node: specifier.local, name: "*" });
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
astVisit(program, [], (node, ancestors) => {
|
|
1475
|
+
if (node.type === "Identifier" && !ancestors.some((ancestor) => ancestor.type.startsWith("TS") && ![
|
|
1476
|
+
"TSAsExpression",
|
|
1477
|
+
"TSSatisfiesExpression",
|
|
1478
|
+
"TSNonNullExpression",
|
|
1479
|
+
"TSInstantiationExpression"
|
|
1480
|
+
].includes(ancestor.type)))
|
|
1481
|
+
runtimeIdentifiers.add(node.start);
|
|
1482
|
+
if (node.type !== "CallExpression")
|
|
1483
|
+
return;
|
|
1484
|
+
const kind = factory(node.callee);
|
|
1485
|
+
const method = node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.property.type === "Identifier" ? node.callee.property.name : undefined;
|
|
1486
|
+
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;
|
|
1487
|
+
const callback = node.arguments[0];
|
|
1488
|
+
if (kind && side && callback && (callback.type === "FunctionExpression" || callback.type === "ArrowFunctionExpression"))
|
|
1489
|
+
boundaries.push({ start: callback.start, end: callback.end, side, kind });
|
|
1490
|
+
});
|
|
1491
|
+
},
|
|
1492
|
+
ImportDeclaration(node) {
|
|
1493
|
+
const typeOnly = node.importKind === "type" || node.specifiers.length > 0 && node.specifiers.every((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type");
|
|
1494
|
+
check(node, node.source.value, typeOnly);
|
|
1495
|
+
if (!typeOnly && ["@tanstack/react-start/rsc", "@tanstack/react-start-rsc"].includes(node.source.value)) {
|
|
1496
|
+
for (const specifier of node.specifiers) {
|
|
1497
|
+
if (specifier.type === "ImportSpecifier" && specifier.importKind !== "type" && specifier.imported.type === "Identifier" && ["renderServerComponent", "renderToReadableStream"].includes(specifier.imported.name))
|
|
1498
|
+
check(node, node.source.value, false, false, specifier);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (!typeOnly && (serverFile || rscFile) && /^(?:react|@tanstack\/react-(?:query|router))$/.test(node.source.value)) {
|
|
1502
|
+
for (const specifier of node.specifiers) {
|
|
1503
|
+
if (specifier.type !== "ImportSpecifier" || specifier.importKind === "type" || specifier.imported.type !== "Identifier")
|
|
1504
|
+
continue;
|
|
1505
|
+
if (/^use[A-Z]/.test(specifier.imported.name) && specifier.imported.name !== "useId") {
|
|
1506
|
+
const references = context.sourceCode.getDeclaredVariables(node).filter((variable) => variable.name === specifier.local.name).flatMap((variable) => variable.references.filter((reference) => runtimeIdentifiers.has(reference.identifier.range[0])));
|
|
1507
|
+
if (!references.length || !references.every((reference) => boundary(reference.identifier.range[0])?.side === "client"))
|
|
1508
|
+
report(specifier, specifier.imported.name, "client hooks do not belong in server/RSC services");
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
},
|
|
1513
|
+
ExportNamedDeclaration(node) {
|
|
1514
|
+
if (node.source)
|
|
1515
|
+
check(node, node.source.value, node.exportKind === "type" || node.specifiers.length > 0 && node.specifiers.every((specifier) => specifier.exportKind === "type"), true);
|
|
1516
|
+
},
|
|
1517
|
+
ExportAllDeclaration(node) {
|
|
1518
|
+
check(node, node.source.value, node.exportKind === "type", true);
|
|
1519
|
+
},
|
|
1520
|
+
ImportExpression(node) {
|
|
1521
|
+
if (node.source.type === "Literal" && typeof node.source.value === "string")
|
|
1522
|
+
check(node, node.source.value, false);
|
|
1523
|
+
else if (!options.allowComputedImportsIn?.includes(file))
|
|
1524
|
+
context.report({ node, messageId: "computed" });
|
|
1525
|
+
},
|
|
1526
|
+
MemberExpression(node) {
|
|
1527
|
+
const name = !node.computed && node.property.type === "Identifier" ? node.property.name : node.computed && node.property.type === "Literal" && typeof node.property.value === "string" ? node.property.value : undefined;
|
|
1528
|
+
if (name && ["renderServerComponent", "renderToReadableStream"].includes(name) && node.object.type === "Identifier" && imported(node.object) === "rsc-renderers" && (shared || !serverPosition(node.start, true)))
|
|
1529
|
+
report(node, name, "server renderers require a server execution boundary");
|
|
1530
|
+
if ((serverFile || rscFile) && name && /^use[A-Z]/.test(name) && name !== "useId" && node.object.type === "Identifier" && imported(node.object) === "client-hooks" && boundary(node.start)?.side !== "client")
|
|
1531
|
+
report(node, name, "client hooks do not belong in server/RSC services");
|
|
1532
|
+
},
|
|
1533
|
+
Identifier: global
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
// src/rules/module-policy/import-boundaries.ts
|
|
1539
|
+
import { defineRule as defineRule8 } from "@oxlint/plugins";
|
|
1540
|
+
import { resolve as resolve2, basename as basename2 } from "node:path";
|
|
1541
|
+
var importBoundaries = defineRule8({
|
|
1542
|
+
meta: {
|
|
1543
|
+
type: "problem",
|
|
1544
|
+
schema: [
|
|
1545
|
+
{
|
|
1546
|
+
type: "object",
|
|
1547
|
+
additionalProperties: false,
|
|
1548
|
+
properties: {
|
|
1549
|
+
web: { type: "array", items: { type: "string" } },
|
|
1550
|
+
backend: { type: "array", items: { type: "string" } },
|
|
1551
|
+
packages: { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }] },
|
|
1552
|
+
fileRoles: { type: "array", items: { type: "string" } },
|
|
1553
|
+
portableLib: { type: "boolean" },
|
|
1554
|
+
backendPackages: { type: "array", items: { type: "string" } },
|
|
1555
|
+
publicEntrypoints: { type: "array", items: { type: "string" } },
|
|
1556
|
+
appPackages: { type: "array", items: { type: "string" } },
|
|
1557
|
+
aliases: { type: "object", additionalProperties: { type: "string" } }
|
|
1558
|
+
},
|
|
1559
|
+
required: ["web", "backend", "packages", "backendPackages", "publicEntrypoints", "appPackages", "aliases"]
|
|
1560
|
+
}
|
|
1561
|
+
],
|
|
1562
|
+
messages: {
|
|
1563
|
+
boundary: "{{source}} violates the module boundary: {{reason}}."
|
|
1564
|
+
}
|
|
1565
|
+
},
|
|
1566
|
+
createOnce(context) {
|
|
1567
|
+
function check(node, source, typeOnly, reExport = false) {
|
|
1568
|
+
const options = optionsFirst(context);
|
|
1569
|
+
const file = context.filename.replaceAll("\\", "/");
|
|
1570
|
+
const contains = (directory, candidate) => candidate === directory || candidate.startsWith(`${directory}/`);
|
|
1571
|
+
const roots = [...options.web, ...options.backend].sort((a, b) => b.length - a.length);
|
|
1572
|
+
const owner = roots.find((path) => contains(path, file));
|
|
1573
|
+
const web = owner && options.web.includes(owner) ? owner : undefined;
|
|
1574
|
+
const target = boundaryResolveTarget(options, file, source);
|
|
1575
|
+
const backendImport = options.backend.some((path) => contains(path, target)) || options.backendPackages.some((name) => source === name || source.startsWith(`${name}/`));
|
|
1576
|
+
let reason;
|
|
1577
|
+
if (web && backendImport && !options.publicEntrypoints.includes(source)) {
|
|
1578
|
+
reason = "frontend code uses only public backend entry points";
|
|
1579
|
+
}
|
|
1580
|
+
if ((Array.isArray(options.packages) ? options.packages : [options.packages]).some((path) => path && contains(path, file)) && (backendImport || options.web.some((path) => contains(path, target)) || options.appPackages.some((name) => source === name || source.startsWith(`${name}/`)))) {
|
|
1581
|
+
reason = "shared packages do not import app internals";
|
|
1582
|
+
}
|
|
1583
|
+
if (file.includes("/services/") && /\/services\/service(?:\.ts)?$/.test(target) && !typeOnly) {
|
|
1584
|
+
reason = "service dependencies are injected rather than read from the registry";
|
|
1585
|
+
}
|
|
1586
|
+
if (/\.(?:types|constants)\.ts$/.test(file) && !typeOnly && /\/(?:core\/|services\/.*(?:\.(?:service|utils)|-(?:action|query)\.|\/(?:actions|queries)\/))/.test(target)) {
|
|
1587
|
+
reason = "domain types/constants use type-only imports from implementations";
|
|
1588
|
+
}
|
|
1589
|
+
if (file.endsWith(".client.ts") && !typeOnly && /^(react|react-dom)(\/|$)/.test(source)) {
|
|
1590
|
+
reason = "client services are React-free; use hooks for React behavior";
|
|
1591
|
+
}
|
|
1592
|
+
if (options.fileRoles?.length) {
|
|
1593
|
+
const targetOwner = roots.find((path) => contains(path, target));
|
|
1594
|
+
const domain = (path, appRoot) => {
|
|
1595
|
+
const relative = path.slice(appRoot.length + 1);
|
|
1596
|
+
if (relative.startsWith("types/") && relative.endsWith(".types.ts"))
|
|
1597
|
+
return relative.slice("types/".length).split(".")[0];
|
|
1598
|
+
if (!relative.startsWith("services/"))
|
|
1599
|
+
return;
|
|
1600
|
+
const parts = relative.slice("services/".length).split("/");
|
|
1601
|
+
return parts.length > 1 ? parts[0] : parts[0].split(".")[0].replace(/-(?:action|query|driver)$/, "");
|
|
1602
|
+
};
|
|
1603
|
+
const sameDomain = owner && targetOwner && owner === targetOwner && domain(file, owner) && domain(file, owner) === domain(target, targetOwner);
|
|
1604
|
+
const targetStem = target.replace(/\.[cm]?[jt]sx?$/, "");
|
|
1605
|
+
const concept = options.fileRoles.some((role) => targetStem.endsWith(`.${role}`));
|
|
1606
|
+
const publicSurface = /\.(?:service|client|server|rsc|utils)$/.test(targetStem) && !/\/(?:[^/]+)-(?:action|query)\./.test(targetStem);
|
|
1607
|
+
const privateTarget = concept || targetOwner && domain(target, targetOwner) && !publicSurface && !/\.(?:types|constants)$/.test(targetStem);
|
|
1608
|
+
if (privateTarget && !sameDomain) {
|
|
1609
|
+
reason = "private domain files stay in the same domain and application; use its service API or shared utilities";
|
|
1610
|
+
}
|
|
1611
|
+
if (privateTarget && reExport && /\.(?:service|client|server|rsc|utils)\.ts$/.test(file) && !/\/(?:[^/]+)-(?:action|query)\./.test(file)) {
|
|
1612
|
+
reason = "public service and utility surfaces must not re-export private domain internals";
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
if (options.portableLib && web && contains(`${web}/lib`, file)) {
|
|
1616
|
+
const portableTarget = target.startsWith("/") ? resolve2(target).replaceAll("\\", "/") : target;
|
|
1617
|
+
const appService = roots.some((path) => contains(`${path}/services`, portableTarget));
|
|
1618
|
+
const backendTarget = options.backend.some((path) => contains(path, portableTarget));
|
|
1619
|
+
const appPackage = options.appPackages.some((name) => source === name || source.startsWith(`${name}/`));
|
|
1620
|
+
const appType = roots.some((path) => contains(`${path}/types`, portableTarget));
|
|
1621
|
+
const stem = basename2(file).replace(/\.[cm]?[jt]sx?$/, "").replace(/\.utils$/, "");
|
|
1622
|
+
const ownType = typeOnly && !reExport && portableTarget.replace(/\.[cm]?[jt]sx?$/, "") === `${web}/types/${stem}.types`;
|
|
1623
|
+
if (backendImport || backendTarget || appService || appPackage) {
|
|
1624
|
+
reason = "portable frontend lib does not import application services or application APIs";
|
|
1625
|
+
} else if (appType && !ownType) {
|
|
1626
|
+
reason = `portable frontend lib may only type-import its own contract '${web}/types/${stem}.types.ts'; app type re-exports are forbidden`;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
if (reason) {
|
|
1630
|
+
context.report({
|
|
1631
|
+
node,
|
|
1632
|
+
messageId: "boundary",
|
|
1633
|
+
data: { source, reason }
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
return {
|
|
1638
|
+
Program(program) {
|
|
1639
|
+
const exported = new Set(exportsCollect(program).filter((binding) => !binding.reExport).map((binding) => binding.localName));
|
|
1640
|
+
for (const statement of program.body) {
|
|
1641
|
+
if (statement.type !== "ImportDeclaration")
|
|
1642
|
+
continue;
|
|
1643
|
+
for (const specifier of statement.specifiers) {
|
|
1644
|
+
if (exported.has(specifier.local.name))
|
|
1645
|
+
check(specifier, statement.source.value, statement.importKind === "type" || specifier.type === "ImportSpecifier" && specifier.importKind === "type", true);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
},
|
|
1649
|
+
ImportDeclaration(node) {
|
|
1650
|
+
check(node, node.source.value, node.importKind === "type" || node.specifiers.length > 0 && node.specifiers.every((item) => item.type === "ImportSpecifier" && item.importKind === "type"));
|
|
1651
|
+
},
|
|
1652
|
+
ExportNamedDeclaration(node) {
|
|
1653
|
+
if (node.source) {
|
|
1654
|
+
check(node, node.source.value, node.exportKind === "type", true);
|
|
1655
|
+
}
|
|
1656
|
+
},
|
|
1657
|
+
ExportAllDeclaration(node) {
|
|
1658
|
+
check(node, node.source.value, node.exportKind === "type", true);
|
|
1659
|
+
},
|
|
1660
|
+
ImportExpression(node) {
|
|
1661
|
+
if (node.source.type === "Literal" && typeof node.source.value === "string") {
|
|
1662
|
+
check(node, node.source.value, false);
|
|
1663
|
+
}
|
|
1664
|
+
},
|
|
1665
|
+
TSImportType(node) {
|
|
1666
|
+
const options = optionsFirst(context);
|
|
1667
|
+
const file = context.filename.replaceAll("\\", "/");
|
|
1668
|
+
if (options.portableLib && options.web.some((path) => file.startsWith(`${path}/lib/`))) {
|
|
1669
|
+
check(node, node.source.value, true);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
// src/rules/module-policy/memoization.ts
|
|
1677
|
+
import { defineRule as defineRule9 } from "@oxlint/plugins";
|
|
1678
|
+
var memoization = defineRule9({
|
|
1679
|
+
meta: {
|
|
1680
|
+
type: "problem",
|
|
1681
|
+
schema: [],
|
|
1682
|
+
messages: {
|
|
1683
|
+
memo: "Use the React Compiler rather than manual memoization."
|
|
1684
|
+
}
|
|
1685
|
+
},
|
|
1686
|
+
createOnce(context) {
|
|
1687
|
+
let namespaces = new Set;
|
|
1688
|
+
return {
|
|
1689
|
+
before() {
|
|
1690
|
+
namespaces = new Set(["React"]);
|
|
1691
|
+
},
|
|
1692
|
+
ImportDeclaration(node) {
|
|
1693
|
+
if (node.source.value !== "react") {
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
for (const item of node.specifiers) {
|
|
1697
|
+
if (item.type === "ImportSpecifier") {
|
|
1698
|
+
const name = item.imported.type === "Identifier" ? item.imported.name : item.imported.value;
|
|
1699
|
+
if (["memo", "useMemo", "useCallback"].includes(name)) {
|
|
1700
|
+
context.report({ node: item, messageId: "memo" });
|
|
1701
|
+
}
|
|
1702
|
+
} else {
|
|
1703
|
+
namespaces.add(item.local.name);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
},
|
|
1707
|
+
MemberExpression(node) {
|
|
1708
|
+
const name = astDottedName(node)?.split(".");
|
|
1709
|
+
if (name?.length === 2 && namespaces.has(name[0]) && ["memo", "useMemo", "useCallback"].includes(name[1])) {
|
|
1710
|
+
context.report({ node, messageId: "memo" });
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
// src/rules/module-policy/index.ts
|
|
1718
|
+
var module_policy_default = eslintCompatPlugin({
|
|
1719
|
+
meta: { name: "modules" },
|
|
1720
|
+
rules: {
|
|
1721
|
+
"dynamic-classes": syntaxRule(':matches(JSXAttribute[name.name="className"] TemplateLiteral[expressions.length>0], JSXAttribute[name.name="className"] BinaryExpression[operator="+"])', "Write complete class names instead of constructing them dynamically."),
|
|
1722
|
+
"backend-switch": syntaxRule("SwitchStatement", "Use ts-pattern in backend application code."),
|
|
1723
|
+
"concurrent-db": syntaxRule('CallExpression[callee.object.name="test"][callee.property.name="concurrent"] ObjectPattern > Property[key.name="db"]', "Obtain an isolated database fixture for concurrent tests."),
|
|
1724
|
+
"domain-constants": domainConstants,
|
|
1725
|
+
"double-negation": syntaxRule('UnaryExpression[operator="!"] > UnaryExpression[operator="!"]', "Use Boolean(value) or an explicit comparison."),
|
|
1726
|
+
"empty-effect": syntaxRule(':matches(CallExpression[callee.name=/^use(Layout)?Effect$/], CallExpression[callee.object.name="React"][callee.property.name=/^use(Layout)?Effect$/]) > :matches(ArrowFunctionExpression, FunctionExpression) > BlockStatement[body.length=0]', "Remove the empty effect."),
|
|
1727
|
+
"import-boundaries": importBoundaries,
|
|
1728
|
+
"tanstack-runtime": tanstackRuntime,
|
|
1729
|
+
"local-type-alias": syntaxRule("TSTypeAliasDeclaration", "Local component/hook contracts use interfaces named for their owner. Derived or reusable aliases belong in domain types; do not substitute an empty interface."),
|
|
1730
|
+
"no-unknown": syntaxRule("TSUnknownKeyword", "Use an owned domain type rather than unknown."),
|
|
1731
|
+
memoization,
|
|
1732
|
+
"pascal-interface": syntaxRule("TSInterfaceDeclaration:not([id.name=/^[A-Z][a-zA-Z0-9]*$/])", "Use PascalCase for interfaces."),
|
|
1733
|
+
"reasoned-directives": directives,
|
|
1734
|
+
"rpc-database": syntaxRule("CallExpression[callee.property.name=/^(selectFrom|insertInto|updateTable|deleteFrom|mergeInto|executeQuery)$/]", "RPC handlers delegate database work to services."),
|
|
1735
|
+
"service-functions": serviceFunctions,
|
|
1736
|
+
"service-types": serviceTypes,
|
|
1737
|
+
"shape-suffix": syntaxRule(":matches(VariableDeclarator, FunctionDeclaration, ClassDeclaration, TSInterfaceDeclaration, TSTypeAliasDeclaration)[id.name=/Shape$/]", "Name this declaration for its domain role rather than the Shape suffix."),
|
|
1738
|
+
"test-modifiers": testModifiers
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
export {
|
|
1742
|
+
module_policy_default as default
|
|
1743
|
+
};
|