@skapxd/eslint-opinionated 0.1.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/README.md +731 -0
- package/dist/astro/index.d.mts +33 -0
- package/dist/astro/index.d.ts +33 -0
- package/dist/astro/index.js +72 -0
- package/dist/astro/index.js.map +1 -0
- package/dist/astro/index.mjs +8 -0
- package/dist/astro/index.mjs.map +1 -0
- package/dist/chunk-3FB4H7N6.mjs +31 -0
- package/dist/chunk-3FB4H7N6.mjs.map +1 -0
- package/dist/chunk-BAHAXSWA.mjs +62 -0
- package/dist/chunk-BAHAXSWA.mjs.map +1 -0
- package/dist/chunk-CQKEQ32W.mjs +99 -0
- package/dist/chunk-CQKEQ32W.mjs.map +1 -0
- package/dist/chunk-RP7BOODV.mjs +1550 -0
- package/dist/chunk-RP7BOODV.mjs.map +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3451 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +3428 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +1781 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +44 -0
- package/dist/index.mjs.map +1 -0
- package/dist/next/index.d.mts +65 -0
- package/dist/next/index.d.ts +65 -0
- package/dist/next/index.js +103 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/index.mjs +8 -0
- package/dist/next/index.mjs.map +1 -0
- package/dist/rules-qISQhAKV.d.mts +5 -0
- package/dist/rules-qISQhAKV.d.ts +5 -0
- package/dist/shared/index.d.mts +110 -0
- package/dist/shared/index.d.ts +110 -0
- package/dist/shared/index.js +1684 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/index.mjs +15 -0
- package/dist/shared/index.mjs.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,1550 @@
|
|
|
1
|
+
// src/utils/get-file-name.ts
|
|
2
|
+
function getFileName(filename) {
|
|
3
|
+
return filename.split(/[\\/]/).at(-1) ?? filename;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// src/utils/get-source-extension.ts
|
|
7
|
+
function getSourceExtension(fileName) {
|
|
8
|
+
return fileName.endsWith(".tsx") ? ".tsx" : ".ts";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/utils/get-directory-name.ts
|
|
12
|
+
function getDirectoryName(filename) {
|
|
13
|
+
return filename.split(/[\\/]/).slice(0, -1).join("/");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/utils/is-http-route-method.ts
|
|
17
|
+
function isHttpRouteMethod(functionName) {
|
|
18
|
+
return ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"].includes(
|
|
19
|
+
functionName
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/utils/to-kebab-case.ts
|
|
24
|
+
function toKebabCase(value) {
|
|
25
|
+
return value.replace(/OEmbed/g, "Oembed").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLocaleLowerCase();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/utils/get-suggested-helper-file-name.ts
|
|
29
|
+
function getSuggestedHelperFileName({ extension, fileStem, functionName }) {
|
|
30
|
+
const helperFunctionName = fileStem === "route" && isHttpRouteMethod(functionName) ? `handle${functionName}` : functionName;
|
|
31
|
+
return `${toKebabCase(helperFunctionName)}${extension}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/utils/get-path-parts.ts
|
|
35
|
+
function getPathParts(filename) {
|
|
36
|
+
return filename.split(/[\\/]/).filter(Boolean);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/utils/is-in-source-root.ts
|
|
40
|
+
function isInSourceRoot(filename) {
|
|
41
|
+
const pathParts = getPathParts(filename);
|
|
42
|
+
return pathParts.at(-2) === "src";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/utils/is-inside-app-directory.ts
|
|
46
|
+
function isInsideAppDirectory(filename) {
|
|
47
|
+
return getPathParts(filename).includes("app");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/constants/next-app-metadata-file-stems.ts
|
|
51
|
+
var nextAppMetadataFileStems = [
|
|
52
|
+
"apple-icon",
|
|
53
|
+
"icon",
|
|
54
|
+
"manifest",
|
|
55
|
+
"opengraph-image",
|
|
56
|
+
"robots",
|
|
57
|
+
"sitemap",
|
|
58
|
+
"twitter-image"
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// src/constants/next-app-route-segment-file-stems.ts
|
|
62
|
+
var nextAppRouteSegmentFileStems = [
|
|
63
|
+
"default",
|
|
64
|
+
"error",
|
|
65
|
+
"forbidden",
|
|
66
|
+
"global-error",
|
|
67
|
+
"global-not-found",
|
|
68
|
+
"layout",
|
|
69
|
+
"loading",
|
|
70
|
+
"not-found",
|
|
71
|
+
"page",
|
|
72
|
+
"route",
|
|
73
|
+
"template",
|
|
74
|
+
"unauthorized"
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// src/constants/next-project-root-file-stems.ts
|
|
78
|
+
var nextProjectRootFileStems = [
|
|
79
|
+
"instrumentation",
|
|
80
|
+
"instrumentation-client",
|
|
81
|
+
"mdx-components",
|
|
82
|
+
"proxy"
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// src/utils/is-next-convention-file.ts
|
|
86
|
+
function isNextConventionFile({ fileStem, filename }) {
|
|
87
|
+
if ([
|
|
88
|
+
...nextAppRouteSegmentFileStems,
|
|
89
|
+
...nextAppMetadataFileStems
|
|
90
|
+
].includes(fileStem)) {
|
|
91
|
+
return isInsideAppDirectory(filename);
|
|
92
|
+
}
|
|
93
|
+
if (nextProjectRootFileStems.includes(fileStem)) {
|
|
94
|
+
return isInSourceRoot(filename);
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/utils/get-suggested-helper-path.ts
|
|
100
|
+
function getSuggestedHelperPath({ extension, fileStem, filename, functionName }) {
|
|
101
|
+
const helperFileName = getSuggestedHelperFileName({
|
|
102
|
+
extension,
|
|
103
|
+
fileStem,
|
|
104
|
+
functionName
|
|
105
|
+
});
|
|
106
|
+
if (isNextConventionFile({ fileStem, filename })) {
|
|
107
|
+
return `${getDirectoryName(filename)}/${helperFileName}`;
|
|
108
|
+
}
|
|
109
|
+
return `${getDirectoryName(filename)}/${toKebabCase(fileStem)}/${helperFileName}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/utils/get-move-suggestion.ts
|
|
113
|
+
function getMoveSuggestion({ filename, functionName }) {
|
|
114
|
+
const fileName = getFileName(filename);
|
|
115
|
+
const extension = getSourceExtension(fileName);
|
|
116
|
+
const fileStem = fileName.slice(0, -extension.length);
|
|
117
|
+
const suggestedPath = getSuggestedHelperPath({
|
|
118
|
+
extension,
|
|
119
|
+
fileStem,
|
|
120
|
+
filename,
|
|
121
|
+
functionName
|
|
122
|
+
});
|
|
123
|
+
if (fileStem === "route" && isHttpRouteMethod(functionName)) {
|
|
124
|
+
return `Mueve la implementacion de \`${functionName}\` a \`${suggestedPath}\` si solo se usa aqui y deja \`${functionName}\` en route.ts delegando a ese helper. No conviertas route.ts en route/index.ts porque Next no lo reconoce.`;
|
|
125
|
+
}
|
|
126
|
+
if (isNextConventionFile({ fileStem, filename })) {
|
|
127
|
+
return `Mueve \`${functionName}\` a \`${suggestedPath}\` si solo se usa aqui y deja \`${fileName}\` como entrypoint de Next. No conviertas \`${fileName}\` en \`${fileStem}/index${extension}\` porque Next exige el nombre exacto del archivo.`;
|
|
128
|
+
}
|
|
129
|
+
return `Mueve \`${functionName}\` a \`${suggestedPath}\` si solo se usa aqui.`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/utils/get-function-node-name.ts
|
|
133
|
+
function getFunctionNodeName(node) {
|
|
134
|
+
return node.id?.name ?? "helper";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/utils/get-variable-declarator-name.ts
|
|
138
|
+
function getVariableDeclaratorName(variableDeclarator) {
|
|
139
|
+
return variableDeclarator.id.type === "Identifier" ? variableDeclarator.id.name : getFunctionNodeName(variableDeclarator.init);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/utils/is-function-node.ts
|
|
143
|
+
function isFunctionNode(node) {
|
|
144
|
+
return node?.type === "FunctionDeclaration" || node?.type === "FunctionExpression" || node?.type === "ArrowFunctionExpression";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/utils/get-root-function-entries.ts
|
|
148
|
+
function getRootFunctionEntries(statement) {
|
|
149
|
+
const declaration = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration : statement;
|
|
150
|
+
if (!declaration) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
if (isFunctionNode(declaration)) {
|
|
154
|
+
return [
|
|
155
|
+
{
|
|
156
|
+
name: getFunctionNodeName(declaration),
|
|
157
|
+
node: declaration
|
|
158
|
+
}
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
if (declaration.type !== "VariableDeclaration") {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
return declaration.declarations.filter((variableDeclarator) => isFunctionNode(variableDeclarator.init)).map((variableDeclarator) => ({
|
|
165
|
+
name: getVariableDeclaratorName(variableDeclarator),
|
|
166
|
+
node: variableDeclarator.init
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/utils/get-suggested-helper-file-names.ts
|
|
171
|
+
function getSuggestedHelperFileNames({ extension, fileStem, functionNames }) {
|
|
172
|
+
return [
|
|
173
|
+
...new Set(
|
|
174
|
+
functionNames.map(
|
|
175
|
+
(functionName) => getSuggestedHelperFileName({
|
|
176
|
+
extension,
|
|
177
|
+
fileStem,
|
|
178
|
+
functionName
|
|
179
|
+
})
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/utils/get-tree-child-lines.ts
|
|
186
|
+
function getTreeChildLines({ indent = "", names }) {
|
|
187
|
+
return names.map((name, index) => {
|
|
188
|
+
const branch = index === names.length - 1 ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
|
|
189
|
+
return `${indent}${branch} ${name}`;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/utils/get-structure-suggestion.ts
|
|
194
|
+
function getStructureSuggestion({ filename, functionNames }) {
|
|
195
|
+
const fileName = getFileName(filename);
|
|
196
|
+
const extension = getSourceExtension(fileName);
|
|
197
|
+
const fileStem = fileName.slice(0, -extension.length);
|
|
198
|
+
const directoryName = getDirectoryName(filename);
|
|
199
|
+
const helperFileNames = getSuggestedHelperFileNames({
|
|
200
|
+
extension,
|
|
201
|
+
fileStem,
|
|
202
|
+
functionNames
|
|
203
|
+
});
|
|
204
|
+
if (isNextConventionFile({ fileStem, filename })) {
|
|
205
|
+
return [
|
|
206
|
+
`${directoryName}/`,
|
|
207
|
+
...getTreeChildLines({
|
|
208
|
+
names: [fileName, ...helperFileNames]
|
|
209
|
+
})
|
|
210
|
+
].join("\n");
|
|
211
|
+
}
|
|
212
|
+
return [
|
|
213
|
+
`${directoryName}/${toKebabCase(fileStem)}/`,
|
|
214
|
+
...getTreeChildLines({
|
|
215
|
+
names: [`index${extension}`, ...helperFileNames]
|
|
216
|
+
})
|
|
217
|
+
].join("\n");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/rules/one-root-function-per-file.ts
|
|
221
|
+
var oneRootFunctionPerFile = {
|
|
222
|
+
meta: {
|
|
223
|
+
type: "suggestion",
|
|
224
|
+
docs: {
|
|
225
|
+
description: "Limita cada archivo a una sola funcion declarada en la raiz."
|
|
226
|
+
},
|
|
227
|
+
messages: {
|
|
228
|
+
tooManyRootFunctions: "Este archivo tiene {{count}} funciones en la raiz. Deja solo una funcion top-level por archivo. {{moveSuggestion}}\n\nEstructura sugerida:\n{{structureSuggestion}}\n\nSi se reutiliza, muevela a un modulo compartido con nombre de dominio."
|
|
229
|
+
},
|
|
230
|
+
schema: []
|
|
231
|
+
},
|
|
232
|
+
create(context) {
|
|
233
|
+
return {
|
|
234
|
+
Program(node) {
|
|
235
|
+
const rootFunctions = node.body.flatMap(
|
|
236
|
+
(statement) => getRootFunctionEntries(statement)
|
|
237
|
+
);
|
|
238
|
+
if (rootFunctions.length <= 1) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const firstHelper = rootFunctions[1];
|
|
242
|
+
const helperFunctionNames = rootFunctions.slice(1).map((rootFunction) => rootFunction.name);
|
|
243
|
+
context.report({
|
|
244
|
+
data: {
|
|
245
|
+
count: String(rootFunctions.length),
|
|
246
|
+
moveSuggestion: getMoveSuggestion({
|
|
247
|
+
filename: context.filename ?? context.getFilename(),
|
|
248
|
+
functionName: firstHelper.name
|
|
249
|
+
}),
|
|
250
|
+
structureSuggestion: getStructureSuggestion({
|
|
251
|
+
filename: context.filename ?? context.getFilename(),
|
|
252
|
+
functionNames: helperFunctionNames
|
|
253
|
+
})
|
|
254
|
+
},
|
|
255
|
+
messageId: "tooManyRootFunctions",
|
|
256
|
+
node: firstHelper.node
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// src/utils/is-ast-node.ts
|
|
264
|
+
function isAstNode(value) {
|
|
265
|
+
return Boolean(value && typeof value === "object" && "type" in value);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/utils/get-node-children.ts
|
|
269
|
+
function getNodeChildren(node) {
|
|
270
|
+
return Object.entries(node).flatMap(([key, value]) => {
|
|
271
|
+
if (["parent", "loc", "range", "tokens", "comments"].includes(key)) {
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
if (Array.isArray(value)) {
|
|
275
|
+
return value.filter(isAstNode);
|
|
276
|
+
}
|
|
277
|
+
return isAstNode(value) ? [value] : [];
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/utils/contains-jsx.ts
|
|
282
|
+
function containsJsx(node) {
|
|
283
|
+
if (!isAstNode(node)) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment") {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
return getNodeChildren(node).some((child) => containsJsx(child));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/utils/contains-own-jsx.ts
|
|
293
|
+
function containsOwnJsx(node) {
|
|
294
|
+
if (!isAstNode(node)) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment") {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
if (isFunctionNode(node)) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
return getNodeChildren(node).some((child) => containsOwnJsx(child));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/utils/function-returns-jsx.ts
|
|
307
|
+
function functionReturnsJsx(functionNode) {
|
|
308
|
+
if (functionNode.body?.type !== "BlockStatement") {
|
|
309
|
+
return containsJsx(functionNode.body);
|
|
310
|
+
}
|
|
311
|
+
return containsOwnJsx(functionNode.body);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/utils/is-pascal-case-name.ts
|
|
315
|
+
function isPascalCaseName(value) {
|
|
316
|
+
return /^[A-Z][A-Za-z0-9]*$/.test(value);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/utils/to-pascal-case.ts
|
|
320
|
+
function toPascalCase(value) {
|
|
321
|
+
return value.replace(/^[a-z]/, (letter) => letter.toLocaleUpperCase());
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/rules/jsx-return-name-pascal-case.ts
|
|
325
|
+
var jsxReturnNamePascalCase = {
|
|
326
|
+
meta: {
|
|
327
|
+
type: "problem",
|
|
328
|
+
docs: {
|
|
329
|
+
description: "Exige nombres PascalCase para funciones que devuelven JSX."
|
|
330
|
+
},
|
|
331
|
+
messages: {
|
|
332
|
+
invalidName: "La funcion `{{name}}` devuelve JSX. Nombrala como componente, por ejemplo `{{suggestedName}}`, y usala con sintaxis JSX si aplica."
|
|
333
|
+
},
|
|
334
|
+
schema: []
|
|
335
|
+
},
|
|
336
|
+
create(context) {
|
|
337
|
+
function reportIfJsxReturningFunction(node, name, reportNode = node) {
|
|
338
|
+
if (!name || isPascalCaseName(name) || !functionReturnsJsx(node)) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
context.report({
|
|
342
|
+
data: {
|
|
343
|
+
name,
|
|
344
|
+
suggestedName: toPascalCase(name)
|
|
345
|
+
},
|
|
346
|
+
messageId: "invalidName",
|
|
347
|
+
node: reportNode
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
FunctionDeclaration(node) {
|
|
352
|
+
reportIfJsxReturningFunction(node, node.id?.name, node.id ?? node);
|
|
353
|
+
},
|
|
354
|
+
VariableDeclarator(node) {
|
|
355
|
+
if (!isFunctionNode(node.init) || node.id.type !== "Identifier") {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
reportIfJsxReturningFunction(node.init, node.id.name, node.id);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// src/utils/is-member-property-named.ts
|
|
365
|
+
function isMemberPropertyNamed(node, propertyName) {
|
|
366
|
+
if (node.computed) {
|
|
367
|
+
return node.property.type === "Literal" && node.property.value === propertyName;
|
|
368
|
+
}
|
|
369
|
+
return node.property.type === "Identifier" && node.property.name === propertyName;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/utils/is-callee-named.ts
|
|
373
|
+
function isCalleeNamed(node, names) {
|
|
374
|
+
if (node?.type === "Identifier") {
|
|
375
|
+
return names.includes(node.name);
|
|
376
|
+
}
|
|
377
|
+
if (node?.type === "MemberExpression") {
|
|
378
|
+
return names.some((name) => isMemberPropertyNamed(node, name));
|
|
379
|
+
}
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/utils/contains-call-named.ts
|
|
384
|
+
function containsCallNamed(node, names) {
|
|
385
|
+
if (!isAstNode(node)) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
if (node.type === "CallExpression" && isCalleeNamed(node.callee, names)) {
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
return getNodeChildren(node).some((child) => containsCallNamed(child, names));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/utils/get-async-result-rule-options.ts
|
|
395
|
+
function getAsyncResultRuleOptions(options = {}) {
|
|
396
|
+
return {
|
|
397
|
+
allowFilePatterns: options.allowFilePatterns ?? [],
|
|
398
|
+
allowNamePatterns: options.allowNamePatterns ?? [],
|
|
399
|
+
checkMissingReturnType: options.checkMissingReturnType ?? true,
|
|
400
|
+
checkMissingReturnTypeWhenCallNames: options.checkMissingReturnTypeWhenCallNames ?? [],
|
|
401
|
+
promiseTypeNames: options.promiseTypeNames ?? ["Promise"],
|
|
402
|
+
requireCallNames: options.requireCallNames ?? [],
|
|
403
|
+
resultTypeNames: options.resultTypeNames ?? ["Result"]
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/utils/get-property-name.ts
|
|
408
|
+
function getPropertyName(node) {
|
|
409
|
+
if (!node) {
|
|
410
|
+
return "anonymous";
|
|
411
|
+
}
|
|
412
|
+
if (node.type === "Identifier") {
|
|
413
|
+
return node.name;
|
|
414
|
+
}
|
|
415
|
+
if (node.type === "Literal") {
|
|
416
|
+
return String(node.value);
|
|
417
|
+
}
|
|
418
|
+
return "anonymous";
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/utils/get-parent-function-name.ts
|
|
422
|
+
function getParentFunctionName(node) {
|
|
423
|
+
const parent = node.parent;
|
|
424
|
+
if (parent?.type === "VariableDeclarator") {
|
|
425
|
+
return getVariableDeclaratorName(parent);
|
|
426
|
+
}
|
|
427
|
+
if (parent?.type === "Property" || parent?.type === "MethodDefinition" || parent?.type === "PropertyDefinition") {
|
|
428
|
+
return getPropertyName(parent.key);
|
|
429
|
+
}
|
|
430
|
+
return getFunctionNodeName(node);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/utils/get-function-expression-name.ts
|
|
434
|
+
function getFunctionExpressionName(node) {
|
|
435
|
+
return node.id?.name ?? getParentFunctionName(node);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/utils/get-parent-function-report-node.ts
|
|
439
|
+
function getParentFunctionReportNode(node) {
|
|
440
|
+
const parent = node.parent;
|
|
441
|
+
if (parent?.type === "VariableDeclarator" && parent.id.type === "Identifier") {
|
|
442
|
+
return parent.id;
|
|
443
|
+
}
|
|
444
|
+
if (parent?.type === "Property" || parent?.type === "MethodDefinition" || parent?.type === "PropertyDefinition") {
|
|
445
|
+
return parent.key;
|
|
446
|
+
}
|
|
447
|
+
return node.id ?? node;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/utils/get-type-context.ts
|
|
451
|
+
function getTypeContext(context) {
|
|
452
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
453
|
+
const parserServices = sourceCode.parserServices;
|
|
454
|
+
if (!parserServices?.program) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
checker: parserServices.program.getTypeChecker(),
|
|
459
|
+
services: parserServices
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/utils/is-anonymous-generated-function-name.ts
|
|
464
|
+
function isAnonymousGeneratedFunctionName(name) {
|
|
465
|
+
return name === "anonymous" || name === "helper";
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/utils/get-type-reference-parameters.ts
|
|
469
|
+
function getTypeReferenceParameters(node) {
|
|
470
|
+
return node.typeArguments?.params ?? node.typeParameters?.params ?? [];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/utils/is-type-reference-named.ts
|
|
474
|
+
function isTypeReferenceNamed(node, names) {
|
|
475
|
+
const typeName = node.typeName;
|
|
476
|
+
return typeName.type === "Identifier" && names.includes(typeName.name);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/utils/is-promise-of-result-type.ts
|
|
480
|
+
function isPromiseOfResultType(node, options) {
|
|
481
|
+
if (node.type !== "TSTypeReference") {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
if (!isTypeReferenceNamed(node, options.promiseTypeNames)) {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
const promiseValueType = getTypeReferenceParameters(node)[0];
|
|
488
|
+
return Boolean(
|
|
489
|
+
promiseValueType && promiseValueType.type === "TSTypeReference" && isTypeReferenceNamed(promiseValueType, options.resultTypeNames)
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/utils/get-package-name.ts
|
|
494
|
+
import { trySafe } from "@skapxd/result";
|
|
495
|
+
import fs from "fs";
|
|
496
|
+
import path from "path";
|
|
497
|
+
var packageNameByDir = /* @__PURE__ */ new Map();
|
|
498
|
+
function getPackageName(fileName) {
|
|
499
|
+
const visited = [];
|
|
500
|
+
let dir = path.dirname(fileName);
|
|
501
|
+
while (true) {
|
|
502
|
+
if (packageNameByDir.has(dir)) {
|
|
503
|
+
const cached = packageNameByDir.get(dir);
|
|
504
|
+
for (const visitedDir of visited) packageNameByDir.set(visitedDir, cached);
|
|
505
|
+
return cached;
|
|
506
|
+
}
|
|
507
|
+
visited.push(dir);
|
|
508
|
+
const packageJsonPath = path.join(dir, "package.json");
|
|
509
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
510
|
+
const parsed = trySafe(
|
|
511
|
+
() => JSON.parse(fs.readFileSync(packageJsonPath, "utf8"))
|
|
512
|
+
);
|
|
513
|
+
const name = parsed.ok ? parsed.value.name ?? null : null;
|
|
514
|
+
for (const visitedDir of visited) packageNameByDir.set(visitedDir, name);
|
|
515
|
+
return name;
|
|
516
|
+
}
|
|
517
|
+
const parent = path.dirname(dir);
|
|
518
|
+
if (parent === dir) {
|
|
519
|
+
for (const visitedDir of visited) packageNameByDir.set(visitedDir, null);
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
dir = parent;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/utils/is-skapxd-result-source-file.ts
|
|
527
|
+
function isSkapxdResultSourceFile(fileName) {
|
|
528
|
+
return getPackageName(fileName) === "@skapxd/result";
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/utils/resolve-alias-symbol.ts
|
|
532
|
+
import ts from "typescript";
|
|
533
|
+
function resolveAliasSymbol(symbol, typeContext) {
|
|
534
|
+
return symbol.flags & ts.SymbolFlags.Alias ? typeContext.checker.getAliasedSymbol(symbol) : symbol;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/utils/is-symbol-from-skapxd-result.ts
|
|
538
|
+
function isSymbolFromSkapxdResult(symbol, typeContext) {
|
|
539
|
+
const resolvedSymbol = resolveAliasSymbol(symbol, typeContext);
|
|
540
|
+
return (resolvedSymbol.getDeclarations() ?? []).some(
|
|
541
|
+
(declaration) => isSkapxdResultSourceFile(declaration.getSourceFile().fileName)
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/utils/is-skapxd-named-type.ts
|
|
546
|
+
function isSkapxdNamedType(type, names, typeContext) {
|
|
547
|
+
return [
|
|
548
|
+
type.aliasSymbol,
|
|
549
|
+
type.symbol
|
|
550
|
+
].some(
|
|
551
|
+
(symbol) => Boolean(
|
|
552
|
+
symbol && names.includes(symbol.getName()) && isSymbolFromSkapxdResult(symbol, typeContext)
|
|
553
|
+
)
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/utils/is-skapxd-result-type.ts
|
|
558
|
+
function isSkapxdResultType(type, typeContext) {
|
|
559
|
+
if (isSkapxdNamedType(type, ["Result", "SafeResult"], typeContext)) {
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
if (!type.isUnion()) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
const hasOk = type.types.some(
|
|
566
|
+
(part) => isSkapxdNamedType(part, ["Ok"], typeContext)
|
|
567
|
+
);
|
|
568
|
+
const hasErr = type.types.some(
|
|
569
|
+
(part) => isSkapxdNamedType(part, ["Err"], typeContext)
|
|
570
|
+
);
|
|
571
|
+
return hasOk && hasErr;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/utils/is-skapxd-result-or-promise-result-type.ts
|
|
575
|
+
function isSkapxdResultOrPromiseResultType(type, typeContext) {
|
|
576
|
+
if (isSkapxdResultType(type, typeContext)) {
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
const promisedType = typeContext.checker.getPromisedTypeOfPromise(type);
|
|
580
|
+
return Boolean(promisedType && isSkapxdResultType(promisedType, typeContext));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/utils/matches-any-pattern.ts
|
|
584
|
+
function matchesAnyPattern(value, patterns) {
|
|
585
|
+
return patterns.some((pattern) => new RegExp(pattern).test(value));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/rules/async-functions-return-result.ts
|
|
589
|
+
var asyncFunctionsReturnResult = {
|
|
590
|
+
meta: {
|
|
591
|
+
type: "problem",
|
|
592
|
+
docs: {
|
|
593
|
+
description: "Exige Promise<Result<...>> en funciones async de dominio."
|
|
594
|
+
},
|
|
595
|
+
messages: {
|
|
596
|
+
missingReturnType: "La funcion async `{{name}}` debe declarar Promise<Result<...>> como tipo de retorno.",
|
|
597
|
+
invalidReturnType: "La funcion async `{{name}}` debe retornar Promise<Result<...>> para modelar errores de forma explicita."
|
|
598
|
+
},
|
|
599
|
+
schema: [
|
|
600
|
+
{
|
|
601
|
+
additionalProperties: false,
|
|
602
|
+
properties: {
|
|
603
|
+
allowFilePatterns: {
|
|
604
|
+
items: { type: "string" },
|
|
605
|
+
type: "array"
|
|
606
|
+
},
|
|
607
|
+
allowNamePatterns: {
|
|
608
|
+
items: { type: "string" },
|
|
609
|
+
type: "array"
|
|
610
|
+
},
|
|
611
|
+
checkMissingReturnType: { type: "boolean" },
|
|
612
|
+
checkMissingReturnTypeWhenCallNames: {
|
|
613
|
+
items: { type: "string" },
|
|
614
|
+
type: "array"
|
|
615
|
+
},
|
|
616
|
+
requireCallNames: {
|
|
617
|
+
items: { type: "string" },
|
|
618
|
+
type: "array"
|
|
619
|
+
},
|
|
620
|
+
promiseTypeNames: {
|
|
621
|
+
items: { type: "string" },
|
|
622
|
+
type: "array"
|
|
623
|
+
},
|
|
624
|
+
resultTypeNames: {
|
|
625
|
+
items: { type: "string" },
|
|
626
|
+
type: "array"
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
type: "object"
|
|
630
|
+
}
|
|
631
|
+
]
|
|
632
|
+
},
|
|
633
|
+
create(context) {
|
|
634
|
+
const options = getAsyncResultRuleOptions(context.options[0]);
|
|
635
|
+
const filename = context.filename ?? context.getFilename();
|
|
636
|
+
const typeContext = getTypeContext(context);
|
|
637
|
+
function isSkapxdResultReturnType(annotation) {
|
|
638
|
+
if (typeContext) {
|
|
639
|
+
const type = typeContext.services.getTypeFromTypeNode(annotation);
|
|
640
|
+
return isSkapxdResultOrPromiseResultType(type, typeContext);
|
|
641
|
+
}
|
|
642
|
+
return isPromiseOfResultType(annotation, options);
|
|
643
|
+
}
|
|
644
|
+
function reportIfInvalidAsyncReturn(node, name, reportNode = node) {
|
|
645
|
+
if (!node.async || matchesAnyPattern(filename, options.allowFilePatterns)) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const functionName = name ?? "anonymous";
|
|
649
|
+
if (isAnonymousGeneratedFunctionName(functionName)) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (matchesAnyPattern(functionName, options.allowNamePatterns)) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (options.requireCallNames.length && !containsCallNamed(node.body, options.requireCallNames)) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const returnType = node.returnType?.typeAnnotation;
|
|
659
|
+
if (!returnType) {
|
|
660
|
+
if (options.checkMissingReturnType || containsCallNamed(node.body, options.checkMissingReturnTypeWhenCallNames)) {
|
|
661
|
+
context.report({
|
|
662
|
+
data: { name: functionName },
|
|
663
|
+
messageId: "missingReturnType",
|
|
664
|
+
node: reportNode
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (isSkapxdResultReturnType(returnType)) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
context.report({
|
|
673
|
+
data: { name: functionName },
|
|
674
|
+
messageId: "invalidReturnType",
|
|
675
|
+
node: reportNode
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
ArrowFunctionExpression(node) {
|
|
680
|
+
reportIfInvalidAsyncReturn(
|
|
681
|
+
node,
|
|
682
|
+
getParentFunctionName(node),
|
|
683
|
+
getParentFunctionReportNode(node)
|
|
684
|
+
);
|
|
685
|
+
},
|
|
686
|
+
FunctionDeclaration(node) {
|
|
687
|
+
reportIfInvalidAsyncReturn(node, node.id?.name, node.id ?? node);
|
|
688
|
+
},
|
|
689
|
+
FunctionExpression(node) {
|
|
690
|
+
reportIfInvalidAsyncReturn(
|
|
691
|
+
node,
|
|
692
|
+
getFunctionExpressionName(node),
|
|
693
|
+
getParentFunctionReportNode(node)
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// src/utils/get-containing-function.ts
|
|
701
|
+
function getContainingFunction(node) {
|
|
702
|
+
let currentNode = node.parent;
|
|
703
|
+
while (currentNode) {
|
|
704
|
+
if (isFunctionNode(currentNode)) {
|
|
705
|
+
return currentNode;
|
|
706
|
+
}
|
|
707
|
+
currentNode = currentNode.parent;
|
|
708
|
+
}
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/utils/get-function-name.ts
|
|
713
|
+
function getFunctionName(node) {
|
|
714
|
+
if (node.type === "FunctionDeclaration") {
|
|
715
|
+
return getFunctionNodeName(node);
|
|
716
|
+
}
|
|
717
|
+
return getParentFunctionName(node);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/utils/get-returned-object-expression.ts
|
|
721
|
+
function getReturnedObjectExpression(node) {
|
|
722
|
+
if (!node) {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
if (node.type === "ObjectExpression") {
|
|
726
|
+
return node;
|
|
727
|
+
}
|
|
728
|
+
if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSNonNullExpression" || node.type === "ChainExpression") {
|
|
729
|
+
return getReturnedObjectExpression(node.expression);
|
|
730
|
+
}
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/utils/unwrap-expression.ts
|
|
735
|
+
function unwrapExpression(node) {
|
|
736
|
+
if (node.type === "ChainExpression" || node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSNonNullExpression") {
|
|
737
|
+
return unwrapExpression(node.expression);
|
|
738
|
+
}
|
|
739
|
+
return node;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/utils/get-boolean-literal-value.ts
|
|
743
|
+
function getBooleanLiteralValue(node) {
|
|
744
|
+
const unwrappedNode = unwrapExpression(node);
|
|
745
|
+
return unwrappedNode.type === "Literal" && typeof unwrappedNode.value === "boolean" ? unwrappedNode.value : null;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/utils/is-property-key-named.ts
|
|
749
|
+
function isPropertyKeyNamed(property, propertyName) {
|
|
750
|
+
if (property.key.type === "Identifier") {
|
|
751
|
+
return property.key.name === propertyName;
|
|
752
|
+
}
|
|
753
|
+
return property.key.type === "Literal" && property.key.value === propertyName;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/utils/has-boolean-ok-property.ts
|
|
757
|
+
function hasBooleanOkProperty(node) {
|
|
758
|
+
return node.properties.some(
|
|
759
|
+
(property) => property.type === "Property" && isPropertyKeyNamed(property, "ok") && getBooleanLiteralValue(property.value) !== null
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// src/utils/is-exported-function.ts
|
|
764
|
+
function isExportedFunction(node) {
|
|
765
|
+
const parent = node.parent;
|
|
766
|
+
if (node.type === "FunctionDeclaration" && (parent?.type === "ExportNamedDeclaration" || parent?.type === "ExportDefaultDeclaration")) {
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
if ((node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") && parent?.type === "VariableDeclarator" && parent.parent?.parent?.type === "ExportNamedDeclaration") {
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/rules/no-ad-hoc-ok-result.ts
|
|
776
|
+
var noAdHocOkResult = {
|
|
777
|
+
meta: {
|
|
778
|
+
type: "problem",
|
|
779
|
+
docs: {
|
|
780
|
+
description: "Prohibe retornar contratos ad hoc con ok en funciones async exportadas."
|
|
781
|
+
},
|
|
782
|
+
messages: {
|
|
783
|
+
adHocOkResult: "No retornes objetos ad hoc con `ok` desde la funcion async `{{name}}`. Usa Result.ok(...) / Result.err(...) con un error discriminado `{ type: ... }`."
|
|
784
|
+
},
|
|
785
|
+
schema: []
|
|
786
|
+
},
|
|
787
|
+
create(context) {
|
|
788
|
+
return {
|
|
789
|
+
ReturnStatement(node) {
|
|
790
|
+
const returnedObject = getReturnedObjectExpression(node.argument);
|
|
791
|
+
if (!returnedObject || !hasBooleanOkProperty(returnedObject)) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const containingFunction = getContainingFunction(node);
|
|
795
|
+
if (!containingFunction?.async || !isExportedFunction(containingFunction)) {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
context.report({
|
|
799
|
+
data: {
|
|
800
|
+
name: getFunctionName(containingFunction)
|
|
801
|
+
},
|
|
802
|
+
messageId: "adHocOkResult",
|
|
803
|
+
node
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// src/utils/get-await-requires-try-safe-options.ts
|
|
811
|
+
function getAwaitRequiresTrySafeOptions(options = {}) {
|
|
812
|
+
return {
|
|
813
|
+
allowFilePatterns: options.allowFilePatterns ?? [],
|
|
814
|
+
trySafeCallNames: options.trySafeCallNames ?? ["trySafe"]
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/utils/get-await-scope-name.ts
|
|
819
|
+
function getAwaitScopeName(node) {
|
|
820
|
+
const containingFunction = getContainingFunction(node);
|
|
821
|
+
return containingFunction ? getFunctionName(containingFunction) : "top-level";
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/utils/get-enclosing-try-safe-call.ts
|
|
825
|
+
function getEnclosingTrySafeCall(node, trySafeCallNames) {
|
|
826
|
+
let currentNode = node.parent;
|
|
827
|
+
while (currentNode) {
|
|
828
|
+
if (isFunctionNode(currentNode)) {
|
|
829
|
+
const parent = currentNode.parent;
|
|
830
|
+
if (parent?.type === "CallExpression" && parent.arguments.includes(currentNode) && isCalleeNamed(parent.callee, trySafeCallNames)) {
|
|
831
|
+
return parent;
|
|
832
|
+
}
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
currentNode = currentNode.parent;
|
|
836
|
+
}
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// src/utils/contains-await-expression.ts
|
|
841
|
+
function containsAwaitExpression(node) {
|
|
842
|
+
if (!isAstNode(node)) {
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
if (node.type === "AwaitExpression") {
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
if (isFunctionNode(node)) {
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
return getNodeChildren(node).some((child) => containsAwaitExpression(child));
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/utils/get-call-expression-example.ts
|
|
855
|
+
function getCallExpressionExample(node, sourceCode) {
|
|
856
|
+
const calleeText = sourceCode.getText(node.callee);
|
|
857
|
+
if (node.arguments.length === 0) {
|
|
858
|
+
return `${calleeText}()`;
|
|
859
|
+
}
|
|
860
|
+
if (node.arguments.length === 1 && unwrapExpression(node.arguments[0]).type === "ObjectExpression") {
|
|
861
|
+
return `${calleeText}({...})`;
|
|
862
|
+
}
|
|
863
|
+
return `${calleeText}(...)`;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/utils/get-awaited-operation-example.ts
|
|
867
|
+
function getAwaitedOperationExample(node, sourceCode) {
|
|
868
|
+
const unwrappedNode = unwrapExpression(node);
|
|
869
|
+
if (unwrappedNode.type === "CallExpression") {
|
|
870
|
+
return getCallExpressionExample(unwrappedNode, sourceCode);
|
|
871
|
+
}
|
|
872
|
+
const expressionText = sourceCode.getText(unwrappedNode).replace(/\s+/g, " ");
|
|
873
|
+
return expressionText.length > 80 ? `${expressionText.slice(0, 77)}...` : expressionText;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/utils/get-try-safe-await-suggestion.ts
|
|
877
|
+
function getTrySafeAwaitSuggestion(node, sourceCode) {
|
|
878
|
+
const callbackKeyword = containsAwaitExpression(node) ? "async " : "";
|
|
879
|
+
return `await trySafe(${callbackKeyword}() => ${getAwaitedOperationExample(
|
|
880
|
+
node,
|
|
881
|
+
sourceCode
|
|
882
|
+
)})`;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/utils/is-skapxd-result-or-promise-result-expression.ts
|
|
886
|
+
function isSkapxdResultOrPromiseResultExpression(node, typeContext) {
|
|
887
|
+
return isSkapxdResultOrPromiseResultType(
|
|
888
|
+
typeContext.services.getTypeAtLocation(node),
|
|
889
|
+
typeContext
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// src/utils/is-try-safe-call.ts
|
|
894
|
+
function isTrySafeCall(node, trySafeCallNames) {
|
|
895
|
+
return node?.type === "CallExpression" && isCalleeNamed(node.callee, trySafeCallNames);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/rules/await-requires-try-safe.ts
|
|
899
|
+
var awaitRequiresTrySafe = {
|
|
900
|
+
meta: {
|
|
901
|
+
type: "problem",
|
|
902
|
+
docs: {
|
|
903
|
+
description: "Exige que los await esten protegidos por trySafe."
|
|
904
|
+
},
|
|
905
|
+
messages: {
|
|
906
|
+
unprotectedAwait: "El await dentro de `{{name}}` no esta protegido por trySafe. Envuelve la operacion asi: `{{suggestion}}`."
|
|
907
|
+
},
|
|
908
|
+
schema: [
|
|
909
|
+
{
|
|
910
|
+
additionalProperties: false,
|
|
911
|
+
properties: {
|
|
912
|
+
allowFilePatterns: {
|
|
913
|
+
items: { type: "string" },
|
|
914
|
+
type: "array"
|
|
915
|
+
},
|
|
916
|
+
trySafeCallNames: {
|
|
917
|
+
items: { type: "string" },
|
|
918
|
+
type: "array"
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
type: "object"
|
|
922
|
+
}
|
|
923
|
+
]
|
|
924
|
+
},
|
|
925
|
+
create(context) {
|
|
926
|
+
const options = getAwaitRequiresTrySafeOptions(context.options[0]);
|
|
927
|
+
const filename = context.filename ?? context.getFilename();
|
|
928
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
929
|
+
const typeContext = getTypeContext(context);
|
|
930
|
+
function isSkapxdTrySafe(callNode) {
|
|
931
|
+
if (!callNode) {
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
if (!typeContext) {
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
const symbol = typeContext.services.getSymbolAtLocation(callNode.callee);
|
|
938
|
+
return Boolean(symbol && isSymbolFromSkapxdResult(symbol, typeContext));
|
|
939
|
+
}
|
|
940
|
+
return {
|
|
941
|
+
AwaitExpression(node) {
|
|
942
|
+
if (matchesAnyPattern(filename, options.allowFilePatterns)) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (typeContext && isSkapxdResultOrPromiseResultExpression(node.argument, typeContext)) {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const directCall = isTrySafeCall(node.argument, options.trySafeCallNames) ? node.argument : null;
|
|
949
|
+
const enclosingCall = getEnclosingTrySafeCall(
|
|
950
|
+
node,
|
|
951
|
+
options.trySafeCallNames
|
|
952
|
+
);
|
|
953
|
+
if (isSkapxdTrySafe(directCall) || isSkapxdTrySafe(enclosingCall)) {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
context.report({
|
|
957
|
+
data: {
|
|
958
|
+
name: getAwaitScopeName(node),
|
|
959
|
+
suggestion: getTrySafeAwaitSuggestion(node.argument, sourceCode)
|
|
960
|
+
},
|
|
961
|
+
messageId: "unprotectedAwait",
|
|
962
|
+
node
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
// src/utils/get-ok-member-object.ts
|
|
970
|
+
function getOkMemberObject(node) {
|
|
971
|
+
const unwrappedNode = unwrapExpression(node);
|
|
972
|
+
if (unwrappedNode.type !== "MemberExpression" || unwrappedNode.object.type !== "Identifier" || !isMemberPropertyNamed(unwrappedNode, "ok")) {
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
return {
|
|
976
|
+
name: unwrappedNode.object.name,
|
|
977
|
+
node: unwrappedNode.object
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// src/utils/is-failed-ok-comparison.ts
|
|
982
|
+
function isFailedOkComparison(operator, comparedValue) {
|
|
983
|
+
return operator === "===" && comparedValue === false || operator === "!==" && comparedValue === true;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/utils/get-failed-result-binary-guard-name.ts
|
|
987
|
+
function getFailedResultBinaryGuardName(node) {
|
|
988
|
+
const leftResult = getOkMemberObject(node.left);
|
|
989
|
+
const rightResult = getOkMemberObject(node.right);
|
|
990
|
+
const leftBoolean = getBooleanLiteralValue(node.left);
|
|
991
|
+
const rightBoolean = getBooleanLiteralValue(node.right);
|
|
992
|
+
if (leftResult && rightBoolean !== null) {
|
|
993
|
+
return isFailedOkComparison(node.operator, rightBoolean) ? leftResult : null;
|
|
994
|
+
}
|
|
995
|
+
if (rightResult && leftBoolean !== null) {
|
|
996
|
+
return isFailedOkComparison(node.operator, leftBoolean) ? rightResult : null;
|
|
997
|
+
}
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// src/utils/get-result-check-argument.ts
|
|
1002
|
+
function getResultCheckArgument(node, methodName) {
|
|
1003
|
+
const unwrappedNode = unwrapExpression(node);
|
|
1004
|
+
if (unwrappedNode.type !== "CallExpression" || unwrappedNode.callee.type !== "MemberExpression" || !isMemberPropertyNamed(unwrappedNode.callee, methodName)) {
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
const argument = unwrappedNode.arguments[0];
|
|
1008
|
+
if (argument?.type !== "Identifier") {
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
1011
|
+
return { name: argument.name, node: argument };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/utils/get-failed-result-guard.ts
|
|
1015
|
+
function getFailedResultGuard(node) {
|
|
1016
|
+
const unwrappedNode = unwrapExpression(node);
|
|
1017
|
+
if (unwrappedNode.type === "UnaryExpression" && unwrappedNode.operator === "!") {
|
|
1018
|
+
return getOkMemberObject(unwrappedNode.argument) ?? getResultCheckArgument(unwrappedNode.argument, "isOk");
|
|
1019
|
+
}
|
|
1020
|
+
if (unwrappedNode.type === "CallExpression") {
|
|
1021
|
+
return getResultCheckArgument(unwrappedNode, "isErr");
|
|
1022
|
+
}
|
|
1023
|
+
if (unwrappedNode.type === "BinaryExpression" && ["===", "!=="].includes(unwrappedNode.operator)) {
|
|
1024
|
+
return getFailedResultBinaryGuardName(unwrappedNode);
|
|
1025
|
+
}
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// src/utils/is-result-err-call.ts
|
|
1030
|
+
function isResultErrCall(node) {
|
|
1031
|
+
return node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && isMemberPropertyNamed(node.callee, "err");
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// src/utils/get-own-result-err-calls.ts
|
|
1035
|
+
function getOwnResultErrCalls(node, isRoot = true) {
|
|
1036
|
+
if (!isAstNode(node)) {
|
|
1037
|
+
return [];
|
|
1038
|
+
}
|
|
1039
|
+
if (isFunctionNode(node) || !isRoot && node.type === "IfStatement") {
|
|
1040
|
+
return [];
|
|
1041
|
+
}
|
|
1042
|
+
const ownCalls = node.type === "CallExpression" && isResultErrCall(node) ? [node] : [];
|
|
1043
|
+
return [
|
|
1044
|
+
...ownCalls,
|
|
1045
|
+
...getNodeChildren(node).flatMap((child) => getOwnResultErrCalls(child, false))
|
|
1046
|
+
];
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// src/utils/get-function-return-type.ts
|
|
1050
|
+
function getFunctionReturnType(node, typeContext) {
|
|
1051
|
+
if (node.returnType?.typeAnnotation) {
|
|
1052
|
+
return typeContext.services.getTypeFromTypeNode(node.returnType.typeAnnotation);
|
|
1053
|
+
}
|
|
1054
|
+
const functionType = typeContext.services.getTypeAtLocation(node);
|
|
1055
|
+
const signature = functionType.getCallSignatures()[0];
|
|
1056
|
+
return signature?.getReturnType() ?? null;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/utils/function-returns-skapxd-result-type.ts
|
|
1060
|
+
function functionReturnsSkapxdResultType(node, typeContext) {
|
|
1061
|
+
const returnType = getFunctionReturnType(node, typeContext);
|
|
1062
|
+
return Boolean(returnType && isSkapxdResultOrPromiseResultType(returnType, typeContext));
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/utils/is-inside-skapxd-result-returning-function.ts
|
|
1066
|
+
function isInsideSkapxdResultReturningFunction(node, typeContext) {
|
|
1067
|
+
const containingFunction = getContainingFunction(node);
|
|
1068
|
+
return Boolean(
|
|
1069
|
+
containingFunction && functionReturnsSkapxdResultType(containingFunction, typeContext)
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// src/utils/is-skapxd-result-err-call.ts
|
|
1074
|
+
function isSkapxdResultErrCall(node, typeContext) {
|
|
1075
|
+
if (!isResultErrCall(node)) {
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
const symbol = typeContext.services.getSymbolAtLocation(node.callee.object);
|
|
1079
|
+
return Boolean(symbol && isSymbolFromSkapxdResult(symbol, typeContext));
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// src/utils/is-skapxd-result-expression.ts
|
|
1083
|
+
function isSkapxdResultExpression(node, typeContext) {
|
|
1084
|
+
return isSkapxdResultType(typeContext.services.getTypeAtLocation(node), typeContext);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// src/utils/is-result-error-member.ts
|
|
1088
|
+
function isResultErrorMember(node, resultName) {
|
|
1089
|
+
const unwrappedNode = unwrapExpression(node);
|
|
1090
|
+
return unwrappedNode.type === "MemberExpression" && unwrappedNode.object.type === "Identifier" && unwrappedNode.object.name === resultName && isMemberPropertyNamed(unwrappedNode, "error");
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/utils/result-err-preserves-cause.ts
|
|
1094
|
+
function resultErrPreservesCause(node, resultName) {
|
|
1095
|
+
const unwrappedNode = unwrapExpression(node);
|
|
1096
|
+
if (isResultErrorMember(unwrappedNode, resultName)) {
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
if (unwrappedNode.type !== "ObjectExpression") {
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
return unwrappedNode.properties.some((property) => {
|
|
1103
|
+
if (property.type !== "Property" || !isPropertyKeyNamed(property, "cause")) {
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
return isResultErrorMember(property.value, resultName);
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// src/rules/result-error-requires-cause.ts
|
|
1111
|
+
var resultErrorRequiresCause = {
|
|
1112
|
+
meta: {
|
|
1113
|
+
type: "problem",
|
|
1114
|
+
docs: {
|
|
1115
|
+
description: "Exige preservar result.error como cause cuando una funcion que retorna Result transforma un Result fallido en Result.err."
|
|
1116
|
+
},
|
|
1117
|
+
messages: {
|
|
1118
|
+
missingCause: "El error de `{{name}}` ya existe como `{{name}}.error`. Preservalo en Result.err con `cause: {{name}}.error`."
|
|
1119
|
+
},
|
|
1120
|
+
schema: []
|
|
1121
|
+
},
|
|
1122
|
+
create(context) {
|
|
1123
|
+
const typeContext = getTypeContext(context);
|
|
1124
|
+
return {
|
|
1125
|
+
IfStatement(node) {
|
|
1126
|
+
const resultGuard = getFailedResultGuard(node.test);
|
|
1127
|
+
if (!typeContext || !resultGuard || !isSkapxdResultExpression(resultGuard.node, typeContext) || !isInsideSkapxdResultReturningFunction(node, typeContext)) {
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
for (const resultErrCall of getOwnResultErrCalls(node.consequent)) {
|
|
1131
|
+
if (!isSkapxdResultErrCall(resultErrCall, typeContext)) {
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
if (resultErrCall.arguments.length === 0) {
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (resultErrPreservesCause(resultErrCall.arguments[0], resultGuard.name)) {
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
context.report({
|
|
1141
|
+
data: {
|
|
1142
|
+
name: resultGuard.name
|
|
1143
|
+
},
|
|
1144
|
+
messageId: "missingCause",
|
|
1145
|
+
node: resultErrCall
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
// src/utils/count-own-use-state-calls-in-node.ts
|
|
1154
|
+
function countOwnUseStateCallsInNode(node) {
|
|
1155
|
+
if (!isAstNode(node)) {
|
|
1156
|
+
return 0;
|
|
1157
|
+
}
|
|
1158
|
+
if (isFunctionNode(node)) {
|
|
1159
|
+
return 0;
|
|
1160
|
+
}
|
|
1161
|
+
const ownCount = node.type === "CallExpression" && isCalleeNamed(node.callee, ["useState"]) ? 1 : 0;
|
|
1162
|
+
return ownCount + getNodeChildren(node).reduce(
|
|
1163
|
+
(total, child) => total + countOwnUseStateCallsInNode(child),
|
|
1164
|
+
0
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// src/utils/count-own-use-state-calls.ts
|
|
1169
|
+
function countOwnUseStateCalls(node) {
|
|
1170
|
+
const body = node.body;
|
|
1171
|
+
return isAstNode(body) ? countOwnUseStateCallsInNode(body) : 0;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// src/utils/get-function-line-count.ts
|
|
1175
|
+
function getFunctionLineCount(node) {
|
|
1176
|
+
return node.loc ? node.loc.end.line - node.loc.start.line + 1 : 0;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/utils/get-max-hook-size-options.ts
|
|
1180
|
+
function getMaxHookSizeOptions(options = {}) {
|
|
1181
|
+
return {
|
|
1182
|
+
maxLines: options.maxLines ?? 120,
|
|
1183
|
+
maxUseState: options.maxUseState ?? 1
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// src/utils/is-hook-name.ts
|
|
1188
|
+
function isHookName(name) {
|
|
1189
|
+
return /^use[A-Z0-9]/.test(name ?? "");
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// src/rules/max-hook-size.ts
|
|
1193
|
+
var maxHookSize = {
|
|
1194
|
+
meta: {
|
|
1195
|
+
type: "suggestion",
|
|
1196
|
+
docs: {
|
|
1197
|
+
description: "Limita el tama\xF1o y cantidad de estados propios en hooks React."
|
|
1198
|
+
},
|
|
1199
|
+
messages: {
|
|
1200
|
+
tooLargeHook: "El hook `{{name}}` es demasiado grande: tiene {{lines}} lineas. Maximo permitido: {{maxLines}} lineas. Extrae efectos, handlers o flujos a hooks/archivos semanticos.",
|
|
1201
|
+
tooManyUseState: "El hook `{{name}}` declara {{useStateCount}} useState. Maximo permitido: {{maxUseState}}. Usa useReducer con acciones semanticas cuando varios campos cambian juntos, o extrae estado a hooks especializados."
|
|
1202
|
+
},
|
|
1203
|
+
schema: [
|
|
1204
|
+
{
|
|
1205
|
+
additionalProperties: false,
|
|
1206
|
+
properties: {
|
|
1207
|
+
maxLines: { type: "number" },
|
|
1208
|
+
maxUseState: { type: "number" }
|
|
1209
|
+
},
|
|
1210
|
+
type: "object"
|
|
1211
|
+
}
|
|
1212
|
+
]
|
|
1213
|
+
},
|
|
1214
|
+
create(context) {
|
|
1215
|
+
const options = getMaxHookSizeOptions(context.options[0]);
|
|
1216
|
+
function reportIfOversizedHook(node, name, reportNode = node) {
|
|
1217
|
+
if (!isHookName(name)) {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
const lines = getFunctionLineCount(node);
|
|
1221
|
+
const useStateCount = countOwnUseStateCalls(node);
|
|
1222
|
+
if (lines > options.maxLines) {
|
|
1223
|
+
context.report({
|
|
1224
|
+
data: {
|
|
1225
|
+
lines: String(lines),
|
|
1226
|
+
maxLines: String(options.maxLines),
|
|
1227
|
+
name
|
|
1228
|
+
},
|
|
1229
|
+
messageId: "tooLargeHook",
|
|
1230
|
+
node: reportNode
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
if (useStateCount > options.maxUseState) {
|
|
1234
|
+
context.report({
|
|
1235
|
+
data: {
|
|
1236
|
+
maxUseState: String(options.maxUseState),
|
|
1237
|
+
name,
|
|
1238
|
+
useStateCount: String(useStateCount)
|
|
1239
|
+
},
|
|
1240
|
+
messageId: "tooManyUseState",
|
|
1241
|
+
node: reportNode
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return {
|
|
1246
|
+
ArrowFunctionExpression(node) {
|
|
1247
|
+
reportIfOversizedHook(
|
|
1248
|
+
node,
|
|
1249
|
+
getParentFunctionName(node),
|
|
1250
|
+
getParentFunctionReportNode(node)
|
|
1251
|
+
);
|
|
1252
|
+
},
|
|
1253
|
+
FunctionDeclaration(node) {
|
|
1254
|
+
reportIfOversizedHook(node, node.id?.name, node.id ?? node);
|
|
1255
|
+
},
|
|
1256
|
+
FunctionExpression(node) {
|
|
1257
|
+
reportIfOversizedHook(
|
|
1258
|
+
node,
|
|
1259
|
+
getFunctionExpressionName(node),
|
|
1260
|
+
getParentFunctionReportNode(node)
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
// src/utils/count-parent-segments.ts
|
|
1268
|
+
function countParentSegments(source) {
|
|
1269
|
+
let count = 0;
|
|
1270
|
+
for (const part of source.split("/")) {
|
|
1271
|
+
if (part === "..") {
|
|
1272
|
+
count += 1;
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
if (part === ".") {
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
break;
|
|
1279
|
+
}
|
|
1280
|
+
return count;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// src/rules/no-deep-relative-imports.ts
|
|
1284
|
+
var noDeepRelativeImports = {
|
|
1285
|
+
meta: {
|
|
1286
|
+
type: "suggestion",
|
|
1287
|
+
docs: {
|
|
1288
|
+
description: "Limita la profundidad de los imports relativos (`../`). Empuja hacia estructuras planas o alias de ruta."
|
|
1289
|
+
},
|
|
1290
|
+
messages: {
|
|
1291
|
+
deepRelativeImport: "El import `{{source}}` sube {{depth}} niveles con `../`. Maximo permitido: {{maxDepth}}. Usa un alias de ruta (ej. `@/...`) o acerca el modulo a quien lo usa."
|
|
1292
|
+
},
|
|
1293
|
+
schema: [
|
|
1294
|
+
{
|
|
1295
|
+
additionalProperties: false,
|
|
1296
|
+
properties: {
|
|
1297
|
+
maxDepth: { type: "number" }
|
|
1298
|
+
},
|
|
1299
|
+
type: "object"
|
|
1300
|
+
}
|
|
1301
|
+
]
|
|
1302
|
+
},
|
|
1303
|
+
create(context) {
|
|
1304
|
+
const maxDepth = context.options[0]?.maxDepth ?? 0;
|
|
1305
|
+
function reportIfTooDeep(source) {
|
|
1306
|
+
if (!source || typeof source.value !== "string") {
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
const depth = countParentSegments(source.value);
|
|
1310
|
+
if (depth <= maxDepth) {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
context.report({
|
|
1314
|
+
data: {
|
|
1315
|
+
depth: String(depth),
|
|
1316
|
+
maxDepth: String(maxDepth),
|
|
1317
|
+
source: source.value
|
|
1318
|
+
},
|
|
1319
|
+
messageId: "deepRelativeImport",
|
|
1320
|
+
node: source
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
return {
|
|
1324
|
+
ExportAllDeclaration(node) {
|
|
1325
|
+
reportIfTooDeep(node.source);
|
|
1326
|
+
},
|
|
1327
|
+
ExportNamedDeclaration(node) {
|
|
1328
|
+
reportIfTooDeep(node.source);
|
|
1329
|
+
},
|
|
1330
|
+
ImportDeclaration(node) {
|
|
1331
|
+
reportIfTooDeep(node.source);
|
|
1332
|
+
},
|
|
1333
|
+
ImportExpression(node) {
|
|
1334
|
+
reportIfTooDeep(node.source);
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
// src/rules/no-functions-inside-components.ts
|
|
1341
|
+
var noFunctionsInsideComponents = {
|
|
1342
|
+
meta: {
|
|
1343
|
+
type: "suggestion",
|
|
1344
|
+
docs: {
|
|
1345
|
+
description: "Prohibe definir funciones dentro de componentes React; se recrean en cada render."
|
|
1346
|
+
},
|
|
1347
|
+
messages: {
|
|
1348
|
+
functionInsideComponent: "No definas funciones dentro del componente `{{component}}`: se recrean en cada render. Muevela a un hook (`useX`) o a un helper fuera del componente."
|
|
1349
|
+
},
|
|
1350
|
+
schema: []
|
|
1351
|
+
},
|
|
1352
|
+
create(context) {
|
|
1353
|
+
function isComponentFunction(node) {
|
|
1354
|
+
return isFunctionNode(node) && isPascalCaseName(getFunctionName(node));
|
|
1355
|
+
}
|
|
1356
|
+
function reportIfInsideComponent(node) {
|
|
1357
|
+
const enclosingFunction = getContainingFunction(node);
|
|
1358
|
+
if (!enclosingFunction || !isComponentFunction(enclosingFunction)) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
context.report({
|
|
1362
|
+
data: {
|
|
1363
|
+
component: getFunctionName(enclosingFunction)
|
|
1364
|
+
},
|
|
1365
|
+
messageId: "functionInsideComponent",
|
|
1366
|
+
node
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
return {
|
|
1370
|
+
ArrowFunctionExpression: reportIfInsideComponent,
|
|
1371
|
+
FunctionDeclaration: reportIfInsideComponent,
|
|
1372
|
+
FunctionExpression: reportIfInsideComponent
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
|
|
1377
|
+
// src/rules/no-try-catch.ts
|
|
1378
|
+
var noTryCatch = {
|
|
1379
|
+
meta: {
|
|
1380
|
+
type: "suggestion",
|
|
1381
|
+
docs: {
|
|
1382
|
+
description: "Prohibe try/catch; usa trySafe de @skapxd/result para modelar el error como Result."
|
|
1383
|
+
},
|
|
1384
|
+
messages: {
|
|
1385
|
+
noTryCatch: "Usa `trySafe` de @skapxd/result en lugar de try/catch. El error queda modelado como Result en vez de saltar como excepcion."
|
|
1386
|
+
},
|
|
1387
|
+
schema: []
|
|
1388
|
+
},
|
|
1389
|
+
create(context) {
|
|
1390
|
+
return {
|
|
1391
|
+
TryStatement(node) {
|
|
1392
|
+
context.report({
|
|
1393
|
+
messageId: "noTryCatch",
|
|
1394
|
+
node
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
// src/rules/prefer-ts-pattern.ts
|
|
1402
|
+
var preferTsPattern = {
|
|
1403
|
+
meta: {
|
|
1404
|
+
type: "suggestion",
|
|
1405
|
+
docs: {
|
|
1406
|
+
description: "Prefiere match() de ts-pattern sobre switch/case y ternarios anidados."
|
|
1407
|
+
},
|
|
1408
|
+
messages: {
|
|
1409
|
+
noSwitch: "Usa `match()` de ts-pattern en lugar de switch/case para un control de flujo exhaustivo y tipado.",
|
|
1410
|
+
noNestedTernary: "Usa `match()` de ts-pattern en lugar de ternarios anidados; mejora la legibilidad y la exhaustividad."
|
|
1411
|
+
},
|
|
1412
|
+
schema: []
|
|
1413
|
+
},
|
|
1414
|
+
create(context) {
|
|
1415
|
+
return {
|
|
1416
|
+
ConditionalExpression(node) {
|
|
1417
|
+
if (node.parent?.type === "ConditionalExpression") {
|
|
1418
|
+
context.report({
|
|
1419
|
+
messageId: "noNestedTernary",
|
|
1420
|
+
node
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
},
|
|
1424
|
+
SwitchStatement(node) {
|
|
1425
|
+
context.report({
|
|
1426
|
+
messageId: "noSwitch",
|
|
1427
|
+
node
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
// src/rules/no-jsx-ternary-null.ts
|
|
1435
|
+
var noJsxTernaryNull = {
|
|
1436
|
+
meta: {
|
|
1437
|
+
type: "suggestion",
|
|
1438
|
+
docs: {
|
|
1439
|
+
description: "Prefiere `condicion && <Elemento />` sobre un ternario con `null` al renderizar JSX."
|
|
1440
|
+
},
|
|
1441
|
+
messages: {
|
|
1442
|
+
preferLogicalAnd: "Usa `condicion && elemento` en lugar de un ternario con `null` para renderizar JSX condicional."
|
|
1443
|
+
},
|
|
1444
|
+
schema: []
|
|
1445
|
+
},
|
|
1446
|
+
create(context) {
|
|
1447
|
+
function isNullLiteral(node) {
|
|
1448
|
+
return node?.type === "Literal" && node.value === null;
|
|
1449
|
+
}
|
|
1450
|
+
return {
|
|
1451
|
+
ConditionalExpression(node) {
|
|
1452
|
+
const container = node.parent;
|
|
1453
|
+
if (container?.type !== "JSXExpressionContainer") {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
const host = container.parent;
|
|
1457
|
+
if (host?.type !== "JSXElement" && host?.type !== "JSXFragment") {
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
if (!isNullLiteral(node.alternate) && !isNullLiteral(node.consequent)) {
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
context.report({
|
|
1464
|
+
messageId: "preferLogicalAnd",
|
|
1465
|
+
node
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
// src/utils/is-promise-type.ts
|
|
1473
|
+
function isPromiseType(type, typeContext) {
|
|
1474
|
+
return typeContext.checker.getPromisedTypeOfPromise(type) !== void 0;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/rules/no-promise-chain.ts
|
|
1478
|
+
var defaultMethods = ["catch", "finally", "then"];
|
|
1479
|
+
var noPromiseChain = {
|
|
1480
|
+
meta: {
|
|
1481
|
+
type: "suggestion",
|
|
1482
|
+
docs: {
|
|
1483
|
+
description: "Prohibe encadenar .then/.catch/.finally en promesas; usa await (envuelto en trySafe)."
|
|
1484
|
+
},
|
|
1485
|
+
messages: {
|
|
1486
|
+
noPromiseChain: "No encadenes `.{{method}}()` en una promesa. La unica forma de tratar funciones asincronas es `await` (envuelto en `trySafe`)."
|
|
1487
|
+
},
|
|
1488
|
+
schema: [
|
|
1489
|
+
{
|
|
1490
|
+
additionalProperties: false,
|
|
1491
|
+
properties: {
|
|
1492
|
+
methods: {
|
|
1493
|
+
items: { type: "string" },
|
|
1494
|
+
type: "array"
|
|
1495
|
+
}
|
|
1496
|
+
},
|
|
1497
|
+
type: "object"
|
|
1498
|
+
}
|
|
1499
|
+
]
|
|
1500
|
+
},
|
|
1501
|
+
create(context) {
|
|
1502
|
+
const methods = context.options[0]?.methods ?? defaultMethods;
|
|
1503
|
+
const typeContext = getTypeContext(context);
|
|
1504
|
+
return {
|
|
1505
|
+
CallExpression(node) {
|
|
1506
|
+
const callee = node.callee;
|
|
1507
|
+
if (callee.type !== "MemberExpression") {
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
const method = methods.find((name) => isMemberPropertyNamed(callee, name));
|
|
1511
|
+
if (!method) {
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (typeContext) {
|
|
1515
|
+
const type = typeContext.services.getTypeAtLocation(callee.object);
|
|
1516
|
+
if (!isPromiseType(type, typeContext)) {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
context.report({
|
|
1521
|
+
data: { method },
|
|
1522
|
+
messageId: "noPromiseChain",
|
|
1523
|
+
node: callee.property
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
// src/shared/rules.ts
|
|
1531
|
+
var rules = {
|
|
1532
|
+
"one-root-function-per-file": oneRootFunctionPerFile,
|
|
1533
|
+
"jsx-return-name-pascal-case": jsxReturnNamePascalCase,
|
|
1534
|
+
"async-functions-return-result": asyncFunctionsReturnResult,
|
|
1535
|
+
"no-ad-hoc-ok-result": noAdHocOkResult,
|
|
1536
|
+
"await-requires-try-safe": awaitRequiresTrySafe,
|
|
1537
|
+
"result-error-requires-cause": resultErrorRequiresCause,
|
|
1538
|
+
"max-hook-size": maxHookSize,
|
|
1539
|
+
"no-deep-relative-imports": noDeepRelativeImports,
|
|
1540
|
+
"no-functions-inside-components": noFunctionsInsideComponents,
|
|
1541
|
+
"no-try-catch": noTryCatch,
|
|
1542
|
+
"prefer-ts-pattern": preferTsPattern,
|
|
1543
|
+
"no-jsx-ternary-null": noJsxTernaryNull,
|
|
1544
|
+
"no-promise-chain": noPromiseChain
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
export {
|
|
1548
|
+
rules
|
|
1549
|
+
};
|
|
1550
|
+
//# sourceMappingURL=chunk-RP7BOODV.mjs.map
|