@webpieces/dev-config 0.2.94 → 0.2.95
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/architecture/executors/validate-no-any-unknown/executor.js +2 -2
- package/architecture/executors/validate-no-any-unknown/executor.js.map +1 -1
- package/architecture/executors/validate-no-any-unknown/executor.ts +2 -2
- package/architecture/executors/validate-no-skiplevel-deps/executor.js.map +1 -1
- package/architecture/executors/validate-no-skiplevel-deps/executor.ts +2 -2
- package/eslint-plugin/__tests__/catch-error-pattern.test.ts +41 -26
- package/eslint-plugin/__tests__/no-unmanaged-exceptions.test.ts +20 -20
- package/eslint-plugin/rules/catch-error-pattern.d.ts +3 -3
- package/eslint-plugin/rules/catch-error-pattern.js +84 -137
- package/eslint-plugin/rules/catch-error-pattern.js.map +1 -1
- package/eslint-plugin/rules/catch-error-pattern.ts +123 -158
- package/eslint-plugin/rules/enforce-architecture.js +3 -3
- package/eslint-plugin/rules/enforce-architecture.js.map +1 -1
- package/eslint-plugin/rules/enforce-architecture.ts +8 -8
- package/eslint-plugin/rules/max-file-lines.js +3 -3
- package/eslint-plugin/rules/max-file-lines.js.map +1 -1
- package/eslint-plugin/rules/max-file-lines.ts +5 -5
- package/eslint-plugin/rules/max-method-lines.js +3 -3
- package/eslint-plugin/rules/max-method-lines.js.map +1 -1
- package/eslint-plugin/rules/max-method-lines.ts +5 -5
- package/executors/validate-eslint-sync/executor.d.ts +3 -2
- package/executors/validate-eslint-sync/executor.js.map +1 -1
- package/executors/validate-eslint-sync/executor.ts +6 -2
- package/executors/validate-versions-locked/executor.js +3 -2
- package/executors/validate-versions-locked/executor.js.map +1 -1
- package/executors/validate-versions-locked/executor.ts +5 -4
- package/package.json +1 -1
- package/src/generators/init/generator.js.map +1 -1
- package/templates/webpieces.exceptions.md +15 -15
|
@@ -31,7 +31,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
31
31
|
code: `
|
|
32
32
|
try {
|
|
33
33
|
await operation();
|
|
34
|
-
} catch (err:
|
|
34
|
+
} catch (err: unknown) {
|
|
35
35
|
const error = toError(err);
|
|
36
36
|
expect(error).toBeDefined();
|
|
37
37
|
}
|
|
@@ -43,7 +43,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
43
43
|
try {
|
|
44
44
|
await controller.save(request);
|
|
45
45
|
fail('Should have thrown');
|
|
46
|
-
} catch (err:
|
|
46
|
+
} catch (err: unknown) {
|
|
47
47
|
const error = toError(err);
|
|
48
48
|
expect(error.message).toContain('Invalid');
|
|
49
49
|
}
|
|
@@ -56,7 +56,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
56
56
|
code: `
|
|
57
57
|
try {
|
|
58
58
|
await operation();
|
|
59
|
-
} catch (err:
|
|
59
|
+
} catch (err: unknown) {
|
|
60
60
|
const error = toError(err);
|
|
61
61
|
}
|
|
62
62
|
`,
|
|
@@ -68,7 +68,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
68
68
|
try {
|
|
69
69
|
await service.process();
|
|
70
70
|
fail();
|
|
71
|
-
} catch (err:
|
|
71
|
+
} catch (err: unknown) {
|
|
72
72
|
const error = toError(err);
|
|
73
73
|
expect(error).toBeDefined();
|
|
74
74
|
}
|
|
@@ -82,7 +82,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
82
82
|
code: `
|
|
83
83
|
try {
|
|
84
84
|
await operation();
|
|
85
|
-
} catch (err:
|
|
85
|
+
} catch (err: unknown) {
|
|
86
86
|
const error = toError(err);
|
|
87
87
|
}
|
|
88
88
|
`,
|
|
@@ -92,7 +92,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
92
92
|
code: `
|
|
93
93
|
try {
|
|
94
94
|
await runIntegrationTest();
|
|
95
|
-
} catch (err:
|
|
95
|
+
} catch (err: unknown) {
|
|
96
96
|
const error = toError(err);
|
|
97
97
|
console.error(error);
|
|
98
98
|
}
|
|
@@ -103,7 +103,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
103
103
|
code: `
|
|
104
104
|
try {
|
|
105
105
|
const result = performAction();
|
|
106
|
-
} catch (err:
|
|
106
|
+
} catch (err: unknown) {
|
|
107
107
|
const error = toError(err);
|
|
108
108
|
}
|
|
109
109
|
`,
|
|
@@ -158,7 +158,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
158
158
|
code: `
|
|
159
159
|
try {
|
|
160
160
|
await operation();
|
|
161
|
-
} catch (err:
|
|
161
|
+
} catch (err: unknown) {
|
|
162
162
|
const error = toError(err);
|
|
163
163
|
}
|
|
164
164
|
`,
|
|
@@ -172,7 +172,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
172
172
|
try {
|
|
173
173
|
const result = await this.service.save(request);
|
|
174
174
|
return result;
|
|
175
|
-
} catch (err:
|
|
175
|
+
} catch (err: unknown) {
|
|
176
176
|
const error = toError(err);
|
|
177
177
|
throw error;
|
|
178
178
|
}
|
|
@@ -190,7 +190,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
190
190
|
code: `
|
|
191
191
|
try {
|
|
192
192
|
await this.database.query(sql);
|
|
193
|
-
} catch (err:
|
|
193
|
+
} catch (err: unknown) {
|
|
194
194
|
const error = toError(err);
|
|
195
195
|
}
|
|
196
196
|
`,
|
|
@@ -204,7 +204,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
204
204
|
try {
|
|
205
205
|
const user = await this.db.findOne({ id });
|
|
206
206
|
return user;
|
|
207
|
-
} catch (err:
|
|
207
|
+
} catch (err: unknown) {
|
|
208
208
|
const error = toError(err);
|
|
209
209
|
console.error('Failed to fetch user:', error);
|
|
210
210
|
return null;
|
|
@@ -223,7 +223,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
223
223
|
code: `
|
|
224
224
|
try {
|
|
225
225
|
return JSON.parse(body);
|
|
226
|
-
} catch (err:
|
|
226
|
+
} catch (err: unknown) {
|
|
227
227
|
const error = toError(err);
|
|
228
228
|
}
|
|
229
229
|
`,
|
|
@@ -236,7 +236,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
236
236
|
async filter(meta: MethodMeta, next: NextFilter): Promise<Action> {
|
|
237
237
|
try {
|
|
238
238
|
return await next.execute();
|
|
239
|
-
} catch (err:
|
|
239
|
+
} catch (err: unknown) {
|
|
240
240
|
const error = toError(err);
|
|
241
241
|
console.error('Filter error:', error);
|
|
242
242
|
throw error;
|
|
@@ -257,7 +257,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
257
257
|
try {
|
|
258
258
|
const response = await fetch(url);
|
|
259
259
|
return await response.json();
|
|
260
|
-
} catch (err:
|
|
260
|
+
} catch (err: unknown) {
|
|
261
261
|
const error = toError(err);
|
|
262
262
|
throw new Error(\`Fetch failed: \${error.message}\`);
|
|
263
263
|
}
|
|
@@ -275,7 +275,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
275
275
|
async function operation1() {
|
|
276
276
|
try {
|
|
277
277
|
await doSomething();
|
|
278
|
-
} catch (err:
|
|
278
|
+
} catch (err: unknown) {
|
|
279
279
|
const error = toError(err);
|
|
280
280
|
}
|
|
281
281
|
}
|
|
@@ -283,7 +283,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
283
283
|
async function operation2() {
|
|
284
284
|
try {
|
|
285
285
|
await doSomethingElse();
|
|
286
|
-
} catch (err:
|
|
286
|
+
} catch (err: unknown) {
|
|
287
287
|
const error = toError(err);
|
|
288
288
|
}
|
|
289
289
|
}
|
|
@@ -303,10 +303,10 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
303
303
|
try {
|
|
304
304
|
try {
|
|
305
305
|
await operation();
|
|
306
|
-
} catch (err2:
|
|
306
|
+
} catch (err2: unknown) {
|
|
307
307
|
const error2 = toError(err2);
|
|
308
308
|
}
|
|
309
|
-
} catch (err:
|
|
309
|
+
} catch (err: unknown) {
|
|
310
310
|
const error = toError(err);
|
|
311
311
|
}
|
|
312
312
|
`,
|
|
@@ -325,7 +325,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
325
325
|
const handler = async () => {
|
|
326
326
|
try {
|
|
327
327
|
await doSomething();
|
|
328
|
-
} catch (err:
|
|
328
|
+
} catch (err: unknown) {
|
|
329
329
|
const error = toError(err);
|
|
330
330
|
}
|
|
331
331
|
};
|
|
@@ -343,7 +343,7 @@ ruleTester.run('no-unmanaged-exceptions', rule, {
|
|
|
343
343
|
process(data: Data): Result {
|
|
344
344
|
try {
|
|
345
345
|
return this.performProcessing(data);
|
|
346
|
-
} catch (err:
|
|
346
|
+
} catch (err: unknown) {
|
|
347
347
|
const error = toError(err);
|
|
348
348
|
return null;
|
|
349
349
|
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* ESLint rule to enforce standardized catch block error handling patterns
|
|
3
3
|
*
|
|
4
4
|
* Enforces three approved patterns:
|
|
5
|
-
* 1. Standard: catch (err:
|
|
6
|
-
* 2. Ignored: catch (err:
|
|
7
|
-
* 3. Nested: catch (err2:
|
|
5
|
+
* 1. Standard: catch (err: unknown) { const error = toError(err); }
|
|
6
|
+
* 2. Ignored: catch (err: unknown) { //const error = toError(err); }
|
|
7
|
+
* 3. Nested: catch (err2: unknown) { const error2 = toError(err2); }
|
|
8
8
|
*/
|
|
9
9
|
import type { Rule } from 'eslint';
|
|
10
10
|
declare const rule: Rule.RuleModule;
|
|
@@ -3,10 +3,84 @@
|
|
|
3
3
|
* ESLint rule to enforce standardized catch block error handling patterns
|
|
4
4
|
*
|
|
5
5
|
* Enforces three approved patterns:
|
|
6
|
-
* 1. Standard: catch (err:
|
|
7
|
-
* 2. Ignored: catch (err:
|
|
8
|
-
* 3. Nested: catch (err2:
|
|
6
|
+
* 1. Standard: catch (err: unknown) { const error = toError(err); }
|
|
7
|
+
* 2. Ignored: catch (err: unknown) { //const error = toError(err); }
|
|
8
|
+
* 3. Nested: catch (err2: unknown) { const error2 = toError(err2); }
|
|
9
9
|
*/
|
|
10
|
+
function validateParamName(context, param, expectedParamName) {
|
|
11
|
+
if (param.type === 'Identifier' && param.name !== expectedParamName) {
|
|
12
|
+
context.report({
|
|
13
|
+
node: param,
|
|
14
|
+
messageId: 'wrongParameterName',
|
|
15
|
+
data: { actual: param.name },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function validateTypeAnnotation(context, param, expectedParamName) {
|
|
20
|
+
if (!param.typeAnnotation ||
|
|
21
|
+
!param.typeAnnotation.typeAnnotation ||
|
|
22
|
+
param.typeAnnotation.typeAnnotation.type !== 'TSUnknownKeyword') {
|
|
23
|
+
context.report({
|
|
24
|
+
node: param,
|
|
25
|
+
messageId: 'missingTypeAnnotation',
|
|
26
|
+
data: { param: param.name || expectedParamName },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function hasIgnoreComment(catchNode, sourceCode, expectedVarName, actualParamName) {
|
|
31
|
+
const catchBlockStart = catchNode.body.range[0];
|
|
32
|
+
const catchBlockEnd = catchNode.body.range[1];
|
|
33
|
+
const catchBlockText = sourceCode.text.substring(catchBlockStart, catchBlockEnd);
|
|
34
|
+
const ignorePattern = new RegExp(`//\\s*const\\s+${expectedVarName}\\s*=\\s*toError\\(${actualParamName}\\)`);
|
|
35
|
+
return ignorePattern.test(catchBlockText);
|
|
36
|
+
}
|
|
37
|
+
function reportMissingToError(context,
|
|
38
|
+
// webpieces-disable no-any-unknown -- ESLint AST node param requires any type
|
|
39
|
+
node, paramName) {
|
|
40
|
+
context.report({
|
|
41
|
+
node,
|
|
42
|
+
messageId: 'missingToError',
|
|
43
|
+
data: { param: paramName },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function validateToErrorCall(context,
|
|
47
|
+
// webpieces-disable no-any-unknown -- ESLint AST node param requires any type
|
|
48
|
+
firstStatement, expectedParamName, expectedVarName, actualParamName) {
|
|
49
|
+
if (firstStatement.type !== 'VariableDeclaration') {
|
|
50
|
+
reportMissingToError(context, firstStatement, expectedParamName);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const varDecl = firstStatement;
|
|
54
|
+
const declaration = varDecl.declarations[0];
|
|
55
|
+
if (!declaration) {
|
|
56
|
+
reportMissingToError(context, firstStatement, expectedParamName);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (declaration.id.type !== 'Identifier' || declaration.id.name !== expectedVarName) {
|
|
60
|
+
context.report({
|
|
61
|
+
node: declaration.id,
|
|
62
|
+
messageId: 'wrongVariableName',
|
|
63
|
+
data: { expected: expectedVarName, actual: declaration.id.name || 'unknown' },
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!declaration.init || declaration.init.type !== 'CallExpression') {
|
|
68
|
+
reportMissingToError(context, declaration.init || declaration, expectedParamName);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const callExpr = declaration.init;
|
|
72
|
+
const callee = callExpr.callee;
|
|
73
|
+
if (callee.type !== 'Identifier' || callee.name !== 'toError') {
|
|
74
|
+
reportMissingToError(context, callee, expectedParamName);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const args = callExpr.arguments;
|
|
78
|
+
if (args.length !== 1 ||
|
|
79
|
+
args[0].type !== 'Identifier' ||
|
|
80
|
+
args[0].name !== actualParamName) {
|
|
81
|
+
reportMissingToError(context, callExpr, actualParamName);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
10
84
|
const rule = {
|
|
11
85
|
meta: {
|
|
12
86
|
type: 'problem',
|
|
@@ -19,7 +93,7 @@ const rule = {
|
|
|
19
93
|
messages: {
|
|
20
94
|
missingToError: 'Catch block must call toError({{param}}) as first statement or comment it out to explicitly ignore errors',
|
|
21
95
|
wrongVariableName: 'Error variable must be named "{{expected}}", got "{{actual}}"',
|
|
22
|
-
missingTypeAnnotation: 'Catch parameter must be typed as "
|
|
96
|
+
missingTypeAnnotation: 'Catch parameter must be typed as "unknown": catch ({{param}}: unknown)',
|
|
23
97
|
wrongParameterName: 'Catch parameter must be named "err" (or "err2", "err3" for nested catches), got "{{actual}}"',
|
|
24
98
|
toErrorNotFirst: 'toError({{param}}) must be the first statement in the catch block',
|
|
25
99
|
},
|
|
@@ -27,22 +101,17 @@ const rule = {
|
|
|
27
101
|
schema: [],
|
|
28
102
|
},
|
|
29
103
|
create(context) {
|
|
30
|
-
// Track nesting depth for err, err2, err3, etc.
|
|
31
104
|
const catchStack = [];
|
|
32
105
|
return {
|
|
33
106
|
CatchClause(node) {
|
|
34
107
|
const catchNode = node;
|
|
35
|
-
// Calculate depth (1-based: first catch is depth 1)
|
|
36
108
|
const depth = catchStack.length + 1;
|
|
37
109
|
catchStack.push(catchNode);
|
|
38
|
-
// Build expected names based on depth
|
|
39
110
|
const suffix = depth === 1 ? '' : String(depth);
|
|
40
111
|
const expectedParamName = 'err' + suffix;
|
|
41
112
|
const expectedVarName = 'error' + suffix;
|
|
42
|
-
// Get the catch parameter
|
|
43
113
|
const param = catchNode.param;
|
|
44
114
|
if (!param) {
|
|
45
|
-
// No parameter - unusual but technically valid (though not our pattern)
|
|
46
115
|
context.report({
|
|
47
116
|
node: catchNode,
|
|
48
117
|
messageId: 'missingTypeAnnotation',
|
|
@@ -50,141 +119,19 @@ const rule = {
|
|
|
50
119
|
});
|
|
51
120
|
return;
|
|
52
121
|
}
|
|
53
|
-
// Track the actual parameter name for validation (may differ from expected)
|
|
54
122
|
const actualParamName = param.type === 'Identifier' ? param.name : expectedParamName;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
context.report({
|
|
58
|
-
node: param,
|
|
59
|
-
messageId: 'wrongParameterName',
|
|
60
|
-
data: {
|
|
61
|
-
actual: param.name,
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
// RULE 2: Must have type annotation ": any"
|
|
66
|
-
if (!param.typeAnnotation ||
|
|
67
|
-
!param.typeAnnotation.typeAnnotation ||
|
|
68
|
-
param.typeAnnotation.typeAnnotation.type !== 'TSAnyKeyword') {
|
|
69
|
-
context.report({
|
|
70
|
-
node: param,
|
|
71
|
-
messageId: 'missingTypeAnnotation',
|
|
72
|
-
data: {
|
|
73
|
-
param: param.name || expectedParamName,
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
// RULE 3: Check first statement in catch block
|
|
78
|
-
const body = catchNode.body.body;
|
|
123
|
+
validateParamName(context, param, expectedParamName);
|
|
124
|
+
validateTypeAnnotation(context, param, expectedParamName);
|
|
79
125
|
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
80
|
-
|
|
81
|
-
// This allows Pattern 2 (empty catch with only comment) to be valid
|
|
82
|
-
// Look for: //const error = toError(err);
|
|
83
|
-
const catchBlockStart = catchNode.body.range[0];
|
|
84
|
-
const catchBlockEnd = catchNode.body.range[1];
|
|
85
|
-
const catchBlockText = sourceCode.text.substring(catchBlockStart, catchBlockEnd);
|
|
86
|
-
const ignorePattern = new RegExp(`//\\s*const\\s+${expectedVarName}\\s*=\\s*toError\\(${actualParamName}\\)`);
|
|
87
|
-
if (ignorePattern.test(catchBlockText)) {
|
|
88
|
-
// Pattern 2: Explicitly ignored - valid!
|
|
126
|
+
if (hasIgnoreComment(catchNode, sourceCode, expectedVarName, actualParamName)) {
|
|
89
127
|
return;
|
|
90
128
|
}
|
|
91
|
-
|
|
129
|
+
const body = catchNode.body.body;
|
|
92
130
|
if (body.length === 0) {
|
|
93
|
-
|
|
94
|
-
context.report({
|
|
95
|
-
node: catchNode.body,
|
|
96
|
-
messageId: 'missingToError',
|
|
97
|
-
data: {
|
|
98
|
-
param: expectedParamName,
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
const firstStatement = body[0];
|
|
104
|
-
// Check if first statement is: const error = toError(err)
|
|
105
|
-
if (firstStatement.type !== 'VariableDeclaration') {
|
|
106
|
-
context.report({
|
|
107
|
-
node: firstStatement,
|
|
108
|
-
messageId: 'missingToError',
|
|
109
|
-
data: {
|
|
110
|
-
param: expectedParamName,
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const varDecl = firstStatement;
|
|
116
|
-
const declaration = varDecl.declarations[0];
|
|
117
|
-
if (!declaration) {
|
|
118
|
-
context.report({
|
|
119
|
-
node: firstStatement,
|
|
120
|
-
messageId: 'missingToError',
|
|
121
|
-
data: {
|
|
122
|
-
param: expectedParamName,
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
// Check variable name
|
|
128
|
-
if (declaration.id.type !== 'Identifier' ||
|
|
129
|
-
declaration.id.name !== expectedVarName) {
|
|
130
|
-
context.report({
|
|
131
|
-
node: declaration.id,
|
|
132
|
-
messageId: 'wrongVariableName',
|
|
133
|
-
data: {
|
|
134
|
-
expected: expectedVarName,
|
|
135
|
-
actual: declaration.id.name || 'unknown',
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
// Check initialization: toError(err)
|
|
141
|
-
if (!declaration.init) {
|
|
142
|
-
context.report({
|
|
143
|
-
node: declaration,
|
|
144
|
-
messageId: 'missingToError',
|
|
145
|
-
data: {
|
|
146
|
-
param: expectedParamName,
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
if (declaration.init.type !== 'CallExpression') {
|
|
152
|
-
context.report({
|
|
153
|
-
node: declaration.init,
|
|
154
|
-
messageId: 'missingToError',
|
|
155
|
-
data: {
|
|
156
|
-
param: expectedParamName,
|
|
157
|
-
},
|
|
158
|
-
});
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
const callExpr = declaration.init;
|
|
162
|
-
const callee = callExpr.callee;
|
|
163
|
-
if (callee.type !== 'Identifier' || callee.name !== 'toError') {
|
|
164
|
-
context.report({
|
|
165
|
-
node: callee,
|
|
166
|
-
messageId: 'missingToError',
|
|
167
|
-
data: {
|
|
168
|
-
param: expectedParamName,
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
// Check argument: must be the catch parameter (use actual param name)
|
|
174
|
-
const args = callExpr.arguments;
|
|
175
|
-
if (args.length !== 1 ||
|
|
176
|
-
args[0].type !== 'Identifier' ||
|
|
177
|
-
args[0].name !== actualParamName) {
|
|
178
|
-
context.report({
|
|
179
|
-
node: callExpr,
|
|
180
|
-
messageId: 'missingToError',
|
|
181
|
-
data: {
|
|
182
|
-
param: actualParamName,
|
|
183
|
-
},
|
|
184
|
-
});
|
|
131
|
+
reportMissingToError(context, catchNode.body, expectedParamName);
|
|
185
132
|
return;
|
|
186
133
|
}
|
|
187
|
-
|
|
134
|
+
validateToErrorCall(context, body[0], expectedParamName, expectedVarName, actualParamName);
|
|
188
135
|
},
|
|
189
136
|
'CatchClause:exit'() {
|
|
190
137
|
catchStack.pop();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catch-error-pattern.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/eslint-plugin/rules/catch-error-pattern.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESLint rule to enforce standardized catch block error handling patterns\n *\n * Enforces three approved patterns:\n * 1. Standard: catch (err: any) { const error = toError(err); }\n * 2. Ignored: catch (err: any) { //const error = toError(err); }\n * 3. Nested: catch (err2: any) { const error2 = toError(err2); }\n */\n\nimport type { Rule } from 'eslint';\n\n// Using any for ESTree nodes to avoid complex type gymnastics\n// ESLint rules work with dynamic AST nodes anyway\ninterface CatchClauseNode {\n type: 'CatchClause';\n param?: IdentifierNode | null;\n body: BlockStatementNode;\n [key: string]: any;\n}\n\ninterface IdentifierNode {\n type: 'Identifier';\n name: string;\n typeAnnotation?: TypeAnnotationNode;\n [key: string]: any;\n}\n\ninterface TypeAnnotationNode {\n typeAnnotation?: {\n type: string;\n };\n}\n\ninterface BlockStatementNode {\n type: 'BlockStatement';\n body: any[];\n range: [number, number];\n [key: string]: any;\n}\n\ninterface VariableDeclarationNode {\n type: 'VariableDeclaration';\n declarations: VariableDeclaratorNode[];\n [key: string]: any;\n}\n\ninterface VariableDeclaratorNode {\n type: 'VariableDeclarator';\n id: IdentifierNode;\n init?: CallExpressionNode | null;\n [key: string]: any;\n}\n\ninterface CallExpressionNode {\n type: 'CallExpression';\n callee: IdentifierNode;\n arguments: any[];\n [key: string]: any;\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce standardized catch block error handling patterns',\n category: 'Best Practices',\n recommended: true,\n url: 'https://github.com/deanhiller/webpieces-ts/blob/main/claude.patterns.md#error-handling-pattern',\n },\n messages: {\n missingToError:\n 'Catch block must call toError({{param}}) as first statement or comment it out to explicitly ignore errors',\n wrongVariableName: 'Error variable must be named \"{{expected}}\", got \"{{actual}}\"',\n missingTypeAnnotation: 'Catch parameter must be typed as \"any\": catch ({{param}}: any)',\n wrongParameterName:\n 'Catch parameter must be named \"err\" (or \"err2\", \"err3\" for nested catches), got \"{{actual}}\"',\n toErrorNotFirst: 'toError({{param}}) must be the first statement in the catch block',\n },\n fixable: undefined,\n schema: [],\n },\n\n create(context: Rule.RuleContext): Rule.RuleListener {\n // Track nesting depth for err, err2, err3, etc.\n const catchStack: CatchClauseNode[] = [];\n\n return {\n CatchClause(node: any): void {\n const catchNode = node as CatchClauseNode;\n\n // Calculate depth (1-based: first catch is depth 1)\n const depth = catchStack.length + 1;\n catchStack.push(catchNode);\n\n // Build expected names based on depth\n const suffix = depth === 1 ? '' : String(depth);\n const expectedParamName = 'err' + suffix;\n const expectedVarName = 'error' + suffix;\n\n // Get the catch parameter\n const param = catchNode.param;\n if (!param) {\n // No parameter - unusual but technically valid (though not our pattern)\n context.report({\n node: catchNode,\n messageId: 'missingTypeAnnotation',\n data: { param: expectedParamName },\n });\n return;\n }\n\n // Track the actual parameter name for validation (may differ from expected)\n const actualParamName =\n param.type === 'Identifier' ? param.name : expectedParamName;\n\n // RULE 1: Parameter must be named correctly (err, err2, err3, etc.)\n if (param.type === 'Identifier' && param.name !== expectedParamName) {\n context.report({\n node: param,\n messageId: 'wrongParameterName',\n data: {\n actual: param.name,\n },\n });\n }\n\n // RULE 2: Must have type annotation \": any\"\n if (\n !param.typeAnnotation ||\n !param.typeAnnotation.typeAnnotation ||\n param.typeAnnotation.typeAnnotation.type !== 'TSAnyKeyword'\n ) {\n context.report({\n node: param,\n messageId: 'missingTypeAnnotation',\n data: {\n param: param.name || expectedParamName,\n },\n });\n }\n\n // RULE 3: Check first statement in catch block\n const body = catchNode.body.body;\n const sourceCode = context.sourceCode || context.getSourceCode();\n\n // IMPORTANT: Check for commented ignore pattern FIRST (before checking if body is empty)\n // This allows Pattern 2 (empty catch with only comment) to be valid\n // Look for: //const error = toError(err);\n const catchBlockStart = catchNode.body.range![0];\n const catchBlockEnd = catchNode.body.range![1];\n const catchBlockText = sourceCode.text.substring(catchBlockStart, catchBlockEnd);\n\n const ignorePattern = new RegExp(\n `//\\\\s*const\\\\s+${expectedVarName}\\\\s*=\\\\s*toError\\\\(${actualParamName}\\\\)`,\n );\n\n if (ignorePattern.test(catchBlockText)) {\n // Pattern 2: Explicitly ignored - valid!\n return;\n }\n\n // Now check if body is empty (after checking for commented pattern)\n if (body.length === 0) {\n // Empty catch block without comment - not allowed\n context.report({\n node: catchNode.body,\n messageId: 'missingToError',\n data: {\n param: expectedParamName,\n },\n });\n return;\n }\n\n const firstStatement = body[0];\n\n // Check if first statement is: const error = toError(err)\n if (firstStatement.type !== 'VariableDeclaration') {\n context.report({\n node: firstStatement,\n messageId: 'missingToError',\n data: {\n param: expectedParamName,\n },\n });\n return;\n }\n\n const varDecl = firstStatement as VariableDeclarationNode;\n const declaration = varDecl.declarations[0];\n if (!declaration) {\n context.report({\n node: firstStatement,\n messageId: 'missingToError',\n data: {\n param: expectedParamName,\n },\n });\n return;\n }\n\n // Check variable name\n if (\n declaration.id.type !== 'Identifier' ||\n declaration.id.name !== expectedVarName\n ) {\n context.report({\n node: declaration.id,\n messageId: 'wrongVariableName',\n data: {\n expected: expectedVarName,\n actual: declaration.id.name || 'unknown',\n },\n });\n return;\n }\n\n // Check initialization: toError(err)\n if (!declaration.init) {\n context.report({\n node: declaration,\n messageId: 'missingToError',\n data: {\n param: expectedParamName,\n },\n });\n return;\n }\n\n if (declaration.init.type !== 'CallExpression') {\n context.report({\n node: declaration.init,\n messageId: 'missingToError',\n data: {\n param: expectedParamName,\n },\n });\n return;\n }\n\n const callExpr = declaration.init as CallExpressionNode;\n const callee = callExpr.callee;\n if (callee.type !== 'Identifier' || callee.name !== 'toError') {\n context.report({\n node: callee,\n messageId: 'missingToError',\n data: {\n param: expectedParamName,\n },\n });\n return;\n }\n\n // Check argument: must be the catch parameter (use actual param name)\n const args = callExpr.arguments;\n if (\n args.length !== 1 ||\n args[0].type !== 'Identifier' ||\n (args[0] as IdentifierNode).name !== actualParamName\n ) {\n context.report({\n node: callExpr,\n messageId: 'missingToError',\n data: {\n param: actualParamName,\n },\n });\n return;\n }\n\n // All checks passed! ✅\n },\n\n 'CatchClause:exit'(): void {\n catchStack.pop();\n },\n };\n },\n};\n\nexport = rule;\n"]}
|
|
1
|
+
{"version":3,"file":"catch-error-pattern.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/eslint-plugin/rules/catch-error-pattern.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESLint rule to enforce standardized catch block error handling patterns\n *\n * Enforces three approved patterns:\n * 1. Standard: catch (err: unknown) { const error = toError(err); }\n * 2. Ignored: catch (err: unknown) { //const error = toError(err); }\n * 3. Nested: catch (err2: unknown) { const error2 = toError(err2); }\n */\n\nimport type { Rule } from 'eslint';\n\n// Using any for ESTree nodes to avoid complex type gymnastics\n// ESLint rules work with dynamic AST nodes anyway\ninterface CatchClauseNode {\n type: 'CatchClause';\n param?: IdentifierNode | null;\n body: BlockStatementNode;\n [key: string]: any;\n}\n\ninterface IdentifierNode {\n type: 'Identifier';\n name: string;\n typeAnnotation?: TypeAnnotationNode;\n [key: string]: any;\n}\n\ninterface TypeAnnotationNode {\n typeAnnotation?: {\n type: string;\n };\n}\n\ninterface BlockStatementNode {\n type: 'BlockStatement';\n body: any[];\n range: [number, number];\n [key: string]: any;\n}\n\ninterface VariableDeclarationNode {\n type: 'VariableDeclaration';\n declarations: VariableDeclaratorNode[];\n [key: string]: any;\n}\n\ninterface VariableDeclaratorNode {\n type: 'VariableDeclarator';\n id: IdentifierNode;\n init?: CallExpressionNode | null;\n [key: string]: any;\n}\n\ninterface CallExpressionNode {\n type: 'CallExpression';\n callee: IdentifierNode;\n arguments: any[];\n [key: string]: any;\n}\n\nfunction validateParamName(\n context: Rule.RuleContext,\n param: IdentifierNode,\n expectedParamName: string,\n): void {\n if (param.type === 'Identifier' && param.name !== expectedParamName) {\n context.report({\n node: param,\n messageId: 'wrongParameterName',\n data: { actual: param.name },\n });\n }\n}\n\nfunction validateTypeAnnotation(\n context: Rule.RuleContext,\n param: IdentifierNode,\n expectedParamName: string,\n): void {\n if (\n !param.typeAnnotation ||\n !param.typeAnnotation.typeAnnotation ||\n param.typeAnnotation.typeAnnotation.type !== 'TSUnknownKeyword'\n ) {\n context.report({\n node: param,\n messageId: 'missingTypeAnnotation',\n data: { param: param.name || expectedParamName },\n });\n }\n}\n\nfunction hasIgnoreComment(\n catchNode: CatchClauseNode,\n sourceCode: Rule.RuleContext['sourceCode'],\n expectedVarName: string,\n actualParamName: string,\n): boolean {\n const catchBlockStart = catchNode.body.range![0];\n const catchBlockEnd = catchNode.body.range![1];\n const catchBlockText = sourceCode.text.substring(catchBlockStart, catchBlockEnd);\n\n const ignorePattern = new RegExp(\n `//\\\\s*const\\\\s+${expectedVarName}\\\\s*=\\\\s*toError\\\\(${actualParamName}\\\\)`,\n );\n\n return ignorePattern.test(catchBlockText);\n}\n\nfunction reportMissingToError(\n context: Rule.RuleContext,\n // webpieces-disable no-any-unknown -- ESLint AST node param requires any type\n node: any,\n paramName: string,\n): void {\n context.report({\n node,\n messageId: 'missingToError',\n data: { param: paramName },\n });\n}\n\nfunction validateToErrorCall(\n context: Rule.RuleContext,\n // webpieces-disable no-any-unknown -- ESLint AST node param requires any type\n firstStatement: any,\n expectedParamName: string,\n expectedVarName: string,\n actualParamName: string,\n): void {\n if (firstStatement.type !== 'VariableDeclaration') {\n reportMissingToError(context, firstStatement, expectedParamName);\n return;\n }\n\n const varDecl = firstStatement as VariableDeclarationNode;\n const declaration = varDecl.declarations[0];\n if (!declaration) {\n reportMissingToError(context, firstStatement, expectedParamName);\n return;\n }\n\n if (declaration.id.type !== 'Identifier' || declaration.id.name !== expectedVarName) {\n context.report({\n node: declaration.id,\n messageId: 'wrongVariableName',\n data: { expected: expectedVarName, actual: declaration.id.name || 'unknown' },\n });\n return;\n }\n\n if (!declaration.init || declaration.init.type !== 'CallExpression') {\n reportMissingToError(context, declaration.init || declaration, expectedParamName);\n return;\n }\n\n const callExpr = declaration.init as CallExpressionNode;\n const callee = callExpr.callee;\n if (callee.type !== 'Identifier' || callee.name !== 'toError') {\n reportMissingToError(context, callee, expectedParamName);\n return;\n }\n\n const args = callExpr.arguments;\n if (\n args.length !== 1 ||\n args[0].type !== 'Identifier' ||\n (args[0] as IdentifierNode).name !== actualParamName\n ) {\n reportMissingToError(context, callExpr, actualParamName);\n }\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce standardized catch block error handling patterns',\n category: 'Best Practices',\n recommended: true,\n url: 'https://github.com/deanhiller/webpieces-ts/blob/main/claude.patterns.md#error-handling-pattern',\n },\n messages: {\n missingToError:\n 'Catch block must call toError({{param}}) as first statement or comment it out to explicitly ignore errors',\n wrongVariableName: 'Error variable must be named \"{{expected}}\", got \"{{actual}}\"',\n missingTypeAnnotation: 'Catch parameter must be typed as \"unknown\": catch ({{param}}: unknown)',\n wrongParameterName:\n 'Catch parameter must be named \"err\" (or \"err2\", \"err3\" for nested catches), got \"{{actual}}\"',\n toErrorNotFirst: 'toError({{param}}) must be the first statement in the catch block',\n },\n fixable: undefined,\n schema: [],\n },\n\n create(context: Rule.RuleContext): Rule.RuleListener {\n const catchStack: CatchClauseNode[] = [];\n\n return {\n CatchClause(node: any): void {\n const catchNode = node as CatchClauseNode;\n const depth = catchStack.length + 1;\n catchStack.push(catchNode);\n\n const suffix = depth === 1 ? '' : String(depth);\n const expectedParamName = 'err' + suffix;\n const expectedVarName = 'error' + suffix;\n\n const param = catchNode.param;\n if (!param) {\n context.report({\n node: catchNode,\n messageId: 'missingTypeAnnotation',\n data: { param: expectedParamName },\n });\n return;\n }\n\n const actualParamName =\n param.type === 'Identifier' ? param.name : expectedParamName;\n\n validateParamName(context, param, expectedParamName);\n validateTypeAnnotation(context, param, expectedParamName);\n\n const sourceCode = context.sourceCode || context.getSourceCode();\n if (hasIgnoreComment(catchNode, sourceCode, expectedVarName, actualParamName)) {\n return;\n }\n\n const body = catchNode.body.body;\n if (body.length === 0) {\n reportMissingToError(context, catchNode.body, expectedParamName);\n return;\n }\n\n validateToErrorCall(context, body[0], expectedParamName, expectedVarName, actualParamName);\n },\n\n 'CatchClause:exit'(): void {\n catchStack.pop();\n },\n };\n },\n};\n\nexport = rule;\n"]}
|