@webpieces/dev-config 0.2.93 → 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.
Files changed (29) hide show
  1. package/architecture/executors/validate-no-any-unknown/executor.js +2 -2
  2. package/architecture/executors/validate-no-any-unknown/executor.js.map +1 -1
  3. package/architecture/executors/validate-no-any-unknown/executor.ts +2 -2
  4. package/architecture/executors/validate-no-skiplevel-deps/executor.js.map +1 -1
  5. package/architecture/executors/validate-no-skiplevel-deps/executor.ts +2 -2
  6. package/eslint-plugin/__tests__/catch-error-pattern.test.ts +41 -26
  7. package/eslint-plugin/__tests__/no-unmanaged-exceptions.test.ts +20 -20
  8. package/eslint-plugin/rules/catch-error-pattern.d.ts +3 -3
  9. package/eslint-plugin/rules/catch-error-pattern.js +84 -137
  10. package/eslint-plugin/rules/catch-error-pattern.js.map +1 -1
  11. package/eslint-plugin/rules/catch-error-pattern.ts +123 -158
  12. package/eslint-plugin/rules/enforce-architecture.js +3 -3
  13. package/eslint-plugin/rules/enforce-architecture.js.map +1 -1
  14. package/eslint-plugin/rules/enforce-architecture.ts +8 -8
  15. package/eslint-plugin/rules/max-file-lines.js +3 -3
  16. package/eslint-plugin/rules/max-file-lines.js.map +1 -1
  17. package/eslint-plugin/rules/max-file-lines.ts +5 -5
  18. package/eslint-plugin/rules/max-method-lines.js +3 -3
  19. package/eslint-plugin/rules/max-method-lines.js.map +1 -1
  20. package/eslint-plugin/rules/max-method-lines.ts +5 -5
  21. package/executors/validate-eslint-sync/executor.d.ts +3 -2
  22. package/executors/validate-eslint-sync/executor.js.map +1 -1
  23. package/executors/validate-eslint-sync/executor.ts +6 -2
  24. package/executors/validate-versions-locked/executor.js +3 -2
  25. package/executors/validate-versions-locked/executor.js.map +1 -1
  26. package/executors/validate-versions-locked/executor.ts +5 -4
  27. package/package.json +1 -1
  28. package/src/generators/init/generator.js.map +1 -1
  29. 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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
306
+ } catch (err2: unknown) {
307
307
  const error2 = toError(err2);
308
308
  }
