@webpieces/dev-config 0.0.0-dev → 0.2.21
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/generate/executor.d.ts +17 -0
- package/architecture/executors/generate/executor.js +67 -0
- package/architecture/executors/generate/executor.js.map +1 -0
- package/architecture/executors/generate/executor.ts +83 -0
- package/architecture/executors/generate/schema.json +14 -0
- package/architecture/executors/validate-architecture-unchanged/executor.d.ts +17 -0
- package/architecture/executors/validate-architecture-unchanged/executor.js +65 -0
- package/architecture/executors/validate-architecture-unchanged/executor.js.map +1 -0
- package/architecture/executors/validate-architecture-unchanged/executor.ts +81 -0
- package/architecture/executors/validate-architecture-unchanged/schema.json +14 -0
- package/architecture/executors/validate-no-cycles/executor.d.ts +16 -0
- package/architecture/executors/validate-no-cycles/executor.js +48 -0
- package/architecture/executors/validate-no-cycles/executor.js.map +1 -0
- package/architecture/executors/validate-no-cycles/executor.ts +60 -0
- package/architecture/executors/validate-no-cycles/schema.json +8 -0
- package/architecture/executors/validate-no-skiplevel-deps/executor.d.ts +19 -0
- package/architecture/executors/validate-no-skiplevel-deps/executor.js +227 -0
- package/architecture/executors/validate-no-skiplevel-deps/executor.js.map +1 -0
- package/architecture/executors/validate-no-skiplevel-deps/executor.ts +267 -0
- package/architecture/executors/validate-no-skiplevel-deps/schema.json +8 -0
- package/architecture/executors/visualize/executor.d.ts +17 -0
- package/architecture/executors/visualize/executor.js +49 -0
- package/architecture/executors/visualize/executor.js.map +1 -0
- package/architecture/executors/visualize/executor.ts +63 -0
- package/architecture/executors/visualize/schema.json +14 -0
- package/architecture/index.d.ts +19 -0
- package/architecture/index.js +23 -0
- package/architecture/index.js.map +1 -0
- package/architecture/index.ts +20 -0
- package/architecture/lib/graph-comparator.d.ts +39 -0
- package/architecture/lib/graph-comparator.js +100 -0
- package/architecture/lib/graph-comparator.js.map +1 -0
- package/architecture/lib/graph-comparator.ts +141 -0
- package/architecture/lib/graph-generator.d.ts +19 -0
- package/architecture/lib/graph-generator.js +88 -0
- package/architecture/lib/graph-generator.js.map +1 -0
- package/architecture/lib/graph-generator.ts +102 -0
- package/architecture/lib/graph-loader.d.ts +31 -0
- package/architecture/lib/graph-loader.js +70 -0
- package/architecture/lib/graph-loader.js.map +1 -0
- package/architecture/lib/graph-loader.ts +82 -0
- package/architecture/lib/graph-sorter.d.ts +37 -0
- package/architecture/lib/graph-sorter.js +110 -0
- package/architecture/lib/graph-sorter.js.map +1 -0
- package/architecture/lib/graph-sorter.ts +137 -0
- package/architecture/lib/graph-visualizer.d.ts +29 -0
- package/architecture/lib/graph-visualizer.js +209 -0
- package/architecture/lib/graph-visualizer.js.map +1 -0
- package/architecture/lib/graph-visualizer.ts +222 -0
- package/architecture/lib/package-validator.d.ts +38 -0
- package/architecture/lib/package-validator.js +105 -0
- package/architecture/lib/package-validator.js.map +1 -0
- package/architecture/lib/package-validator.ts +144 -0
- package/config/eslint/base.mjs +6 -0
- package/eslint-plugin/__tests__/catch-error-pattern.test.ts +0 -1
- package/eslint-plugin/__tests__/max-file-lines.test.ts +29 -17
- package/eslint-plugin/__tests__/max-method-lines.test.ts +27 -15
- package/eslint-plugin/__tests__/no-unmanaged-exceptions.test.ts +359 -0
- package/eslint-plugin/index.d.ts +9 -0
- package/eslint-plugin/index.js +11 -0
- package/eslint-plugin/index.js.map +1 -1
- package/eslint-plugin/index.ts +11 -0
- package/eslint-plugin/rules/enforce-architecture.d.ts +15 -0
- package/eslint-plugin/rules/enforce-architecture.js +406 -0
- package/eslint-plugin/rules/enforce-architecture.js.map +1 -0
- package/eslint-plugin/rules/enforce-architecture.ts +469 -0
- package/eslint-plugin/rules/max-file-lines.js +11 -11
- package/eslint-plugin/rules/max-file-lines.js.map +1 -1
- package/eslint-plugin/rules/max-file-lines.ts +11 -11
- package/eslint-plugin/rules/max-method-lines.js +71 -88
- package/eslint-plugin/rules/max-method-lines.js.map +1 -1
- package/eslint-plugin/rules/max-method-lines.ts +85 -102
- package/eslint-plugin/rules/no-unmanaged-exceptions.d.ts +22 -0
- package/eslint-plugin/rules/no-unmanaged-exceptions.js +605 -0
- package/eslint-plugin/rules/no-unmanaged-exceptions.js.map +1 -0
- package/eslint-plugin/rules/no-unmanaged-exceptions.ts +621 -0
- package/executors.json +29 -0
- package/package.json +13 -7
- package/plugins/circular-deps/index.d.ts +8 -0
- package/plugins/circular-deps/index.js +14 -0
- package/plugins/circular-deps/index.js.map +1 -0
- package/plugins/circular-deps/index.ts +9 -0
- package/plugins/circular-deps/plugin.d.ts +32 -0
- package/plugins/circular-deps/plugin.js +73 -0
- package/plugins/circular-deps/plugin.js.map +1 -0
- package/plugins/circular-deps/plugin.ts +83 -0
|
@@ -25,14 +25,14 @@ Every method should read like a TABLE OF CONTENTS of a book.
|
|
|
25
25
|
|
|
26
26
|
### Pattern 1: Extract Loop Bodies
|
|
27
27
|
\`\`\`typescript
|
|
28
|
-
//
|
|
28
|
+
// BAD: 50 lines embedded in loop
|
|
29
29
|
for (const order of orders) {
|
|
30
30
|
// 20 lines of validation logic
|
|
31
31
|
// 15 lines of processing logic
|
|
32
32
|
// 10 lines of notification logic
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
//
|
|
35
|
+
// GOOD: Extracted to named methods
|
|
36
36
|
for (const order of orders) {
|
|
37
37
|
validateOrder(order);
|
|
38
38
|
processOrderItems(order);
|
|
@@ -42,11 +42,11 @@ for (const order of orders) {
|
|
|
42
42
|
|
|
43
43
|
### Pattern 2: Try-Catch Wrapper for Exception Handling
|
|
44
44
|
\`\`\`typescript
|
|
45
|
-
//
|
|
45
|
+
// GOOD: Separates success path from error handling
|
|
46
46
|
async function handleRequest(req: Request): Promise<Response> {
|
|
47
47
|
try {
|
|
48
48
|
return await executeRequest(req);
|
|
49
|
-
} catch (err:
|
|
49
|
+
} catch (err: unknown) {
|
|
50
50
|
const error = toError(err);
|
|
51
51
|
return createErrorResponse(error);
|
|
52
52
|
}
|
|
@@ -55,7 +55,7 @@ async function handleRequest(req: Request): Promise<Response> {
|
|
|
55
55
|
|
|
56
56
|
### Pattern 3: Sequential Method Calls (Table of Contents)
|
|
57
57
|
\`\`\`typescript
|
|
58
|
-
//
|
|
58
|
+
// GOOD: Self-documenting steps
|
|
59
59
|
function processOrder(order: Order): void {
|
|
60
60
|
validateOrderData(order);
|
|
61
61
|
calculateTotals(order);
|
|
@@ -68,22 +68,22 @@ function processOrder(order: Order): void {
|
|
|
68
68
|
|
|
69
69
|
### Pattern 4: Separate Data Object Creation
|
|
70
70
|
\`\`\`typescript
|
|
71
|
-
//
|
|
71
|
+
// BAD: 15 lines of inline object creation
|
|
72
72
|
doSomething({ field1: ..., field2: ..., field3: ..., /* 15 more fields */ });
|
|
73
73
|
|
|
74
|
-
//
|
|
74
|
+
// GOOD: Extract to factory method
|
|
75
75
|
const request = createRequestObject(data);
|
|
76
76
|
doSomething(request);
|
|
77
77
|
\`\`\`
|
|
78
78
|
|
|
79
79
|
### Pattern 5: Extract Inline Logic to Named Functions
|
|
80
80
|
\`\`\`typescript
|
|
81
|
-
//
|
|
81
|
+
// BAD: Complex inline logic
|
|
82
82
|
if (user.role === 'admin' && user.permissions.includes('write') && !user.suspended) {
|
|
83
83
|
// 30 lines of admin logic
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
//
|
|
86
|
+
// GOOD: Extract to named methods
|
|
87
87
|
if (isAdminWithWriteAccess(user)) {
|
|
88
88
|
performAdminOperation(user);
|
|
89
89
|
}
|
|
@@ -99,12 +99,12 @@ if (isAdminWithWriteAccess(user)) {
|
|
|
99
99
|
6. **TEST** that functionality remains unchanged
|
|
100
100
|
|
|
101
101
|
## Examples of "Logical Units" to Extract
|
|
102
|
-
- Validation logic
|
|
103
|
-
- Data transformation
|
|
104
|
-
- API calls
|
|
105
|
-
- Object creation
|
|
106
|
-
- Loop bodies
|
|
107
|
-
- Error handling
|
|
102
|
+
- Validation logic -> \`validateX()\`
|
|
103
|
+
- Data transformation -> \`transformXToY()\`
|
|
104
|
+
- API calls -> \`fetchXFromApi()\`
|
|
105
|
+
- Object creation -> \`createX()\`
|
|
106
|
+
- Loop bodies -> \`processItem()\`
|
|
107
|
+
- Error handling -> \`handleXError()\`
|
|
108
108
|
|
|
109
109
|
Remember: Methods should read like a table of contents. Each line should be a "chapter title" (method call) that describes what happens, not how it happens.
|
|
110
110
|
`;
|
|
@@ -113,7 +113,6 @@ let methodDocCreated = false;
|
|
|
113
113
|
function getWorkspaceRoot(context) {
|
|
114
114
|
const filename = context.filename || context.getFilename();
|
|
115
115
|
let dir = path.dirname(filename);
|
|
116
|
-
// Walk up directory tree to find workspace root
|
|
117
116
|
while (dir !== path.dirname(dir)) {
|
|
118
117
|
const pkgPath = path.join(dir, 'package.json');
|
|
119
118
|
if (fs.existsSync(pkgPath)) {
|
|
@@ -125,12 +124,12 @@ function getWorkspaceRoot(context) {
|
|
|
125
124
|
}
|
|
126
125
|
catch (err) {
|
|
127
126
|
//const error = toError(err);
|
|
128
|
-
|
|
127
|
+
void err;
|
|
129
128
|
}
|
|
130
129
|
}
|
|
131
130
|
dir = path.dirname(dir);
|
|
132
131
|
}
|
|
133
|
-
return process.cwd();
|
|
132
|
+
return process.cwd();
|
|
134
133
|
}
|
|
135
134
|
function ensureDocFile(docPath, content) {
|
|
136
135
|
try {
|
|
@@ -140,20 +139,66 @@ function ensureDocFile(docPath, content) {
|
|
|
140
139
|
}
|
|
141
140
|
catch (err) {
|
|
142
141
|
//const error = toError(err);
|
|
143
|
-
//
|
|
142
|
+
// err is used in console.warn below
|
|
144
143
|
console.warn(`[webpieces] Could not create doc file: ${docPath}`, err);
|
|
145
144
|
return false;
|
|
146
145
|
}
|
|
147
146
|
}
|
|
148
147
|
function ensureMethodDoc(context) {
|
|
149
|
-
if (methodDocCreated)
|
|
150
|
-
return; // Performance: only create once per lint run
|
|
151
148
|
const workspaceRoot = getWorkspaceRoot(context);
|
|
152
149
|
const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.methods.md');
|
|
150
|
+
// Check if file exists AND flag is true - if both, skip
|
|
151
|
+
if (methodDocCreated && fs.existsSync(docPath))
|
|
152
|
+
return;
|
|
153
153
|
if (ensureDocFile(docPath, METHOD_DOC_CONTENT)) {
|
|
154
154
|
methodDocCreated = true;
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
function getFunctionName(funcNode) {
|
|
158
|
+
if (funcNode.type === 'FunctionDeclaration' && funcNode.id?.name) {
|
|
159
|
+
return funcNode.id.name;
|
|
160
|
+
}
|
|
161
|
+
if (funcNode.type === 'FunctionExpression' && funcNode.id?.name) {
|
|
162
|
+
return funcNode.id.name;
|
|
163
|
+
}
|
|
164
|
+
return 'anonymous';
|
|
165
|
+
}
|
|
166
|
+
function reportTooLong(ctx, node, name, lineCount) {
|
|
167
|
+
ctx.context.report({
|
|
168
|
+
node,
|
|
169
|
+
messageId: 'tooLong',
|
|
170
|
+
data: {
|
|
171
|
+
name,
|
|
172
|
+
actual: String(lineCount),
|
|
173
|
+
max: String(ctx.maxLines),
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function checkFunctionNode(ctx, node) {
|
|
178
|
+
ensureMethodDoc(ctx.context);
|
|
179
|
+
const funcNode = node;
|
|
180
|
+
// Skip function expressions inside method definitions
|
|
181
|
+
if (funcNode.type === 'FunctionExpression' && funcNode['parent']?.type === 'MethodDefinition') {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!funcNode.loc || !funcNode.body)
|
|
185
|
+
return;
|
|
186
|
+
const name = getFunctionName(funcNode);
|
|
187
|
+
const lineCount = funcNode.loc.end.line - funcNode.loc.start.line + 1;
|
|
188
|
+
if (lineCount > ctx.maxLines) {
|
|
189
|
+
reportTooLong(ctx, funcNode, name, lineCount);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function checkMethodNode(ctx, node) {
|
|
193
|
+
ensureMethodDoc(ctx.context);
|
|
194
|
+
if (!node.loc || !node.value)
|
|
195
|
+
return;
|
|
196
|
+
const name = node.key?.name || 'anonymous';
|
|
197
|
+
const lineCount = node.loc.end.line - node.loc.start.line + 1;
|
|
198
|
+
if (lineCount > ctx.maxLines) {
|
|
199
|
+
reportTooLong(ctx, node, name, lineCount);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
157
202
|
const rule = {
|
|
158
203
|
meta: {
|
|
159
204
|
type: 'suggestion',
|
|
@@ -182,74 +227,12 @@ const rule = {
|
|
|
182
227
|
},
|
|
183
228
|
create(context) {
|
|
184
229
|
const options = context.options[0];
|
|
185
|
-
const
|
|
186
|
-
function checkFunction(node) {
|
|
187
|
-
ensureMethodDoc(context);
|
|
188
|
-
const funcNode = node;
|
|
189
|
-
// Skip if this is a function expression that's part of a method definition
|
|
190
|
-
// (method definitions will be handled by checkMethod)
|
|
191
|
-
if (funcNode.type === 'FunctionExpression' &&
|
|
192
|
-
funcNode['parent']?.type === 'MethodDefinition') {
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
// Skip if no location info or no body
|
|
196
|
-
if (!funcNode.loc || !funcNode.body) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
// Get function name
|
|
200
|
-
let name = 'anonymous';
|
|
201
|
-
if (funcNode.type === 'FunctionDeclaration' && funcNode.id?.name) {
|
|
202
|
-
name = funcNode.id.name;
|
|
203
|
-
}
|
|
204
|
-
else if (funcNode.type === 'FunctionExpression' && funcNode.id?.name) {
|
|
205
|
-
name = funcNode.id.name;
|
|
206
|
-
}
|
|
207
|
-
// Calculate line count
|
|
208
|
-
const startLine = funcNode.loc.start.line;
|
|
209
|
-
const endLine = funcNode.loc.end.line;
|
|
210
|
-
const lineCount = endLine - startLine + 1;
|
|
211
|
-
if (lineCount > maxLines) {
|
|
212
|
-
context.report({
|
|
213
|
-
node: funcNode,
|
|
214
|
-
messageId: 'tooLong',
|
|
215
|
-
data: {
|
|
216
|
-
name,
|
|
217
|
-
actual: String(lineCount),
|
|
218
|
-
max: String(maxLines),
|
|
219
|
-
},
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
function checkMethod(node) {
|
|
224
|
-
ensureMethodDoc(context);
|
|
225
|
-
const methodNode = node;
|
|
226
|
-
// Skip if no location info
|
|
227
|
-
if (!methodNode.loc || !methodNode.value) {
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
// Get method name from key
|
|
231
|
-
const name = methodNode.key?.name || 'anonymous';
|
|
232
|
-
// Calculate line count for the method (including the method definition)
|
|
233
|
-
const startLine = methodNode.loc.start.line;
|
|
234
|
-
const endLine = methodNode.loc.end.line;
|
|
235
|
-
const lineCount = endLine - startLine + 1;
|
|
236
|
-
if (lineCount > maxLines) {
|
|
237
|
-
context.report({
|
|
238
|
-
node: methodNode,
|
|
239
|
-
messageId: 'tooLong',
|
|
240
|
-
data: {
|
|
241
|
-
name,
|
|
242
|
-
actual: String(lineCount),
|
|
243
|
-
max: String(maxLines),
|
|
244
|
-
},
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
}
|
|
230
|
+
const ctx = { context, maxLines: options?.max ?? 70 };
|
|
248
231
|
return {
|
|
249
|
-
FunctionDeclaration:
|
|
250
|
-
FunctionExpression:
|
|
251
|
-
ArrowFunctionExpression:
|
|
252
|
-
MethodDefinition:
|
|
232
|
+
FunctionDeclaration: (node) => checkFunctionNode(ctx, node),
|
|
233
|
+
FunctionExpression: (node) => checkFunctionNode(ctx, node),
|
|
234
|
+
ArrowFunctionExpression: (node) => checkFunctionNode(ctx, node),
|
|
235
|
+
MethodDefinition: (node) => checkMethodNode(ctx, node),
|
|
253
236
|
};
|
|
254
237
|
},
|
|
255
238
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"max-method-lines.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/eslint-plugin/rules/max-method-lines.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESLint rule to enforce maximum method length\n *\n * Enforces a configurable maximum line count for methods, functions, and arrow functions.\n * Default: 70 lines\n *\n * Configuration:\n * '@webpieces/max-method-lines': ['error', { max: 70 }]\n */\n\nimport type { Rule } from 'eslint';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\ninterface MethodLinesOptions {\n max: number;\n}\n\ninterface FunctionNode {\n type:\n | 'FunctionDeclaration'\n | 'FunctionExpression'\n | 'ArrowFunctionExpression'\n | 'MethodDefinition';\n body?: any;\n loc?: {\n start: { line: number };\n end: { line: number };\n };\n key?: {\n name?: string;\n };\n id?: {\n name?: string;\n };\n [key: string]: any;\n}\n\nconst METHOD_DOC_CONTENT = `# AI Agent Instructions: Method Too Long\n\n**READ THIS FILE to fix methods that are too long**\n\n## Core Principle\nEvery method should read like a TABLE OF CONTENTS of a book.\n- Each method call is a \"chapter\"\n- When you dive into a method, you find another table of contents\n- Keeping methods under 70 lines is achievable with proper extraction\n\n## Command: Extract Code into Named Methods\n\n### Pattern 1: Extract Loop Bodies\n\\`\\`\\`typescript\n// ❌ BAD: 50 lines embedded in loop\nfor (const order of orders) {\n // 20 lines of validation logic\n // 15 lines of processing logic\n // 10 lines of notification logic\n}\n\n// ✅ GOOD: Extracted to named methods\nfor (const order of orders) {\n validateOrder(order);\n processOrderItems(order);\n sendNotifications(order);\n}\n\\`\\`\\`\n\n### Pattern 2: Try-Catch Wrapper for Exception Handling\n\\`\\`\\`typescript\n// ✅ GOOD: Separates success path from error handling\nasync function handleRequest(req: Request): Promise<Response> {\n try {\n return await executeRequest(req);\n } catch (err: any) {\n const error = toError(err);\n return createErrorResponse(error);\n }\n}\n\\`\\`\\`\n\n### Pattern 3: Sequential Method Calls (Table of Contents)\n\\`\\`\\`typescript\n// ✅ GOOD: Self-documenting steps\nfunction processOrder(order: Order): void {\n validateOrderData(order);\n calculateTotals(order);\n applyDiscounts(order);\n processPayment(order);\n updateInventory(order);\n sendConfirmation(order);\n}\n\\`\\`\\`\n\n### Pattern 4: Separate Data Object Creation\n\\`\\`\\`typescript\n// ❌ BAD: 15 lines of inline object creation\ndoSomething({ field1: ..., field2: ..., field3: ..., /* 15 more fields */ });\n\n// ✅ GOOD: Extract to factory method\nconst request = createRequestObject(data);\ndoSomething(request);\n\\`\\`\\`\n\n### Pattern 5: Extract Inline Logic to Named Functions\n\\`\\`\\`typescript\n// ❌ BAD: Complex inline logic\nif (user.role === 'admin' && user.permissions.includes('write') && !user.suspended) {\n // 30 lines of admin logic\n}\n\n// ✅ GOOD: Extract to named methods\nif (isAdminWithWriteAccess(user)) {\n performAdminOperation(user);\n}\n\\`\\`\\`\n\n## AI Agent Action Steps\n\n1. **IDENTIFY** the long method in the error message\n2. **READ** the method to understand its logical sections\n3. **EXTRACT** logical units into separate methods with descriptive names\n4. **REPLACE** inline code with method calls\n5. **VERIFY** each extracted method is <70 lines\n6. **TEST** that functionality remains unchanged\n\n## Examples of \"Logical Units\" to Extract\n- Validation logic → \\`validateX()\\`\n- Data transformation → \\`transformXToY()\\`\n- API calls → \\`fetchXFromApi()\\`\n- Object creation → \\`createX()\\`\n- Loop bodies → \\`processItem()\\`\n- Error handling → \\`handleXError()\\`\n\nRemember: Methods should read like a table of contents. Each line should be a \"chapter title\" (method call) that describes what happens, not how it happens.\n`;\n\n// Module-level flag to prevent redundant file creation\nlet methodDocCreated = false;\n\nfunction getWorkspaceRoot(context: Rule.RuleContext): string {\n const filename = context.filename || context.getFilename();\n let dir = path.dirname(filename);\n\n // Walk up directory tree to find workspace root\n while (dir !== path.dirname(dir)) {\n const pkgPath = path.join(dir, 'package.json');\n if (fs.existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));\n if (pkg.workspaces || pkg.name === 'webpieces-ts') {\n return dir;\n }\n } catch (err: any) {\n //const error = toError(err);\n // Continue searching if JSON parse fails\n }\n }\n dir = path.dirname(dir);\n }\n return process.cwd(); // Fallback\n}\n\nfunction ensureDocFile(docPath: string, content: string): boolean {\n try {\n fs.mkdirSync(path.dirname(docPath), { recursive: true });\n fs.writeFileSync(docPath, content, 'utf-8');\n return true;\n } catch (err: any) {\n //const error = toError(err);\n // Graceful degradation: log warning but don't break lint\n console.warn(`[webpieces] Could not create doc file: ${docPath}`, err);\n return false;\n }\n}\n\nfunction ensureMethodDoc(context: Rule.RuleContext): void {\n if (methodDocCreated) return; // Performance: only create once per lint run\n\n const workspaceRoot = getWorkspaceRoot(context);\n const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.methods.md');\n\n if (ensureDocFile(docPath, METHOD_DOC_CONTENT)) {\n methodDocCreated = true;\n }\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Enforce maximum method length',\n category: 'Best Practices',\n recommended: false,\n url: 'https://github.com/deanhiller/webpieces-ts',\n },\n messages: {\n tooLong:\n 'AI Agent: READ tmp/webpieces/webpieces.methods.md for fix instructions. Method \"{{name}}\" has {{actual}} lines (max: {{max}})',\n },\n fixable: undefined,\n schema: [\n {\n type: 'object',\n properties: {\n max: {\n type: 'integer',\n minimum: 1,\n },\n },\n additionalProperties: false,\n },\n ],\n },\n\n create(context: Rule.RuleContext): Rule.RuleListener {\n const options = context.options[0] as MethodLinesOptions | undefined;\n const maxLines = options?.max ?? 70;\n\n function checkFunction(node: any): void {\n ensureMethodDoc(context);\n\n const funcNode = node as FunctionNode;\n\n // Skip if this is a function expression that's part of a method definition\n // (method definitions will be handled by checkMethod)\n if (\n funcNode.type === 'FunctionExpression' &&\n funcNode['parent']?.type === 'MethodDefinition'\n ) {\n return;\n }\n\n // Skip if no location info or no body\n if (!funcNode.loc || !funcNode.body) {\n return;\n }\n\n // Get function name\n let name = 'anonymous';\n if (funcNode.type === 'FunctionDeclaration' && funcNode.id?.name) {\n name = funcNode.id.name;\n } else if (funcNode.type === 'FunctionExpression' && funcNode.id?.name) {\n name = funcNode.id.name;\n }\n\n // Calculate line count\n const startLine = funcNode.loc.start.line;\n const endLine = funcNode.loc.end.line;\n const lineCount = endLine - startLine + 1;\n\n if (lineCount > maxLines) {\n context.report({\n node: funcNode as any,\n messageId: 'tooLong',\n data: {\n name,\n actual: String(lineCount),\n max: String(maxLines),\n },\n });\n }\n }\n\n function checkMethod(node: any): void {\n ensureMethodDoc(context);\n\n const methodNode = node;\n\n // Skip if no location info\n if (!methodNode.loc || !methodNode.value) {\n return;\n }\n\n // Get method name from key\n const name = methodNode.key?.name || 'anonymous';\n\n // Calculate line count for the method (including the method definition)\n const startLine = methodNode.loc.start.line;\n const endLine = methodNode.loc.end.line;\n const lineCount = endLine - startLine + 1;\n\n if (lineCount > maxLines) {\n context.report({\n node: methodNode as any,\n messageId: 'tooLong',\n data: {\n name,\n actual: String(lineCount),\n max: String(maxLines),\n },\n });\n }\n }\n\n return {\n FunctionDeclaration: checkFunction,\n FunctionExpression: checkFunction,\n ArrowFunctionExpression: checkFunction,\n MethodDefinition: checkMethod,\n };\n },\n};\n\nexport = rule;\n"]}
|
|
1
|
+
{"version":3,"file":"max-method-lines.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/eslint-plugin/rules/max-method-lines.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESLint rule to enforce maximum method length\n *\n * Enforces a configurable maximum line count for methods, functions, and arrow functions.\n * Default: 70 lines\n *\n * Configuration:\n * '@webpieces/max-method-lines': ['error', { max: 70 }]\n */\n\nimport type { Rule } from 'eslint';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\ninterface MethodLinesOptions {\n max: number;\n}\n\ninterface FunctionNode {\n type:\n | 'FunctionDeclaration'\n | 'FunctionExpression'\n | 'ArrowFunctionExpression'\n | 'MethodDefinition';\n body?: any;\n loc?: {\n start: { line: number };\n end: { line: number };\n };\n key?: {\n name?: string;\n };\n id?: {\n name?: string;\n };\n [key: string]: any;\n}\n\ninterface CheckerContext {\n context: Rule.RuleContext;\n maxLines: number;\n}\n\nconst METHOD_DOC_CONTENT = `# AI Agent Instructions: Method Too Long\n\n**READ THIS FILE to fix methods that are too long**\n\n## Core Principle\nEvery method should read like a TABLE OF CONTENTS of a book.\n- Each method call is a \"chapter\"\n- When you dive into a method, you find another table of contents\n- Keeping methods under 70 lines is achievable with proper extraction\n\n## Command: Extract Code into Named Methods\n\n### Pattern 1: Extract Loop Bodies\n\\`\\`\\`typescript\n// BAD: 50 lines embedded in loop\nfor (const order of orders) {\n // 20 lines of validation logic\n // 15 lines of processing logic\n // 10 lines of notification logic\n}\n\n// GOOD: Extracted to named methods\nfor (const order of orders) {\n validateOrder(order);\n processOrderItems(order);\n sendNotifications(order);\n}\n\\`\\`\\`\n\n### Pattern 2: Try-Catch Wrapper for Exception Handling\n\\`\\`\\`typescript\n// GOOD: Separates success path from error handling\nasync function handleRequest(req: Request): Promise<Response> {\n try {\n return await executeRequest(req);\n } catch (err: unknown) {\n const error = toError(err);\n return createErrorResponse(error);\n }\n}\n\\`\\`\\`\n\n### Pattern 3: Sequential Method Calls (Table of Contents)\n\\`\\`\\`typescript\n// GOOD: Self-documenting steps\nfunction processOrder(order: Order): void {\n validateOrderData(order);\n calculateTotals(order);\n applyDiscounts(order);\n processPayment(order);\n updateInventory(order);\n sendConfirmation(order);\n}\n\\`\\`\\`\n\n### Pattern 4: Separate Data Object Creation\n\\`\\`\\`typescript\n// BAD: 15 lines of inline object creation\ndoSomething({ field1: ..., field2: ..., field3: ..., /* 15 more fields */ });\n\n// GOOD: Extract to factory method\nconst request = createRequestObject(data);\ndoSomething(request);\n\\`\\`\\`\n\n### Pattern 5: Extract Inline Logic to Named Functions\n\\`\\`\\`typescript\n// BAD: Complex inline logic\nif (user.role === 'admin' && user.permissions.includes('write') && !user.suspended) {\n // 30 lines of admin logic\n}\n\n// GOOD: Extract to named methods\nif (isAdminWithWriteAccess(user)) {\n performAdminOperation(user);\n}\n\\`\\`\\`\n\n## AI Agent Action Steps\n\n1. **IDENTIFY** the long method in the error message\n2. **READ** the method to understand its logical sections\n3. **EXTRACT** logical units into separate methods with descriptive names\n4. **REPLACE** inline code with method calls\n5. **VERIFY** each extracted method is <70 lines\n6. **TEST** that functionality remains unchanged\n\n## Examples of \"Logical Units\" to Extract\n- Validation logic -> \\`validateX()\\`\n- Data transformation -> \\`transformXToY()\\`\n- API calls -> \\`fetchXFromApi()\\`\n- Object creation -> \\`createX()\\`\n- Loop bodies -> \\`processItem()\\`\n- Error handling -> \\`handleXError()\\`\n\nRemember: Methods should read like a table of contents. Each line should be a \"chapter title\" (method call) that describes what happens, not how it happens.\n`;\n\n// Module-level flag to prevent redundant file creation\nlet methodDocCreated = false;\n\nfunction getWorkspaceRoot(context: Rule.RuleContext): string {\n const filename = context.filename || context.getFilename();\n let dir = path.dirname(filename);\n\n while (dir !== path.dirname(dir)) {\n const pkgPath = path.join(dir, 'package.json');\n if (fs.existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));\n if (pkg.workspaces || pkg.name === 'webpieces-ts') {\n return dir;\n }\n } catch (err: any) {\n //const error = toError(err);\n void err;\n }\n }\n dir = path.dirname(dir);\n }\n return process.cwd();\n}\n\nfunction ensureDocFile(docPath: string, content: string): boolean {\n try {\n fs.mkdirSync(path.dirname(docPath), { recursive: true });\n fs.writeFileSync(docPath, content, 'utf-8');\n return true;\n } catch (err: any) {\n //const error = toError(err);\n // err is used in console.warn below\n console.warn(`[webpieces] Could not create doc file: ${docPath}`, err);\n return false;\n }\n}\n\nfunction ensureMethodDoc(context: Rule.RuleContext): void {\n const workspaceRoot = getWorkspaceRoot(context);\n const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.methods.md');\n\n // Check if file exists AND flag is true - if both, skip\n if (methodDocCreated && fs.existsSync(docPath)) return;\n\n if (ensureDocFile(docPath, METHOD_DOC_CONTENT)) {\n methodDocCreated = true;\n }\n}\n\nfunction getFunctionName(funcNode: FunctionNode): string {\n if (funcNode.type === 'FunctionDeclaration' && funcNode.id?.name) {\n return funcNode.id.name;\n }\n if (funcNode.type === 'FunctionExpression' && funcNode.id?.name) {\n return funcNode.id.name;\n }\n return 'anonymous';\n}\n\nfunction reportTooLong(ctx: CheckerContext, node: any, name: string, lineCount: number): void {\n ctx.context.report({\n node,\n messageId: 'tooLong',\n data: {\n name,\n actual: String(lineCount),\n max: String(ctx.maxLines),\n },\n });\n}\n\nfunction checkFunctionNode(ctx: CheckerContext, node: any): void {\n ensureMethodDoc(ctx.context);\n const funcNode = node as FunctionNode;\n\n // Skip function expressions inside method definitions\n if (funcNode.type === 'FunctionExpression' && funcNode['parent']?.type === 'MethodDefinition') {\n return;\n }\n\n if (!funcNode.loc || !funcNode.body) return;\n\n const name = getFunctionName(funcNode);\n const lineCount = funcNode.loc.end.line - funcNode.loc.start.line + 1;\n\n if (lineCount > ctx.maxLines) {\n reportTooLong(ctx, funcNode, name, lineCount);\n }\n}\n\nfunction checkMethodNode(ctx: CheckerContext, node: any): void {\n ensureMethodDoc(ctx.context);\n\n if (!node.loc || !node.value) return;\n\n const name = node.key?.name || 'anonymous';\n const lineCount = node.loc.end.line - node.loc.start.line + 1;\n\n if (lineCount > ctx.maxLines) {\n reportTooLong(ctx, node, name, lineCount);\n }\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Enforce maximum method length',\n category: 'Best Practices',\n recommended: false,\n url: 'https://github.com/deanhiller/webpieces-ts',\n },\n messages: {\n tooLong:\n 'AI Agent: READ tmp/webpieces/webpieces.methods.md for fix instructions. Method \"{{name}}\" has {{actual}} lines (max: {{max}})',\n },\n fixable: undefined,\n schema: [\n {\n type: 'object',\n properties: {\n max: {\n type: 'integer',\n minimum: 1,\n },\n },\n additionalProperties: false,\n },\n ],\n },\n\n create(context: Rule.RuleContext): Rule.RuleListener {\n const options = context.options[0] as MethodLinesOptions | undefined;\n const ctx: CheckerContext = { context, maxLines: options?.max ?? 70 };\n\n return {\n FunctionDeclaration: (node) => checkFunctionNode(ctx, node),\n FunctionExpression: (node) => checkFunctionNode(ctx, node),\n ArrowFunctionExpression: (node) => checkFunctionNode(ctx, node),\n MethodDefinition: (node) => checkMethodNode(ctx, node),\n };\n },\n};\n\nexport = rule;\n"]}
|
|
@@ -36,6 +36,11 @@ interface FunctionNode {
|
|
|
36
36
|
[key: string]: any;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
interface CheckerContext {
|
|
40
|
+
context: Rule.RuleContext;
|
|
41
|
+
maxLines: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
const METHOD_DOC_CONTENT = `# AI Agent Instructions: Method Too Long
|
|
40
45
|
|
|
41
46
|
**READ THIS FILE to fix methods that are too long**
|
|
@@ -50,14 +55,14 @@ Every method should read like a TABLE OF CONTENTS of a book.
|
|
|
50
55
|
|
|
51
56
|
### Pattern 1: Extract Loop Bodies
|
|
52
57
|
\`\`\`typescript
|
|
53
|
-
//
|
|
58
|
+
// BAD: 50 lines embedded in loop
|
|
54
59
|
for (const order of orders) {
|
|
55
60
|
// 20 lines of validation logic
|
|
56
61
|
// 15 lines of processing logic
|
|
57
62
|
// 10 lines of notification logic
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
//
|
|
65
|
+
// GOOD: Extracted to named methods
|
|
61
66
|
for (const order of orders) {
|
|
62
67
|
validateOrder(order);
|
|
63
68
|
processOrderItems(order);
|
|
@@ -67,11 +72,11 @@ for (const order of orders) {
|
|
|
67
72
|
|
|
68
73
|
### Pattern 2: Try-Catch Wrapper for Exception Handling
|
|
69
74
|
\`\`\`typescript
|
|
70
|
-
//
|
|
75
|
+
// GOOD: Separates success path from error handling
|
|
71
76
|
async function handleRequest(req: Request): Promise<Response> {
|
|
72
77
|
try {
|
|
73
78
|
return await executeRequest(req);
|
|
74
|
-
} catch (err:
|
|
79
|
+
} catch (err: unknown) {
|
|
75
80
|
const error = toError(err);
|
|
76
81
|
return createErrorResponse(error);
|
|
77
82
|
}
|
|
@@ -80,7 +85,7 @@ async function handleRequest(req: Request): Promise<Response> {
|
|
|
80
85
|
|
|
81
86
|
### Pattern 3: Sequential Method Calls (Table of Contents)
|
|
82
87
|
\`\`\`typescript
|
|
83
|
-
//
|
|
88
|
+
// GOOD: Self-documenting steps
|
|
84
89
|
function processOrder(order: Order): void {
|
|
85
90
|
validateOrderData(order);
|
|
86
91
|
calculateTotals(order);
|
|
@@ -93,22 +98,22 @@ function processOrder(order: Order): void {
|
|
|
93
98
|
|
|
94
99
|
### Pattern 4: Separate Data Object Creation
|
|
95
100
|
\`\`\`typescript
|
|
96
|
-
//
|
|
101
|
+
// BAD: 15 lines of inline object creation
|
|
97
102
|
doSomething({ field1: ..., field2: ..., field3: ..., /* 15 more fields */ });
|
|
98
103
|
|
|
99
|
-
//
|
|
104
|
+
// GOOD: Extract to factory method
|
|
100
105
|
const request = createRequestObject(data);
|
|
101
106
|
doSomething(request);
|
|
102
107
|
\`\`\`
|
|
103
108
|
|
|
104
109
|
### Pattern 5: Extract Inline Logic to Named Functions
|
|
105
110
|
\`\`\`typescript
|
|
106
|
-
//
|
|
111
|
+
// BAD: Complex inline logic
|
|
107
112
|
if (user.role === 'admin' && user.permissions.includes('write') && !user.suspended) {
|
|
108
113
|
// 30 lines of admin logic
|
|
109
114
|
}
|
|
110
115
|
|
|
111
|
-
//
|
|
116
|
+
// GOOD: Extract to named methods
|
|
112
117
|
if (isAdminWithWriteAccess(user)) {
|
|
113
118
|
performAdminOperation(user);
|
|
114
119
|
}
|
|
@@ -124,12 +129,12 @@ if (isAdminWithWriteAccess(user)) {
|
|
|
124
129
|
6. **TEST** that functionality remains unchanged
|
|
125
130
|
|
|
126
131
|
## Examples of "Logical Units" to Extract
|
|
127
|
-
- Validation logic
|
|
128
|
-
- Data transformation
|
|
129
|
-
- API calls
|
|
130
|
-
- Object creation
|
|
131
|
-
- Loop bodies
|
|
132
|
-
- Error handling
|
|
132
|
+
- Validation logic -> \`validateX()\`
|
|
133
|
+
- Data transformation -> \`transformXToY()\`
|
|
134
|
+
- API calls -> \`fetchXFromApi()\`
|
|
135
|
+
- Object creation -> \`createX()\`
|
|
136
|
+
- Loop bodies -> \`processItem()\`
|
|
137
|
+
- Error handling -> \`handleXError()\`
|
|
133
138
|
|
|
134
139
|
Remember: Methods should read like a table of contents. Each line should be a "chapter title" (method call) that describes what happens, not how it happens.
|
|
135
140
|
`;
|
|
@@ -141,7 +146,6 @@ function getWorkspaceRoot(context: Rule.RuleContext): string {
|
|
|
141
146
|
const filename = context.filename || context.getFilename();
|
|
142
147
|
let dir = path.dirname(filename);
|
|
143
148
|
|
|
144
|
-
// Walk up directory tree to find workspace root
|
|
145
149
|
while (dir !== path.dirname(dir)) {
|
|
146
150
|
const pkgPath = path.join(dir, 'package.json');
|
|
147
151
|
if (fs.existsSync(pkgPath)) {
|
|
@@ -152,12 +156,12 @@ function getWorkspaceRoot(context: Rule.RuleContext): string {
|
|
|
152
156
|
}
|
|
153
157
|
} catch (err: any) {
|
|
154
158
|
//const error = toError(err);
|
|
155
|
-
|
|
159
|
+
void err;
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
162
|
dir = path.dirname(dir);
|
|
159
163
|
}
|
|
160
|
-
return process.cwd();
|
|
164
|
+
return process.cwd();
|
|
161
165
|
}
|
|
162
166
|
|
|
163
167
|
function ensureDocFile(docPath: string, content: string): boolean {
|
|
@@ -167,23 +171,78 @@ function ensureDocFile(docPath: string, content: string): boolean {
|
|
|
167
171
|
return true;
|
|
168
172
|
} catch (err: any) {
|
|
169
173
|
//const error = toError(err);
|
|
170
|
-
//
|
|
174
|
+
// err is used in console.warn below
|
|
171
175
|
console.warn(`[webpieces] Could not create doc file: ${docPath}`, err);
|
|
172
176
|
return false;
|
|
173
177
|
}
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
function ensureMethodDoc(context: Rule.RuleContext): void {
|
|
177
|
-
if (methodDocCreated) return; // Performance: only create once per lint run
|
|
178
|
-
|
|
179
181
|
const workspaceRoot = getWorkspaceRoot(context);
|
|
180
182
|
const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.methods.md');
|
|
181
183
|
|
|
184
|
+
// Check if file exists AND flag is true - if both, skip
|
|
185
|
+
if (methodDocCreated && fs.existsSync(docPath)) return;
|
|
186
|
+
|
|
182
187
|
if (ensureDocFile(docPath, METHOD_DOC_CONTENT)) {
|
|
183
188
|
methodDocCreated = true;
|
|
184
189
|
}
|
|
185
190
|
}
|
|
186
191
|
|
|
192
|
+
function getFunctionName(funcNode: FunctionNode): string {
|
|
193
|
+
if (funcNode.type === 'FunctionDeclaration' && funcNode.id?.name) {
|
|
194
|
+
return funcNode.id.name;
|
|
195
|
+
}
|
|
196
|
+
if (funcNode.type === 'FunctionExpression' && funcNode.id?.name) {
|
|
197
|
+
return funcNode.id.name;
|
|
198
|
+
}
|
|
199
|
+
return 'anonymous';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function reportTooLong(ctx: CheckerContext, node: any, name: string, lineCount: number): void {
|
|
203
|
+
ctx.context.report({
|
|
204
|
+
node,
|
|
205
|
+
messageId: 'tooLong',
|
|
206
|
+
data: {
|
|
207
|
+
name,
|
|
208
|
+
actual: String(lineCount),
|
|
209
|
+
max: String(ctx.maxLines),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function checkFunctionNode(ctx: CheckerContext, node: any): void {
|
|
215
|
+
ensureMethodDoc(ctx.context);
|
|
216
|
+
const funcNode = node as FunctionNode;
|
|
217
|
+
|
|
218
|
+
// Skip function expressions inside method definitions
|
|
219
|
+
if (funcNode.type === 'FunctionExpression' && funcNode['parent']?.type === 'MethodDefinition') {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!funcNode.loc || !funcNode.body) return;
|
|
224
|
+
|
|
225
|
+
const name = getFunctionName(funcNode);
|
|
226
|
+
const lineCount = funcNode.loc.end.line - funcNode.loc.start.line + 1;
|
|
227
|
+
|
|
228
|
+
if (lineCount > ctx.maxLines) {
|
|
229
|
+
reportTooLong(ctx, funcNode, name, lineCount);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function checkMethodNode(ctx: CheckerContext, node: any): void {
|
|
234
|
+
ensureMethodDoc(ctx.context);
|
|
235
|
+
|
|
236
|
+
if (!node.loc || !node.value) return;
|
|
237
|
+
|
|
238
|
+
const name = node.key?.name || 'anonymous';
|
|
239
|
+
const lineCount = node.loc.end.line - node.loc.start.line + 1;
|
|
240
|
+
|
|
241
|
+
if (lineCount > ctx.maxLines) {
|
|
242
|
+
reportTooLong(ctx, node, name, lineCount);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
187
246
|
const rule: Rule.RuleModule = {
|
|
188
247
|
meta: {
|
|
189
248
|
type: 'suggestion',
|
|
@@ -214,89 +273,13 @@ const rule: Rule.RuleModule = {
|
|
|
214
273
|
|
|
215
274
|
create(context: Rule.RuleContext): Rule.RuleListener {
|
|
216
275
|
const options = context.options[0] as MethodLinesOptions | undefined;
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
function checkFunction(node: any): void {
|
|
220
|
-
ensureMethodDoc(context);
|
|
221
|
-
|
|
222
|
-
const funcNode = node as FunctionNode;
|
|
223
|
-
|
|
224
|
-
// Skip if this is a function expression that's part of a method definition
|
|
225
|
-
// (method definitions will be handled by checkMethod)
|
|
226
|
-
if (
|
|
227
|
-
funcNode.type === 'FunctionExpression' &&
|
|
228
|
-
funcNode['parent']?.type === 'MethodDefinition'
|
|
229
|
-
) {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Skip if no location info or no body
|
|
234
|
-
if (!funcNode.loc || !funcNode.body) {
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Get function name
|
|
239
|
-
let name = 'anonymous';
|
|
240
|
-
if (funcNode.type === 'FunctionDeclaration' && funcNode.id?.name) {
|
|
241
|
-
name = funcNode.id.name;
|
|
242
|
-
} else if (funcNode.type === 'FunctionExpression' && funcNode.id?.name) {
|
|
243
|
-
name = funcNode.id.name;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Calculate line count
|
|
247
|
-
const startLine = funcNode.loc.start.line;
|
|
248
|
-
const endLine = funcNode.loc.end.line;
|
|
249
|
-
const lineCount = endLine - startLine + 1;
|
|
250
|
-
|
|
251
|
-
if (lineCount > maxLines) {
|
|
252
|
-
context.report({
|
|
253
|
-
node: funcNode as any,
|
|
254
|
-
messageId: 'tooLong',
|
|
255
|
-
data: {
|
|
256
|
-
name,
|
|
257
|
-
actual: String(lineCount),
|
|
258
|
-
max: String(maxLines),
|
|
259
|
-
},
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function checkMethod(node: any): void {
|
|
265
|
-
ensureMethodDoc(context);
|
|
266
|
-
|
|
267
|
-
const methodNode = node;
|
|
268
|
-
|
|
269
|
-
// Skip if no location info
|
|
270
|
-
if (!methodNode.loc || !methodNode.value) {
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Get method name from key
|
|
275
|
-
const name = methodNode.key?.name || 'anonymous';
|
|
276
|
-
|
|
277
|
-
// Calculate line count for the method (including the method definition)
|
|
278
|
-
const startLine = methodNode.loc.start.line;
|
|
279
|
-
const endLine = methodNode.loc.end.line;
|
|
280
|
-
const lineCount = endLine - startLine + 1;
|
|
281
|
-
|
|
282
|
-
if (lineCount > maxLines) {
|
|
283
|
-
context.report({
|
|
284
|
-
node: methodNode as any,
|
|
285
|
-
messageId: 'tooLong',
|
|
286
|
-
data: {
|
|
287
|
-
name,
|
|
288
|
-
actual: String(lineCount),
|
|
289
|
-
max: String(maxLines),
|
|
290
|
-
},
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
}
|
|
276
|
+
const ctx: CheckerContext = { context, maxLines: options?.max ?? 70 };
|
|
294
277
|
|
|
295
278
|
return {
|
|
296
|
-
FunctionDeclaration:
|
|
297
|
-
FunctionExpression:
|
|
298
|
-
ArrowFunctionExpression:
|
|
299
|
-
MethodDefinition:
|
|
279
|
+
FunctionDeclaration: (node) => checkFunctionNode(ctx, node),
|
|
280
|
+
FunctionExpression: (node) => checkFunctionNode(ctx, node),
|
|
281
|
+
ArrowFunctionExpression: (node) => checkFunctionNode(ctx, node),
|
|
282
|
+
MethodDefinition: (node) => checkMethodNode(ctx, node),
|
|
300
283
|
};
|
|
301
284
|
},
|
|
302
285
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to discourage try-catch blocks outside test files
|
|
3
|
+
*
|
|
4
|
+
* Works alongside catch-error-pattern rule:
|
|
5
|
+
* - catch-error-pattern: Enforces HOW to handle exceptions (with toError())
|
|
6
|
+
* - no-unmanaged-exceptions: Enforces WHERE try-catch is allowed (tests only by default)
|
|
7
|
+
*
|
|
8
|
+
* Philosophy: Exceptions should bubble to global error handlers where they are logged
|
|
9
|
+
* with traceId and stored for debugging via /debugLocal and /debugCloud endpoints.
|
|
10
|
+
* Local try-catch blocks break this architecture and create blind spots in production.
|
|
11
|
+
*
|
|
12
|
+
* Auto-allowed in:
|
|
13
|
+
* - Test files (.test.ts, .spec.ts, __tests__/)
|
|
14
|
+
*
|
|
15
|
+
* Requires eslint-disable comment in:
|
|
16
|
+
* - Retry loops with exponential backoff
|
|
17
|
+
* - Batch processing where partial failure is expected
|
|
18
|
+
* - Resource cleanup (with approval)
|
|
19
|
+
*/
|
|
20
|
+
import type { Rule } from 'eslint';
|
|
21
|
+
declare const rule: Rule.RuleModule;
|
|
22
|
+
export = rule;
|