@webpieces/dev-config 0.0.0-dev

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 (31) hide show
  1. package/README.md +306 -0
  2. package/bin/set-version.sh +86 -0
  3. package/bin/setup-claude-patterns.sh +51 -0
  4. package/bin/start.sh +107 -0
  5. package/bin/stop.sh +65 -0
  6. package/bin/use-local-webpieces.sh +89 -0
  7. package/bin/use-published-webpieces.sh +33 -0
  8. package/config/eslint/base.mjs +91 -0
  9. package/config/typescript/tsconfig.base.json +25 -0
  10. package/eslint-plugin/__tests__/catch-error-pattern.test.ts +360 -0
  11. package/eslint-plugin/__tests__/max-file-lines.test.ts +195 -0
  12. package/eslint-plugin/__tests__/max-method-lines.test.ts +246 -0
  13. package/eslint-plugin/index.d.ts +14 -0
  14. package/eslint-plugin/index.js +19 -0
  15. package/eslint-plugin/index.js.map +1 -0
  16. package/eslint-plugin/index.ts +18 -0
  17. package/eslint-plugin/rules/catch-error-pattern.d.ts +11 -0
  18. package/eslint-plugin/rules/catch-error-pattern.js +196 -0
  19. package/eslint-plugin/rules/catch-error-pattern.js.map +1 -0
  20. package/eslint-plugin/rules/catch-error-pattern.ts +281 -0
  21. package/eslint-plugin/rules/max-file-lines.d.ts +12 -0
  22. package/eslint-plugin/rules/max-file-lines.js +257 -0
  23. package/eslint-plugin/rules/max-file-lines.js.map +1 -0
  24. package/eslint-plugin/rules/max-file-lines.ts +272 -0
  25. package/eslint-plugin/rules/max-method-lines.d.ts +12 -0
  26. package/eslint-plugin/rules/max-method-lines.js +257 -0
  27. package/eslint-plugin/rules/max-method-lines.js.map +1 -0
  28. package/eslint-plugin/rules/max-method-lines.ts +304 -0
  29. package/package.json +54 -0
  30. package/patterns/CLAUDE.md +293 -0
  31. package/patterns/claude.patterns.md +798 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Tests for max-method-lines ESLint rule