309
- } catch (err: any) {
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: any) {
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: any) {
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: any) { const error = toError(err); }
6
- * 2. Ignored: catch (err: any) { //const error = toError(err); }
7
- * 3. Nested: catch (err2: any) { const error2 = toError(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: any) { const error = toError(err); }
7
- * 2. Ignored: catch (err: any) { //const error = toError(err); }
8
- * 3. Nested: catch (err2: any) { const error2 = toError(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 "any": catch ({{param}}: any)',
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
- // RULE 1: Parameter must be named correctly (err, err2, err3, etc.)
56
- if (param.type === 'Identifier' && param.name !== expectedParamName) {
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
- // IMPORTANT: Check for commented ignore pattern FIRST (before checking if body is empty)
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
- // Now check if body is empty (after checking for commented pattern)
129
+ const body = catchNode.body.body;
92
130
  if (body.length === 0) {
93
- // Empty catch block without comment - not allowed
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
- // All checks passed!
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":";AAAA;;;;;;;GAOG;AAqDH,SAAS,iBAAiB,CACtB,OAAyB,EACzB,KAAqB,EACrB,iBAAyB;IAEzB,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,IAAI,KAAK,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;QAClE,OAAO,CAAC,MAAM,CAAC;YACX,IAAI,EAAE,KAAK;YACX,SAAS,EAAE,oBAAoB;YAC/B,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,EAAE;SAC/B,CAAC,CAAC;IACP,CAAC;AACL,CAAC;AAED,SAAS,sBAAsB,CAC3B,OAAyB,EACzB,KAAqB,EACrB,iBAAyB;IAEzB,IACI,CAAC,KAAK,CAAC,cAAc;QACrB,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc;QACpC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,IAAI,KAAK,kBAAkB,EACjE,CAAC;QACC,OAAO,CAAC,MAAM,CAAC;YACX,IAAI,EAAE,KAAK;YACX,SAAS,EAAE,uBAAuB;YAClC,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,iBAAiB,EAAE;SACnD,CAAC,CAAC;IACP,CAAC;AACL,CAAC;AAED,SAAS,gBAAgB,CACrB,SAA0B,EAC1B,UAA0C,EAC1C,eAAuB,EACvB,eAAuB;IAEvB,MAAM,eAAe,GAAG,SAAS,CAAC,IAAI,CAAC,KAAM,CAAC,CAAC,CAAC,CAAC;IACjD,MAAM,aAAa,GAAG,SAAS,CAAC,IAAI,CAAC,KAAM,CAAC,CAAC,CAAC,CAAC;IAC/C,MAAM,cAAc,GAAG,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,eAAe,EAAE,aAAa,CAAC,CAAC;IAEjF,MAAM,aAAa,GAAG,IAAI,MAAM,CAC5B,kBAAkB,eAAe,sBAAsB,eAAe,KAAK,CAC9E,CAAC;IAEF,OAAO,aAAa,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;AAC9C,CAAC;AAED,SAAS,oBAAoB,CACzB,OAAyB;AACzB,8EAA8E;AAC9E,IAAS,EACT,SAAiB;IAEjB,OAAO,CAAC,MAAM,CAAC;QACX,IAAI;QACJ,SAAS,EAAE,gBAAgB;QAC3B,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE;KAC7B,CAAC,CAAC;AACP,CAAC;AAED,SAAS,mBAAmB,CACxB,OAAyB;AACzB,8EAA8E;AAC9E,cAAmB,EACnB,iBAAyB,EACzB,eAAuB,EACvB,eAAuB;IAEvB,IAAI,cAAc,CAAC,IAAI,KAAK,qBAAqB,EAAE,CAAC;QAChD,oBAAoB,CAAC,OAAO,EAAE,cAAc,EAAE,iBAAiB,CAAC,CAAC;QACjE,OAAO;IACX,CAAC;IAED,MAAM,OAAO,GAAG,cAAyC,CAAC;IAC1D,MAAM,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAC5C,IAAI,CAAC,WAAW,EAAE,CAAC;QACf,oBAAoB,CAAC,OAAO,EAAE,cAAc,EAAE,iBAAiB,CAAC,CAAC;QACjE,OAAO;IACX,CAAC;IAED,IAAI,WAAW,CAAC,EAAE,CAAC,IAAI,KAAK,YAAY,IAAI,WAAW,CAAC,EAAE,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;QAClF,OAAO,CAAC,MAAM,CAAC;YACX,IAAI,EAAE,WAAW,CAAC,EAAE;YACpB,SAAS,EAAE,mBAAmB;YAC9B,IAAI,EAAE,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,CAAC,IAAI,IAAI,SAAS,EAAE;SAChF,CAAC,CAAC;QACH,OAAO;IACX,CAAC;IAED,IAAI,CAAC,WAAW,CAAC,IAAI,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;QAClE,oBAAoB,CAAC,OAAO,EAAE,WAAW,CAAC,IAAI,IAAI,WAAW,EAAE,iBAAiB,CAAC,CAAC;QAClF,OAAO;IACX,CAAC;IAED,MAAM,QAAQ,GAAG,WAAW,CAAC,IAA0B,CAAC;IACxD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;IAC/B,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5D,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,iBAAiB,CAAC,CAAC;QACzD,OAAO;IACX,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,SAAS,CAAC;IAChC,IACI,IAAI,CAAC,MAAM,KAAK,CAAC;QACjB,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,YAAY;QAC5B,IAAI,CAAC,CAAC,CAAoB,CAAC,IAAI,KAAK,eAAe,EACtD,CAAC;QACC,oBAAoB,CAAC,OAAO,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC;IAC7D,CAAC;AACL,CAAC;AAED,MAAM,IAAI,GAAoB;IAC1B,IAAI,EAAE;QACF,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACF,WAAW,EAAE,0DAA0D;YACvE,QAAQ,EAAE,gBAAgB;YAC1B,WAAW,EAAE,IAAI;YACjB,GAAG,EAAE,gGAAgG;SACxG;QACD,QAAQ,EAAE;YACN,cAAc,EACV,2GAA2G;YAC/G,iBAAiB,EAAE,+DAA+D;YAClF,qBAAqB,EAAE,wEAAwE;YAC/F,kBAAkB,EACd,8FAA8F;YAClG,eAAe,EAAE,mEAAmE;SACvF;QACD,OAAO,EAAE,SAAS;QAClB,MAAM,EAAE,EAAE;KACb;IAED,MAAM,CAAC,OAAyB;QAC5B,MAAM,UAAU,GAAsB,EAAE,CAAC;QAEzC,OAAO;YACH,WAAW,CAAC,IAAS;gBACjB,MAAM,SAAS,GAAG,IAAuB,CAAC;gBAC1C,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC;gBACpC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBAE3B,MAAM,MAAM,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAChD,MAAM,iBAAiB,GAAG,KAAK,GAAG,MAAM,CAAC;gBACzC,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,CAAC;gBAEzC,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC;gBAC9B,IAAI,CAAC,KAAK,EAAE,CAAC;oBACT,OAAO,CAAC,MAAM,CAAC;wBACX,IAAI,EAAE,SAAS;wBACf,SAAS,EAAE,uBAAuB;wBAClC,IAAI,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE;qBACrC,CAAC,CAAC;oBACH,OAAO;gBACX,CAAC;gBAED,MAAM,eAAe,GACjB,KAAK,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,iBAAiB,CAAC;gBAEjE,iBAAiB,CAAC,OAAO,EAAE,KAAK,EAAE,iBAAiB,CAAC,CAAC;gBACrD,sBAAsB,CAAC,OAAO,EAAE,KAAK,EAAE,iBAAiB,CAAC,CAAC;gBAE1D,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;gBACjE,IAAI,gBAAgB,CAAC,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,CAAC,EAAE,CAAC;oBAC5E,OAAO;gBACX,CAAC;gBAED,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;gBACjC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBACpB,oBAAoB,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;oBACjE,OAAO;gBACX,CAAC;gBAED,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,eAAe,EAAE,eAAe,CAAC,CAAC;YAC/F,CAAC;YAED,kBAAkB;gBACd,UAAU,CAAC,GAAG,EAAE,CAAC;YACrB,CAAC;SACJ,CAAC;IACN,CAAC;CACJ,CAAC;AAEF,iBAAS,IAAI,CAAC","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"]}