@yasainet/eslint 0.0.63 → 0.0.65
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/package.json
CHANGED
package/src/common/layers.mjs
CHANGED
|
@@ -94,7 +94,7 @@ export function createLayersConfigs(featureRoot, { typeAware = true } = {}) {
|
|
|
94
94
|
// into their public API. Uses type-aware inspection of the inferred
|
|
95
95
|
// return type so unannotated functions are still checked.
|
|
96
96
|
...(typeAware ? [noAnyReturnConfig] : []),
|
|
97
|
-
// Services: try-catch + logger + dead error fallbacks
|
|
97
|
+
// Services: try-catch + logger + throw + dead error fallbacks
|
|
98
98
|
{
|
|
99
99
|
name: "layers/services",
|
|
100
100
|
files: [`${featureRoot}/**/services/*.ts`],
|
|
@@ -106,6 +106,11 @@ export function createLayersConfigs(featureRoot, { typeAware = true } = {}) {
|
|
|
106
106
|
message:
|
|
107
107
|
"try-catch is not allowed in services. Error handling belongs in entries.",
|
|
108
108
|
},
|
|
109
|
+
{
|
|
110
|
+
selector: "ThrowStatement",
|
|
111
|
+
message:
|
|
112
|
+
"throw is not allowed in services. Communicate failures via T | null / { data, error } / empty default. Native exceptions from libs auto-propagate to entry's catch.",
|
|
113
|
+
},
|
|
109
114
|
{ selector: loggerSelector, message: loggerMessage },
|
|
110
115
|
{
|
|
111
116
|
selector:
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enforce the canonical entry template for `**\/entries/*.ts` exports:
|
|
3
|
+
*
|
|
4
|
+
* - body must be a single try/catch
|
|
5
|
+
* - try first statement: `logger.info(<obj>, "Start <funcName>")`
|
|
6
|
+
* - try success return preceded by: `logger.info(<obj>, "Success <funcName>")`
|
|
7
|
+
* - try failed branch (when present): `logger.error(<obj>, "Failed <funcName>")`
|
|
8
|
+
* followed by a return with the proper error shape
|
|
9
|
+
* - catch param: `error: unknown`
|
|
10
|
+
* - catch first statement: `logger.error(<obj>, "Unexpected error in <funcName>")`
|
|
11
|
+
* - catch return error.message must be the literal "An unexpected error occurred"
|
|
12
|
+
* - every log object must include the `err` key first (when applicable) and
|
|
13
|
+
* propagate all function input parameters as values
|
|
14
|
+
*
|
|
15
|
+
* Why one rule with many messageIds: each invariant is a small rule
|
|
16
|
+
* conceptually, but they share the same structural traversal and access to
|
|
17
|
+
* funcName / inputArgs. Splitting would duplicate the AST walk.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const CATCH_RETURN_MESSAGE = "An unexpected error occurred";
|
|
21
|
+
|
|
22
|
+
function getInputArgNames(params) {
|
|
23
|
+
const names = [];
|
|
24
|
+
for (const p of params) {
|
|
25
|
+
if (p.type === "Identifier") {
|
|
26
|
+
names.push(p.name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return names;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isLoggerCall(node, level) {
|
|
33
|
+
if (node?.type !== "CallExpression") return false;
|
|
34
|
+
const callee = node.callee;
|
|
35
|
+
return (
|
|
36
|
+
callee.type === "MemberExpression" &&
|
|
37
|
+
callee.object.type === "Identifier" &&
|
|
38
|
+
callee.object.name === "logger" &&
|
|
39
|
+
callee.property.type === "Identifier" &&
|
|
40
|
+
callee.property.name === level
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getStringLiteralArg(callExpr, index) {
|
|
45
|
+
const arg = callExpr.arguments[index];
|
|
46
|
+
if (arg?.type === "Literal" && typeof arg.value === "string") return arg.value;
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getObjectLiteralArg(callExpr, index) {
|
|
51
|
+
const arg = callExpr.arguments[index];
|
|
52
|
+
if (arg?.type === "ObjectExpression") return arg;
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function objectContainsValue(objExpr, identifierName) {
|
|
57
|
+
if (!objExpr) return false;
|
|
58
|
+
for (const prop of objExpr.properties) {
|
|
59
|
+
if (prop.type !== "Property") continue;
|
|
60
|
+
if (
|
|
61
|
+
prop.shorthand &&
|
|
62
|
+
prop.key.type === "Identifier" &&
|
|
63
|
+
prop.key.name === identifierName
|
|
64
|
+
) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (prop.value.type === "Identifier" && prop.value.name === identifierName) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function firstPropertyIsErrKey(objExpr, errorIdentifierName) {
|
|
75
|
+
if (!objExpr || objExpr.properties.length === 0) return false;
|
|
76
|
+
const first = objExpr.properties[0];
|
|
77
|
+
if (first.type !== "Property") return false;
|
|
78
|
+
if (first.key.type !== "Identifier" || first.key.name !== "err") return false;
|
|
79
|
+
// value must be the catch error identifier (or any Identifier — we accept both
|
|
80
|
+
// `err: error` and `err: result.error` since Failed Pattern C uses MemberExpression)
|
|
81
|
+
if (first.value.type === "Identifier") {
|
|
82
|
+
return errorIdentifierName ? first.value.name === errorIdentifierName : true;
|
|
83
|
+
}
|
|
84
|
+
if (first.value.type === "MemberExpression") return true;
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isExpressionStatementWithLoggerCall(stmt, level) {
|
|
89
|
+
if (stmt?.type !== "ExpressionStatement") return false;
|
|
90
|
+
return isLoggerCall(stmt.expression, level);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function reportMissingInputArgs(context, objExpr, inputArgNames, ctx) {
|
|
94
|
+
for (const name of inputArgNames) {
|
|
95
|
+
if (!objectContainsValue(objExpr, name)) {
|
|
96
|
+
context.report({
|
|
97
|
+
node: objExpr ?? ctx.fallbackNode,
|
|
98
|
+
messageId: "logMissingInputArg",
|
|
99
|
+
data: { argName: name, where: ctx.where, funcName: ctx.funcName },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function checkLogCall({
|
|
106
|
+
context,
|
|
107
|
+
callExpr,
|
|
108
|
+
expectedLevel,
|
|
109
|
+
expectedMessage,
|
|
110
|
+
funcName,
|
|
111
|
+
inputArgNames,
|
|
112
|
+
requireErrFirst,
|
|
113
|
+
errorIdentifierName,
|
|
114
|
+
where,
|
|
115
|
+
}) {
|
|
116
|
+
if (!isLoggerCall(callExpr, expectedLevel)) {
|
|
117
|
+
context.report({
|
|
118
|
+
node: callExpr,
|
|
119
|
+
messageId: "logWrongCallShape",
|
|
120
|
+
data: { where, expectedLevel, funcName, expectedMessage },
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const msg = getStringLiteralArg(callExpr, 1);
|
|
125
|
+
if (msg !== expectedMessage) {
|
|
126
|
+
context.report({
|
|
127
|
+
node: callExpr,
|
|
128
|
+
messageId: "logWrongMessage",
|
|
129
|
+
data: { where, expectedMessage, actual: msg ?? "<not-a-literal>" },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const obj = getObjectLiteralArg(callExpr, 0);
|
|
133
|
+
if (!obj) {
|
|
134
|
+
context.report({
|
|
135
|
+
node: callExpr,
|
|
136
|
+
messageId: "logFirstArgNotObject",
|
|
137
|
+
data: { where, funcName },
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (requireErrFirst && !firstPropertyIsErrKey(obj, errorIdentifierName)) {
|
|
142
|
+
context.report({
|
|
143
|
+
node: obj,
|
|
144
|
+
messageId: "logErrKeyNotFirst",
|
|
145
|
+
data: { where, funcName },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
reportMissingInputArgs(context, obj, inputArgNames, {
|
|
149
|
+
where,
|
|
150
|
+
funcName,
|
|
151
|
+
fallbackNode: callExpr,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isReturnDataErrorNull(ret) {
|
|
156
|
+
// `return { data: ..., error: null }` (data shorthand or explicit)
|
|
157
|
+
const arg = ret.argument;
|
|
158
|
+
if (arg?.type !== "ObjectExpression") return false;
|
|
159
|
+
let hasData = false;
|
|
160
|
+
let hasErrorNull = false;
|
|
161
|
+
for (const prop of arg.properties) {
|
|
162
|
+
if (prop.type !== "Property") continue;
|
|
163
|
+
if (prop.key.type !== "Identifier") continue;
|
|
164
|
+
if (prop.key.name === "data") hasData = true;
|
|
165
|
+
if (
|
|
166
|
+
prop.key.name === "error" &&
|
|
167
|
+
prop.value.type === "Literal" &&
|
|
168
|
+
prop.value.value === null
|
|
169
|
+
) {
|
|
170
|
+
hasErrorNull = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return hasData && hasErrorNull;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getReturnErrorMessageLiteral(ret) {
|
|
177
|
+
const arg = ret.argument;
|
|
178
|
+
if (arg?.type !== "ObjectExpression") return null;
|
|
179
|
+
for (const prop of arg.properties) {
|
|
180
|
+
if (prop.type !== "Property") continue;
|
|
181
|
+
if (prop.key.type !== "Identifier" || prop.key.name !== "error") continue;
|
|
182
|
+
if (prop.value.type !== "ObjectExpression") return null;
|
|
183
|
+
for (const inner of prop.value.properties) {
|
|
184
|
+
if (inner.type !== "Property") continue;
|
|
185
|
+
if (inner.key.type !== "Identifier" || inner.key.name !== "message") continue;
|
|
186
|
+
if (inner.value.type === "Literal" && typeof inner.value.value === "string") {
|
|
187
|
+
return inner.value.value;
|
|
188
|
+
}
|
|
189
|
+
return "<non-literal>";
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function checkTryBlock(context, tryBlock, funcName, inputArgNames) {
|
|
196
|
+
if (tryBlock.body.length === 0) {
|
|
197
|
+
context.report({ node: tryBlock, messageId: "tryEmpty", data: { funcName } });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Start log: first statement
|
|
202
|
+
const first = tryBlock.body[0];
|
|
203
|
+
if (!isExpressionStatementWithLoggerCall(first, "info")) {
|
|
204
|
+
context.report({
|
|
205
|
+
node: first,
|
|
206
|
+
messageId: "tryMissingStartLog",
|
|
207
|
+
data: { funcName },
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
checkLogCall({
|
|
211
|
+
context,
|
|
212
|
+
callExpr: first.expression,
|
|
213
|
+
expectedLevel: "info",
|
|
214
|
+
expectedMessage: `Start ${funcName}`,
|
|
215
|
+
funcName,
|
|
216
|
+
inputArgNames,
|
|
217
|
+
requireErrFirst: false,
|
|
218
|
+
errorIdentifierName: null,
|
|
219
|
+
where: "Start",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Walk body to find Success returns and Failed branches
|
|
224
|
+
let successFound = false;
|
|
225
|
+
for (let i = 0; i < tryBlock.body.length; i++) {
|
|
226
|
+
const stmt = tryBlock.body[i];
|
|
227
|
+
|
|
228
|
+
// Success log + return
|
|
229
|
+
if (stmt.type === "ReturnStatement" && isReturnDataErrorNull(stmt)) {
|
|
230
|
+
successFound = true;
|
|
231
|
+
// The previous non-ExpressionStatement non-IfStatement statement should be
|
|
232
|
+
// the Success log. Walk back to find it.
|
|
233
|
+
const prev = findPrecedingLoggerCall(tryBlock.body, i);
|
|
234
|
+
if (
|
|
235
|
+
!prev ||
|
|
236
|
+
!isLoggerCall(prev, "info") ||
|
|
237
|
+
getStringLiteralArg(prev, 1) !== `Success ${funcName}`
|
|
238
|
+
) {
|
|
239
|
+
context.report({
|
|
240
|
+
node: stmt,
|
|
241
|
+
messageId: "trySuccessLogMissing",
|
|
242
|
+
data: { funcName },
|
|
243
|
+
});
|
|
244
|
+
} else {
|
|
245
|
+
checkLogCall({
|
|
246
|
+
context,
|
|
247
|
+
callExpr: prev,
|
|
248
|
+
expectedLevel: "info",
|
|
249
|
+
expectedMessage: `Success ${funcName}`,
|
|
250
|
+
funcName,
|
|
251
|
+
inputArgNames,
|
|
252
|
+
requireErrFirst: false,
|
|
253
|
+
errorIdentifierName: null,
|
|
254
|
+
where: "Success",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Failed branches inside if statements
|
|
260
|
+
if (stmt.type === "IfStatement") {
|
|
261
|
+
checkFailedBranch(context, stmt, funcName, inputArgNames);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!successFound) {
|
|
266
|
+
context.report({
|
|
267
|
+
node: tryBlock,
|
|
268
|
+
messageId: "trySuccessReturnMissing",
|
|
269
|
+
data: { funcName },
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function findPrecedingLoggerCall(body, returnIndex) {
|
|
275
|
+
for (let j = returnIndex - 1; j >= 0; j--) {
|
|
276
|
+
const s = body[j];
|
|
277
|
+
if (s.type === "IfStatement") continue;
|
|
278
|
+
if (
|
|
279
|
+
s.type === "ExpressionStatement" &&
|
|
280
|
+
s.expression.type === "AwaitExpression"
|
|
281
|
+
) {
|
|
282
|
+
// `await revalidatePath(...)` etc — keep walking
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (s.type === "ExpressionStatement") {
|
|
286
|
+
return s.expression;
|
|
287
|
+
}
|
|
288
|
+
if (s.type === "VariableDeclaration") continue;
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function checkFailedBranch(context, ifStmt, funcName, inputArgNames) {
|
|
295
|
+
// We only validate IFs that look like Failed branches: contain a return whose
|
|
296
|
+
// error.message is a string literal (not the catch's "An unexpected ..." literal).
|
|
297
|
+
const consequent = ifStmt.consequent;
|
|
298
|
+
if (consequent.type !== "BlockStatement") return;
|
|
299
|
+
const ret = consequent.body.find((s) => s.type === "ReturnStatement");
|
|
300
|
+
if (!ret) return;
|
|
301
|
+
const errMsg = getReturnErrorMessageLiteral(ret);
|
|
302
|
+
if (errMsg === null) return; // not a Failed-shaped return; skip
|
|
303
|
+
|
|
304
|
+
// Must have logger.error("Failed <funcName>") preceding the return
|
|
305
|
+
const idx = consequent.body.indexOf(ret);
|
|
306
|
+
let loggerCall = null;
|
|
307
|
+
for (let j = 0; j < idx; j++) {
|
|
308
|
+
const s = consequent.body[j];
|
|
309
|
+
if (
|
|
310
|
+
s.type === "ExpressionStatement" &&
|
|
311
|
+
isLoggerCall(s.expression, "error")
|
|
312
|
+
) {
|
|
313
|
+
loggerCall = s.expression;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (!loggerCall) {
|
|
318
|
+
context.report({
|
|
319
|
+
node: ret,
|
|
320
|
+
messageId: "failedLogMissing",
|
|
321
|
+
data: { funcName },
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const isPatternC = errMsg === "<non-literal>"; // .message access
|
|
327
|
+
checkLogCall({
|
|
328
|
+
context,
|
|
329
|
+
callExpr: loggerCall,
|
|
330
|
+
expectedLevel: "error",
|
|
331
|
+
expectedMessage: `Failed ${funcName}`,
|
|
332
|
+
funcName,
|
|
333
|
+
inputArgNames,
|
|
334
|
+
requireErrFirst: isPatternC,
|
|
335
|
+
errorIdentifierName: null,
|
|
336
|
+
where: "Failed",
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function checkCatchClause(context, handler, funcName, inputArgNames) {
|
|
341
|
+
const param = handler.param;
|
|
342
|
+
if (
|
|
343
|
+
!param ||
|
|
344
|
+
param.type !== "Identifier" ||
|
|
345
|
+
param.name !== "error" ||
|
|
346
|
+
!param.typeAnnotation ||
|
|
347
|
+
param.typeAnnotation.typeAnnotation?.type !== "TSUnknownKeyword"
|
|
348
|
+
) {
|
|
349
|
+
context.report({
|
|
350
|
+
node: param ?? handler,
|
|
351
|
+
messageId: "catchParamWrongType",
|
|
352
|
+
data: { funcName },
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
const block = handler.body;
|
|
356
|
+
if (block.body.length === 0) {
|
|
357
|
+
context.report({ node: block, messageId: "catchEmpty", data: { funcName } });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const first = block.body[0];
|
|
361
|
+
if (!isExpressionStatementWithLoggerCall(first, "error")) {
|
|
362
|
+
context.report({
|
|
363
|
+
node: first,
|
|
364
|
+
messageId: "catchMissingErrorLog",
|
|
365
|
+
data: { funcName },
|
|
366
|
+
});
|
|
367
|
+
} else {
|
|
368
|
+
checkLogCall({
|
|
369
|
+
context,
|
|
370
|
+
callExpr: first.expression,
|
|
371
|
+
expectedLevel: "error",
|
|
372
|
+
expectedMessage: `Unexpected error in ${funcName}`,
|
|
373
|
+
funcName,
|
|
374
|
+
inputArgNames,
|
|
375
|
+
requireErrFirst: true,
|
|
376
|
+
errorIdentifierName: "error",
|
|
377
|
+
where: "catch",
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Last statement must be a return whose error.message is the catch literal
|
|
382
|
+
const last = block.body[block.body.length - 1];
|
|
383
|
+
if (last?.type !== "ReturnStatement") {
|
|
384
|
+
context.report({
|
|
385
|
+
node: last ?? block,
|
|
386
|
+
messageId: "catchLastNotReturn",
|
|
387
|
+
data: { funcName },
|
|
388
|
+
});
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const msg = getReturnErrorMessageLiteral(last);
|
|
392
|
+
if (msg !== CATCH_RETURN_MESSAGE) {
|
|
393
|
+
context.report({
|
|
394
|
+
node: last,
|
|
395
|
+
messageId: "catchWrongReturnMessage",
|
|
396
|
+
data: { funcName, expected: CATCH_RETURN_MESSAGE, actual: msg ?? "<missing>" },
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export const entryTemplateRule = {
|
|
402
|
+
meta: {
|
|
403
|
+
type: "problem",
|
|
404
|
+
docs: {
|
|
405
|
+
description:
|
|
406
|
+
"Enforce the canonical try/catch + Start/Failed/Success/Unexpected logger structure for entries.",
|
|
407
|
+
},
|
|
408
|
+
messages: {
|
|
409
|
+
bodyNotTryCatch:
|
|
410
|
+
"entry '{{ funcName }}' must have a single try/catch as its body.",
|
|
411
|
+
tryEmpty: "entry '{{ funcName }}' try block is empty.",
|
|
412
|
+
tryMissingStartLog:
|
|
413
|
+
"entry '{{ funcName }}' try block must start with `logger.info(<obj>, \"Start {{ funcName }}\")`.",
|
|
414
|
+
trySuccessLogMissing:
|
|
415
|
+
"entry '{{ funcName }}' success return must be preceded by `logger.info(<obj>, \"Success {{ funcName }}\")`.",
|
|
416
|
+
trySuccessReturnMissing:
|
|
417
|
+
"entry '{{ funcName }}' must contain a success return `return { data, error: null }`.",
|
|
418
|
+
failedLogMissing:
|
|
419
|
+
"entry '{{ funcName }}' Failed branch must call `logger.error(<obj>, \"Failed {{ funcName }}\")` before return.",
|
|
420
|
+
catchParamWrongType:
|
|
421
|
+
"entry '{{ funcName }}' catch param must be `error: unknown`.",
|
|
422
|
+
catchEmpty: "entry '{{ funcName }}' catch block is empty.",
|
|
423
|
+
catchMissingErrorLog:
|
|
424
|
+
"entry '{{ funcName }}' catch block must start with `logger.error(<obj>, \"Unexpected error in {{ funcName }}\")`.",
|
|
425
|
+
catchLastNotReturn:
|
|
426
|
+
"entry '{{ funcName }}' catch block must end with a return statement.",
|
|
427
|
+
catchWrongReturnMessage:
|
|
428
|
+
"entry '{{ funcName }}' catch return error.message must be the literal '{{ expected }}'. Got: '{{ actual }}'.",
|
|
429
|
+
logWrongCallShape:
|
|
430
|
+
"{{ where }} log in '{{ funcName }}' must be `logger.{{ expectedLevel }}(<obj>, \"{{ expectedMessage }}\")`.",
|
|
431
|
+
logWrongMessage:
|
|
432
|
+
"{{ where }} log message must be '{{ expectedMessage }}'. Got: '{{ actual }}'.",
|
|
433
|
+
logFirstArgNotObject:
|
|
434
|
+
"{{ where }} log in '{{ funcName }}' first argument must be an object literal.",
|
|
435
|
+
logErrKeyNotFirst:
|
|
436
|
+
"{{ where }} log in '{{ funcName }}' object must start with `err:` key.",
|
|
437
|
+
logMissingInputArg:
|
|
438
|
+
"{{ where }} log in '{{ funcName }}' is missing input arg '{{ argName }}'. All function inputs must propagate to log objects.",
|
|
439
|
+
},
|
|
440
|
+
schema: [],
|
|
441
|
+
},
|
|
442
|
+
create(context) {
|
|
443
|
+
return {
|
|
444
|
+
ExportNamedDeclaration(node) {
|
|
445
|
+
if (!node.declaration) return;
|
|
446
|
+
const decl = node.declaration;
|
|
447
|
+
if (decl.type !== "FunctionDeclaration") return;
|
|
448
|
+
if (!decl.async) return;
|
|
449
|
+
if (!decl.id) return;
|
|
450
|
+
const funcName = decl.id.name;
|
|
451
|
+
const inputArgNames = getInputArgNames(decl.params);
|
|
452
|
+
|
|
453
|
+
const body = decl.body.body;
|
|
454
|
+
if (body.length !== 1 || body[0].type !== "TryStatement") {
|
|
455
|
+
context.report({
|
|
456
|
+
node: decl.id,
|
|
457
|
+
messageId: "bodyNotTryCatch",
|
|
458
|
+
data: { funcName },
|
|
459
|
+
});
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const tryStmt = body[0];
|
|
463
|
+
checkTryBlock(context, tryStmt.block, funcName, inputArgNames);
|
|
464
|
+
if (tryStmt.handler) {
|
|
465
|
+
checkCatchClause(context, tryStmt.handler, funcName, inputArgNames);
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
},
|
|
470
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { entryTemplateRule } from "./entry-template.mjs";
|
|
1
2
|
import { featureNameRule } from "./feature-name.mjs";
|
|
2
3
|
import { formStateNamingRule } from "./form-state-naming.mjs";
|
|
3
4
|
import { importPathStyleRule } from "./import-path-style.mjs";
|
|
@@ -13,6 +14,7 @@ import { supabaseSelectTypedColumnsRule } from "./supabase-select-typed-columns.
|
|
|
13
14
|
/** Single plugin object to avoid ESLint "Cannot redefine plugin" errors. */
|
|
14
15
|
export const localPlugin = {
|
|
15
16
|
rules: {
|
|
17
|
+
"entry-template": entryTemplateRule,
|
|
16
18
|
"feature-name": featureNameRule,
|
|
17
19
|
"form-state-naming": formStateNamingRule,
|
|
18
20
|
"import-path-style": importPathStyleRule,
|
package/src/common/naming.mjs
CHANGED
|
@@ -296,6 +296,15 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
|
|
|
296
296
|
},
|
|
297
297
|
});
|
|
298
298
|
|
|
299
|
+
configs.push({
|
|
300
|
+
name: "naming/entry-template",
|
|
301
|
+
files: featuresGlob(featureRoot, "**/entries/*.ts"),
|
|
302
|
+
plugins: { local: localPlugin },
|
|
303
|
+
rules: {
|
|
304
|
+
"local/entry-template": "error",
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
299
308
|
configs.push(
|
|
300
309
|
{
|
|
301
310
|
name: "naming/entries-shared",
|