3
+ */
4
+
5
+ import { RuleTester } from 'eslint';
6
+ import rule from '../rules/max-method-lines';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+
10
+ // Use require to load parser at runtime (avoids TypeScript import issues)
11
+ const tsParser = require('@typescript-eslint/parser');
12
+
13
+ const ruleTester = new RuleTester({
14
+ languageOptions: {
15
+ parser: tsParser,
16
+ parserOptions: {
17
+ ecmaVersion: 2020,
18
+ sourceType: 'module',
19
+ },
20
+ },
21
+ });
22
+
23
+ ruleTester.run('max-method-lines', rule, {
24
+ valid: [
25
+ // Short function (well under limit)
26
+ {
27
+ code: `function shortFunc() {
28
+ return 42;
29
+ }`,
30
+ },
31
+ // Function with exactly 70 lines (default limit)
32
+ {
33
+ code: `function exactlySeventyLines() {
34
+ ${Array(68)
35
+ .fill(0)
36
+ .map((_, i) => ` const line${i} = ${i};`)
37
+ .join('\n')}
38
+ }`,
39
+ },
40
+ // Function with 69 lines (just under default limit)
41
+ {
42
+ code: `function sixtyNineLines() {
43
+ ${Array(67)
44
+ .fill(0)
45
+ .map((_, i) => ` const line${i} = ${i};`)
46
+ .join('\n')}
47
+ }`,
48
+ },
49
+ // Custom limit: 10 lines
50
+ {
51
+ code: `function shortFunc() {
52
+ const a = 1;
53
+ const b = 2;
54
+ const c = 3;
55
+ const d = 4;
56
+ const e = 5;
57
+ const f = 6;
58
+ const g = 7;
59
+ return a + b + c + d + e + f + g;
60
+ }`,
61
+ options: [{ max: 10 }],
62
+ },
63
+ // Arrow function under limit
64
+ {
65
+ code: `const shortArrow = () => {
66
+ return 42;
67
+ };`,
68
+ },
69
+ // Method definition under limit
70
+ {
71
+ code: `class MyClass {
72
+ shortMethod() {
73
+ return 42;
74
+ }
75
+ }`,
76
+ },
77
+ // Function expression under limit
78
+ {
79
+ code: `const func = function() {
80
+ return 42;
81
+ };`,
82
+ },
83
+ ],
84
+
85
+ invalid: [
86
+ // Function with 71 lines (exceeds default limit)
87
+ {
88
+ code: `function tooLong() {
89
+ ${Array(69)
90
+ .fill(0)
91
+ .map((_, i) => ` const line${i} = ${i};`)
92
+ .join('\n')}
93
+ }`,
94
+ errors: [
95
+ {
96
+ messageId: 'tooLong',
97
+ data: { name: 'tooLong', actual: '71', max: '70' },
98
+ },
99
+ ],
100
+ },
101
+ // Function with 100 lines (way over limit)
102
+ {
103
+ code: `function wayTooLong() {
104
+ ${Array(98)
105
+ .fill(0)
106
+ .map((_, i) => ` const line${i} = ${i};`)
107
+ .join('\n')}
108
+ }`,
109
+ errors: [
110
+ {
111
+ messageId: 'tooLong',
112
+ data: { name: 'wayTooLong', actual: '100', max: '70' },
113
+ },
114
+ ],
115
+ },
116
+ // Custom limit: exceed 5 lines
117
+ {
118
+ code: `function tooLongForCustom() {
119
+ const a = 1;
120
+ const b = 2;
121
+ const c = 3;
122
+ const d = 4;
123
+ return a + b + c + d;
124
+ }`,
125
+ options: [{ max: 5 }],
126
+ errors: [
127
+ {
128
+ messageId: 'tooLong',
129
+ data: { name: 'tooLongForCustom', actual: '7', max: '5' },
130
+ },
131
+ ],
132
+ },
133
+ // Arrow function exceeding limit
134
+ {
135
+ code: `const tooLongArrow = () => {
136
+ ${Array(69)
137
+ .fill(0)
138
+ .map((_, i) => ` const line${i} = ${i};`)
139
+ .join('\n')}
140
+ };`,
141
+ errors: [
142
+ {
143
+ messageId: 'tooLong',
144
+ data: { name: 'anonymous', actual: '71', max: '70' },
145
+ },
146
+ ],
147
+ },
148
+ // Method definition exceeding limit
149
+ {
150
+ code: `class MyClass {
151
+ tooLongMethod() {
152
+ ${Array(69)
153
+ .fill(0)
154
+ .map((_, i) => ` const line${i} = ${i};`)
155
+ .join('\n')}
156
+ }
157
+ }`,
158
+ errors: [
159
+ {
160
+ messageId: 'tooLong',
161
+ data: { name: 'tooLongMethod', actual: '71', max: '70' },
162
+ },
163
+ ],
164
+ },
165
+ // Function expression exceeding limit
166
+ {
167
+ code: `const func = function tooLongFunc() {
168
+ ${Array(69)
169
+ .fill(0)
170
+ .map((_, i) => ` const line${i} = ${i};`)
171
+ .join('\n')}
172
+ };`,
173
+ errors: [
174
+ {
175
+ messageId: 'tooLong',
176
+ data: { name: 'tooLongFunc', actual: '71', max: '70' },
177
+ },
178
+ ],
179
+ },
180
+ // Multiple functions, one exceeds limit
181
+ {
182
+ code: `function shortFunc() {
183
+ return 42;
184
+ }
185
+
186
+ function tooLong() {
187
+ ${Array(69)
188
+ .fill(0)
189
+ .map((_, i) => ` const line${i} = ${i};`)
190
+ .join('\n')}
191
+ }
192
+
193
+ function anotherShort() {
194
+ return 24;
195
+ }`,
196
+ errors: [
197
+ {
198
+ messageId: 'tooLong',
199
+ data: { name: 'tooLong', actual: '71', max: '70' },
200
+ },
201
+ ],
202
+ },
203
+ ],
204
+ });
205
+
206
+ console.log('✅ All max-method-lines rule tests passed!');
207
+
208
+ // Test documentation file creation
209
+ const docPath = path.join(process.cwd(), 'tmp', 'webpieces', 'webpieces.methods.md');
210
+
211
+ // Run a test that triggers violation (will create doc file)
212
+ try {
213
+ ruleTester.run('max-method-lines-doc-test', rule, {
214
+ valid: [],
215
+ invalid: [
216
+ {
217
+ code: `function veryLongMethod() {
218
+ ${Array(100)
219
+ .fill(0)
220
+ .map((_, i) => ` const line${i} = ${i};`)
221
+ .join('\n')}
222
+ }`,
223
+ errors: [{ messageId: 'tooLong' }],
224
+ },
225
+ ],
226
+ });
227
+ } catch (err: any) {
228
+ //const error = toError(err);
229
+ // Test may fail due to too many errors, but file should be created
230
+ }
231
+
232
+ // Verify file was created
233
+ if (!fs.existsSync(docPath)) {
234
+ throw new Error('Documentation file was not created at ' + docPath);
235
+ }
236
+
237
+ // Verify content has AI directive
238
+ const content = fs.readFileSync(docPath, 'utf-8');
239
+ if (!content.includes('READ THIS FILE to fix methods that are too long')) {
240
+ throw new Error('Documentation file missing AI directive');
241
+ }
242
+ if (!content.includes('TABLE OF CONTENTS')) {
243
+ throw new Error('Documentation file missing table of contents principle');
244
+ }
245
+
246
+ console.log('✅ Documentation file creation test passed!');
@@ -0,0 +1,14 @@
1
+ /**
2
+ * ESLint plugin for WebPieces
3
+ * Provides rules for enforcing WebPieces code patterns
4
+ *
5
+ * This plugin is automatically included in @webpieces/dev-config
6
+ */
7
+ declare const _default: {
8
+ rules: {
9
+ 'catch-error-pattern': import("eslint").Rule.RuleModule;
10
+ 'max-method-lines': import("eslint").Rule.RuleModule;
11
+ 'max-file-lines': import("eslint").Rule.RuleModule;
12
+ };
13
+ };
14
+ export = _default;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ /**
3
+ * ESLint plugin for WebPieces
4
+ * Provides rules for enforcing WebPieces code patterns
5
+ *
6
+ * This plugin is automatically included in @webpieces/dev-config
7
+ */
8
+ const tslib_1 = require("tslib");
9
+ const catch_error_pattern_1 = tslib_1.__importDefault(require("./rules/catch-error-pattern"));
10
+ const max_method_lines_1 = tslib_1.__importDefault(require("./rules/max-method-lines"));
11
+ const max_file_lines_1 = tslib_1.__importDefault(require("./rules/max-file-lines"));
12
+ module.exports = {
13
+ rules: {
14
+ 'catch-error-pattern': catch_error_pattern_1.default,
15
+ 'max-method-lines': max_method_lines_1.default,
16
+ 'max-file-lines': max_file_lines_1.default,
17
+ },
18
+ };
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/tooling/dev-config/eslint-plugin/index.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;AAEH,8FAA4D;AAC5D,wFAAsD;AACtD,oFAAkD;AAElD,iBAAS;IACL,KAAK,EAAE;QACH,qBAAqB,EAAE,6BAAiB;QACxC,kBAAkB,EAAE,0BAAc;QAClC,gBAAgB,EAAE,wBAAY;KACjC;CACJ,CAAC","sourcesContent":["/**\n * ESLint plugin for WebPieces\n * Provides rules for enforcing WebPieces code patterns\n *\n * This plugin is automatically included in @webpieces/dev-config\n */\n\nimport catchErrorPattern from './rules/catch-error-pattern';\nimport maxMethodLines from './rules/max-method-lines';\nimport maxFileLines from './rules/max-file-lines';\n\nexport = {\n rules: {\n 'catch-error-pattern': catchErrorPattern,\n 'max-method-lines': maxMethodLines,\n 'max-file-lines': maxFileLines,\n },\n};\n"]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * ESLint plugin for WebPieces
3
+ * Provides rules for enforcing WebPieces code patterns
4
+ *
5
+ * This plugin is automatically included in @webpieces/dev-config
6
+ */
7
+
8
+ import catchErrorPattern from './rules/catch-error-pattern';
9
+ import maxMethodLines from './rules/max-method-lines';
10
+ import maxFileLines from './rules/max-file-lines';
11
+
12
+ export = {
13
+ rules: {
14
+ 'catch-error-pattern': catchErrorPattern,
15
+ 'max-method-lines': maxMethodLines,
16
+ 'max-file-lines': maxFileLines,
17
+ },
18
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ESLint rule to enforce standardized catch block error handling patterns
3
+ *
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); }
8
+ */
9
+ import type { Rule } from 'eslint';
10
+ declare const rule: Rule.RuleModule;
11
+ export = rule;
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ /**
3
+ * ESLint rule to enforce standardized catch block error handling patterns
4
+ *
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); }
9
+ */
10
+ const rule = {
11
+ meta: {
12
+ type: 'problem',
13
+ docs: {
14
+ description: 'Enforce standardized catch block error handling patterns',
15
+ category: 'Best Practices',
16
+ recommended: true,
17
+ url: 'https://github.com/deanhiller/webpieces-ts/blob/main/claude.patterns.md#error-handling-pattern',
18
+ },
19
+ messages: {
20
+ missingToError: 'Catch block must call toError({{param}}) as first statement or comment it out to explicitly ignore errors',
21
+ wrongVariableName: 'Error variable must be named "{{expected}}", got "{{actual}}"',
22
+ missingTypeAnnotation: 'Catch parameter must be typed as "any": catch ({{param}}: any)',
23
+ wrongParameterName: 'Catch parameter must be named "err" (or "err2", "err3" for nested catches), got "{{actual}}"',
24
+ toErrorNotFirst: 'toError({{param}}) must be the first statement in the catch block',
25
+ },
26
+ fixable: undefined,
27
+ schema: [],
28
+ },
29
+ create(context) {
30
+ // Track nesting depth for err, err2, err3, etc.
31
+ const catchStack = [];
32
+ return {
33
+ CatchClause(node) {
34
+ const catchNode = node;
35
+ // Calculate depth (1-based: first catch is depth 1)
36
+ const depth = catchStack.length + 1;
37
+ catchStack.push(catchNode);
38
+ // Build expected names based on depth
39
+ const suffix = depth === 1 ? '' : String(depth);
40
+ const expectedParamName = 'err' + suffix;
41
+ const expectedVarName = 'error' + suffix;
42
+ // Get the catch parameter
43
+ const param = catchNode.param;
44
+ if (!param) {
45
+ // No parameter - unusual but technically valid (though not our pattern)
46
+ context.report({
47
+ node: catchNode,
48
+ messageId: 'missingTypeAnnotation',
49
+ data: { param: expectedParamName },
50
+ });
51
+ return;
52
+ }
53
+ // Track the actual parameter name for validation (may differ from expected)
54
+ 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;
79
+ 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!
89
+ return;
90
+ }
91
+ // Now check if body is empty (after checking for commented pattern)
92
+ 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
+ });
185
+ return;
186
+ }
187
+ // All checks passed! ✅
188
+ },
189
+ 'CatchClause:exit'() {
190
+ catchStack.pop();
191
+ },
192
+ };
193
+ },
194
+ };
195
+ module.exports = rule;
196
+ //# sourceMappingURL=catch-error-pattern.js.map
@@ -0,0 +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"]}