@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.
- package/README.md +306 -0
- package/bin/set-version.sh +86 -0
- package/bin/setup-claude-patterns.sh +51 -0
- package/bin/start.sh +107 -0
- package/bin/stop.sh +65 -0
- package/bin/use-local-webpieces.sh +89 -0
- package/bin/use-published-webpieces.sh +33 -0
- package/config/eslint/base.mjs +91 -0
- package/config/typescript/tsconfig.base.json +25 -0
- package/eslint-plugin/__tests__/catch-error-pattern.test.ts +360 -0
- package/eslint-plugin/__tests__/max-file-lines.test.ts +195 -0
- package/eslint-plugin/__tests__/max-method-lines.test.ts +246 -0
- package/eslint-plugin/index.d.ts +14 -0
- package/eslint-plugin/index.js +19 -0
- package/eslint-plugin/index.js.map +1 -0
- package/eslint-plugin/index.ts +18 -0
- package/eslint-plugin/rules/catch-error-pattern.d.ts +11 -0
- package/eslint-plugin/rules/catch-error-pattern.js +196 -0
- package/eslint-plugin/rules/catch-error-pattern.js.map +1 -0
- package/eslint-plugin/rules/catch-error-pattern.ts +281 -0
- package/eslint-plugin/rules/max-file-lines.d.ts +12 -0
- package/eslint-plugin/rules/max-file-lines.js +257 -0
- package/eslint-plugin/rules/max-file-lines.js.map +1 -0
- package/eslint-plugin/rules/max-file-lines.ts +272 -0
- package/eslint-plugin/rules/max-method-lines.d.ts +12 -0
- package/eslint-plugin/rules/max-method-lines.js +257 -0
- package/eslint-plugin/rules/max-method-lines.js.map +1 -0
- package/eslint-plugin/rules/max-method-lines.ts +304 -0
- package/package.json +54 -0
- package/patterns/CLAUDE.md +293 -0
- package/patterns/claude.patterns.md +798 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to enforce maximum file length
|
|
3
|
+
*
|
|
4
|
+
* Enforces a configurable maximum line count for files.
|
|
5
|
+
* Default: 700 lines
|
|
6
|
+
*
|
|
7
|
+
* Configuration:
|
|
8
|
+
* '@webpieces/max-file-lines': ['error', { max: 700 }]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Rule } from 'eslint';
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
|
|
15
|
+
interface FileLinesOptions {
|
|
16
|
+
max: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const FILE_DOC_CONTENT = `# AI Agent Instructions: File Too Long
|
|
20
|
+
|
|
21
|
+
**READ THIS FILE to fix files that are too long**
|
|
22
|
+
|
|
23
|
+
## Core Principle
|
|
24
|
+
Files should contain a SINGLE COHESIVE UNIT.
|
|
25
|
+
- One class per file (Java convention)
|
|
26
|
+
- If class is too large, extract child responsibilities
|
|
27
|
+
- Use dependency injection to compose functionality
|
|
28
|
+
|
|
29
|
+
## Command: Reduce File Size
|
|
30
|
+
|
|
31
|
+
### Step 1: Check for Multiple Classes
|
|
32
|
+
If the file contains multiple classes, **SEPARATE each class into its own file**.
|
|
33
|
+
|
|
34
|
+
\`\`\`typescript
|
|
35
|
+
// ❌ BAD: UserController.ts (multiple classes)
|
|
36
|
+
export class UserController { /* ... */ }
|
|
37
|
+
export class UserValidator { /* ... */ }
|
|
38
|
+
export class UserNotifier { /* ... */ }
|
|
39
|
+
|
|
40
|
+
// ✅ GOOD: Three separate files
|
|
41
|
+
// UserController.ts
|
|
42
|
+
export class UserController { /* ... */ }
|
|
43
|
+
|
|
44
|
+
// UserValidator.ts
|
|
45
|
+
export class UserValidator { /* ... */ }
|
|
46
|
+
|
|
47
|
+
// UserNotifier.ts
|
|
48
|
+
export class UserNotifier { /* ... */ }
|
|
49
|
+
\`\`\`
|
|
50
|
+
|
|
51
|
+
### Step 2: Extract Child Responsibilities (if single class is too large)
|
|
52
|
+
|
|
53
|
+
#### Pattern: Create New Service Class with Dependency Injection
|
|
54
|
+
|
|
55
|
+
\`\`\`typescript
|
|
56
|
+
// ❌ BAD: UserController.ts (800 lines, single class)
|
|
57
|
+
@provideSingleton()
|
|
58
|
+
@Controller()
|
|
59
|
+
export class UserController {
|
|
60
|
+
// 200 lines: CRUD operations
|
|
61
|
+
// 300 lines: validation logic
|
|
62
|
+
// 200 lines: notification logic
|
|
63
|
+
// 100 lines: analytics logic
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ✅ GOOD: Extract validation service
|
|
67
|
+
// 1. Create UserValidationService.ts
|
|
68
|
+
@provideSingleton()
|
|
69
|
+
export class UserValidationService {
|
|
70
|
+
validateUserData(data: UserData): ValidationResult {
|
|
71
|
+
// 300 lines of validation logic moved here
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
validateEmail(email: string): boolean { /* ... */ }
|
|
75
|
+
validatePassword(password: string): boolean { /* ... */ }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. Inject into UserController.ts
|
|
79
|
+
@provideSingleton()
|
|
80
|
+
@Controller()
|
|
81
|
+
export class UserController {
|
|
82
|
+
constructor(
|
|
83
|
+
@inject(TYPES.UserValidationService)
|
|
84
|
+
private validator: UserValidationService
|
|
85
|
+
) {}
|
|
86
|
+
|
|
87
|
+
async createUser(data: UserData): Promise<User> {
|
|
88
|
+
const validation = this.validator.validateUserData(data);
|
|
89
|
+
if (!validation.isValid) {
|
|
90
|
+
throw new ValidationError(validation.errors);
|
|
91
|
+
}
|
|
92
|
+
// ... rest of logic
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
\`\`\`
|
|
96
|
+
|
|
97
|
+
## AI Agent Action Steps
|
|
98
|
+
|
|
99
|
+
1. **ANALYZE** the file structure:
|
|
100
|
+
- Count classes (if >1, separate immediately)
|
|
101
|
+
- Identify logical responsibilities within single class
|
|
102
|
+
|
|
103
|
+
2. **IDENTIFY** "child code" to extract:
|
|
104
|
+
- Validation logic → ValidationService
|
|
105
|
+
- Notification logic → NotificationService
|
|
106
|
+
- Data transformation → TransformerService
|
|
107
|
+
- External API calls → ApiService
|
|
108
|
+
- Business rules → RulesEngine
|
|
109
|
+
|
|
110
|
+
3. **CREATE** new service file(s):
|
|
111
|
+
- Start with temporary name: \`XXXX.ts\` or \`ChildService.ts\`
|
|
112
|
+
- Add \`@provideSingleton()\` decorator
|
|
113
|
+
- Move child methods to new class
|
|
114
|
+
|
|
115
|
+
4. **UPDATE** dependency injection:
|
|
116
|
+
- Add to \`TYPES\` constants (if using symbol-based DI)
|
|
117
|
+
- Inject new service into original class constructor
|
|
118
|
+
- Replace direct method calls with \`this.serviceName.method()\`
|
|
119
|
+
|
|
120
|
+
5. **RENAME** extracted file:
|
|
121
|
+
- Read the extracted code to understand its purpose
|
|
122
|
+
- Rename \`XXXX.ts\` to logical name (e.g., \`UserValidationService.ts\`)
|
|
123
|
+
|
|
124
|
+
6. **VERIFY** file sizes:
|
|
125
|
+
- Original file should now be <700 lines
|
|
126
|
+
- Each extracted file should be <700 lines
|
|
127
|
+
- If still too large, extract more services
|
|
128
|
+
|
|
129
|
+
## Examples of Child Responsibilities to Extract
|
|
130
|
+
|
|
131
|
+
| If File Contains | Extract To | Pattern |
|
|
132
|
+
|-----------------|------------|---------|
|
|
133
|
+
| Validation logic (200+ lines) | \`XValidator.ts\` or \`XValidationService.ts\` | Singleton service |
|
|
134
|
+
| Notification logic (150+ lines) | \`XNotifier.ts\` or \`XNotificationService.ts\` | Singleton service |
|
|
135
|
+
| Data transformation (200+ lines) | \`XTransformer.ts\` | Singleton service |
|
|
136
|
+
| External API calls (200+ lines) | \`XApiClient.ts\` | Singleton service |
|
|
137
|
+
| Complex business rules (300+ lines) | \`XRulesEngine.ts\` | Singleton service |
|
|
138
|
+
| Database queries (200+ lines) | \`XRepository.ts\` | Singleton service |
|
|
139
|
+
|
|
140
|
+
## WebPieces Dependency Injection Pattern
|
|
141
|
+
|
|
142
|
+
\`\`\`typescript
|
|
143
|
+
// 1. Define service with @provideSingleton
|
|
144
|
+
import { provideSingleton } from '@webpieces/http-routing';
|
|
145
|
+
|
|
146
|
+
@provideSingleton()
|
|
147
|
+
export class MyService {
|
|
148
|
+
doSomething(): void { /* ... */ }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 2. Inject into consumer
|
|
152
|
+
import { inject } from 'inversify';
|
|
153
|
+
import { TYPES } from './types';
|
|
154
|
+
|
|
155
|
+
@provideSingleton()
|
|
156
|
+
@Controller()
|
|
157
|
+
export class MyController {
|
|
158
|
+
constructor(
|
|
159
|
+
@inject(TYPES.MyService) private service: MyService
|
|
160
|
+
) {}
|
|
161
|
+
}
|
|
162
|
+
\`\`\`
|
|
163
|
+
|
|
164
|
+
Remember: Find the "child code" and pull it down into a new class. Once moved, the code's purpose becomes clear, making it easy to rename to a logical name.
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
// Module-level flag to prevent redundant file creation
|
|
168
|
+
let fileDocCreated = false;
|
|
169
|
+
|
|
170
|
+
function getWorkspaceRoot(context: Rule.RuleContext): string {
|
|
171
|
+
const filename = context.filename || context.getFilename();
|
|
172
|
+
let dir = path.dirname(filename);
|
|
173
|
+
|
|
174
|
+
// Walk up directory tree to find workspace root
|
|
175
|
+
while (dir !== path.dirname(dir)) {
|
|
176
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
177
|
+
if (fs.existsSync(pkgPath)) {
|
|
178
|
+
try {
|
|
179
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
180
|
+
if (pkg.workspaces || pkg.name === 'webpieces-ts') {
|
|
181
|
+
return dir;
|
|
182
|
+
}
|
|
183
|
+
} catch (err: any) {
|
|
184
|
+
//const error = toError(err);
|
|
185
|
+
// Continue searching if JSON parse fails
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
dir = path.dirname(dir);
|
|
189
|
+
}
|
|
190
|
+
return process.cwd(); // Fallback
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function ensureDocFile(docPath: string, content: string): boolean {
|
|
194
|
+
try {
|
|
195
|
+
fs.mkdirSync(path.dirname(docPath), { recursive: true });
|
|
196
|
+
fs.writeFileSync(docPath, content, 'utf-8');
|
|
197
|
+
return true;
|
|
198
|
+
} catch (err: any) {
|
|
199
|
+
//const error = toError(err);
|
|
200
|
+
// Graceful degradation: log warning but don't break lint
|
|
201
|
+
console.warn(`[webpieces] Could not create doc file: ${docPath}`, err);
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function ensureFileDoc(context: Rule.RuleContext): void {
|
|
207
|
+
if (fileDocCreated) return; // Performance: only create once per lint run
|
|
208
|
+
|
|
209
|
+
const workspaceRoot = getWorkspaceRoot(context);
|
|
210
|
+
const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.filesize.md');
|
|
211
|
+
|
|
212
|
+
if (ensureDocFile(docPath, FILE_DOC_CONTENT)) {
|
|
213
|
+
fileDocCreated = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const rule: Rule.RuleModule = {
|
|
218
|
+
meta: {
|
|
219
|
+
type: 'suggestion',
|
|
220
|
+
docs: {
|
|
221
|
+
description: 'Enforce maximum file length',
|
|
222
|
+
category: 'Best Practices',
|
|
223
|
+
recommended: false,
|
|
224
|
+
url: 'https://github.com/deanhiller/webpieces-ts',
|
|
225
|
+
},
|
|
226
|
+
messages: {
|
|
227
|
+
tooLong:
|
|
228
|
+
'AI Agent: READ tmp/webpieces/webpieces.filesize.md for fix instructions. File has {{actual}} lines (max: {{max}})',
|
|
229
|
+
},
|
|
230
|
+
fixable: undefined,
|
|
231
|
+
schema: [
|
|
232
|
+
{
|
|
233
|
+
type: 'object',
|
|
234
|
+
properties: {
|
|
235
|
+
max: {
|
|
236
|
+
type: 'integer',
|
|
237
|
+
minimum: 1,
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
additionalProperties: false,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
create(context: Rule.RuleContext): Rule.RuleListener {
|
|
246
|
+
const options = context.options[0] as FileLinesOptions | undefined;
|
|
247
|
+
const maxLines = options?.max ?? 700;
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
Program(node: any): void {
|
|
251
|
+
ensureFileDoc(context);
|
|
252
|
+
|
|
253
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
254
|
+
const lines = sourceCode.lines;
|
|
255
|
+
const lineCount = lines.length;
|
|
256
|
+
|
|
257
|
+
if (lineCount > maxLines) {
|
|
258
|
+
context.report({
|
|
259
|
+
node,
|
|
260
|
+
messageId: 'tooLong',
|
|
261
|
+
data: {
|
|
262
|
+
actual: String(lineCount),
|
|
263
|
+
max: String(maxLines),
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
export = rule;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to enforce maximum method length
|
|
3
|
+
*
|
|
4
|
+
* Enforces a configurable maximum line count for methods, functions, and arrow functions.
|
|
5
|
+
* Default: 70 lines
|
|
6
|
+
*
|
|
7
|
+
* Configuration:
|
|
8
|
+
* '@webpieces/max-method-lines': ['error', { max: 70 }]
|
|
9
|
+
*/
|
|
10
|
+
import type { Rule } from 'eslint';
|
|
11
|
+
declare const rule: Rule.RuleModule;
|
|
12
|
+
export = rule;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ESLint rule to enforce maximum method length
|
|
4
|
+
*
|
|
5
|
+
* Enforces a configurable maximum line count for methods, functions, and arrow functions.
|
|
6
|
+
* Default: 70 lines
|
|
7
|
+
*
|
|
8
|
+
* Configuration:
|
|
9
|
+
* '@webpieces/max-method-lines': ['error', { max: 70 }]
|
|
10
|
+
*/
|
|
11
|
+
const tslib_1 = require("tslib");
|
|
12
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
13
|
+
const path = tslib_1.__importStar(require("path"));
|
|
14
|
+
const METHOD_DOC_CONTENT = `# AI Agent Instructions: Method Too Long
|
|
15
|
+
|
|
16
|
+
**READ THIS FILE to fix methods that are too long**
|
|
17
|
+
|
|
18
|
+
## Core Principle
|
|
19
|
+
Every method should read like a TABLE OF CONTENTS of a book.
|
|
20
|
+
- Each method call is a "chapter"
|
|
21
|
+
- When you dive into a method, you find another table of contents
|
|
22
|
+
- Keeping methods under 70 lines is achievable with proper extraction
|
|
23
|
+
|
|
24
|
+
## Command: Extract Code into Named Methods
|
|
25
|
+
|
|
26
|
+
### Pattern 1: Extract Loop Bodies
|
|
27
|
+
\`\`\`typescript
|
|
28
|
+
// ❌ BAD: 50 lines embedded in loop
|
|
29
|
+
for (const order of orders) {
|
|
30
|
+
// 20 lines of validation logic
|
|
31
|
+
// 15 lines of processing logic
|
|
32
|
+
// 10 lines of notification logic
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ✅ GOOD: Extracted to named methods
|
|
36
|
+
for (const order of orders) {
|
|
37
|
+
validateOrder(order);
|
|
38
|
+
processOrderItems(order);
|
|
39
|
+
sendNotifications(order);
|
|
40
|
+
}
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
### Pattern 2: Try-Catch Wrapper for Exception Handling
|
|
44
|
+
\`\`\`typescript
|
|
45
|
+
// ✅ GOOD: Separates success path from error handling
|
|
46
|
+
async function handleRequest(req: Request): Promise<Response> {
|
|
47
|
+
try {
|
|
48
|
+
return await executeRequest(req);
|
|
49
|
+
} catch (err: any) {
|
|
50
|
+
const error = toError(err);
|
|
51
|
+
return createErrorResponse(error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
\`\`\`
|
|
55
|
+
|
|
56
|
+
### Pattern 3: Sequential Method Calls (Table of Contents)
|
|
57
|
+
\`\`\`typescript
|
|
58
|
+
// ✅ GOOD: Self-documenting steps
|
|
59
|
+
function processOrder(order: Order): void {
|
|
60
|
+
validateOrderData(order);
|
|
61
|
+
calculateTotals(order);
|
|
62
|
+
applyDiscounts(order);
|
|
63
|
+
processPayment(order);
|
|
64
|
+
updateInventory(order);
|
|
65
|
+
sendConfirmation(order);
|
|
66
|
+
}
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
### Pattern 4: Separate Data Object Creation
|
|
70
|
+
\`\`\`typescript
|
|
71
|
+
// ❌ BAD: 15 lines of inline object creation
|
|
72
|
+
doSomething({ field1: ..., field2: ..., field3: ..., /* 15 more fields */ });
|
|
73
|
+
|
|
74
|
+
// ✅ GOOD: Extract to factory method
|
|
75
|
+
const request = createRequestObject(data);
|
|
76
|
+
doSomething(request);
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
### Pattern 5: Extract Inline Logic to Named Functions
|
|
80
|
+
\`\`\`typescript
|
|
81
|
+
// ❌ BAD: Complex inline logic
|
|
82
|
+
if (user.role === 'admin' && user.permissions.includes('write') && !user.suspended) {
|
|
83
|
+
// 30 lines of admin logic
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ✅ GOOD: Extract to named methods
|
|
87
|
+
if (isAdminWithWriteAccess(user)) {
|
|
88
|
+
performAdminOperation(user);
|
|
89
|
+
}
|
|
90
|
+
\`\`\`
|
|
91
|
+
|
|
92
|
+
## AI Agent Action Steps
|
|
93
|
+
|
|
94
|
+
1. **IDENTIFY** the long method in the error message
|
|
95
|
+
2. **READ** the method to understand its logical sections
|
|
96
|
+
3. **EXTRACT** logical units into separate methods with descriptive names
|
|
97
|
+
4. **REPLACE** inline code with method calls
|
|
98
|
+
5. **VERIFY** each extracted method is <70 lines
|
|
99
|
+
6. **TEST** that functionality remains unchanged
|
|
100
|
+
|
|
101
|
+
## Examples of "Logical Units" to Extract
|
|
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
|
+
|
|
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
|
+
`;
|
|
111
|
+
// Module-level flag to prevent redundant file creation
|
|
112
|
+
let methodDocCreated = false;
|
|
113
|
+
function getWorkspaceRoot(context) {
|
|
114
|
+
const filename = context.filename || context.getFilename();
|
|
115
|
+
let dir = path.dirname(filename);
|
|
116
|
+
// Walk up directory tree to find workspace root
|
|
117
|
+
while (dir !== path.dirname(dir)) {
|
|
118
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
119
|
+
if (fs.existsSync(pkgPath)) {
|
|
120
|
+
try {
|
|
121
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
122
|
+
if (pkg.workspaces || pkg.name === 'webpieces-ts') {
|
|
123
|
+
return dir;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
//const error = toError(err);
|
|
128
|
+
// Continue searching if JSON parse fails
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
dir = path.dirname(dir);
|
|
132
|
+
}
|
|
133
|
+
return process.cwd(); // Fallback
|
|
134
|
+
}
|
|
135
|
+
function ensureDocFile(docPath, content) {
|
|
136
|
+
try {
|
|
137
|
+
fs.mkdirSync(path.dirname(docPath), { recursive: true });
|
|
138
|
+
fs.writeFileSync(docPath, content, 'utf-8');
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
//const error = toError(err);
|
|
143
|
+
// Graceful degradation: log warning but don't break lint
|
|
144
|
+
console.warn(`[webpieces] Could not create doc file: ${docPath}`, err);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function ensureMethodDoc(context) {
|
|
149
|
+
if (methodDocCreated)
|
|
150
|
+
return; // Performance: only create once per lint run
|
|
151
|
+
const workspaceRoot = getWorkspaceRoot(context);
|
|
152
|
+
const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.methods.md');
|
|
153
|
+
if (ensureDocFile(docPath, METHOD_DOC_CONTENT)) {
|
|
154
|
+
methodDocCreated = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const rule = {
|
|
158
|
+
meta: {
|
|
159
|
+
type: 'suggestion',
|
|
160
|
+
docs: {
|
|
161
|
+
description: 'Enforce maximum method length',
|
|
162
|
+
category: 'Best Practices',
|
|
163
|
+
recommended: false,
|
|
164
|
+
url: 'https://github.com/deanhiller/webpieces-ts',
|
|
165
|
+
},
|
|
166
|
+
messages: {
|
|
167
|
+
tooLong: 'AI Agent: READ tmp/webpieces/webpieces.methods.md for fix instructions. Method "{{name}}" has {{actual}} lines (max: {{max}})',
|
|
168
|
+
},
|
|
169
|
+
fixable: undefined,
|
|
170
|
+
schema: [
|
|
171
|
+
{
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {
|
|
174
|
+
max: {
|
|
175
|
+
type: 'integer',
|
|
176
|
+
minimum: 1,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
additionalProperties: false,
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
create(context) {
|
|
184
|
+
const options = context.options[0];
|
|
185
|
+
const maxLines = options?.max ?? 70;
|
|
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
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
FunctionDeclaration: checkFunction,
|
|
250
|
+
FunctionExpression: checkFunction,
|
|
251
|
+
ArrowFunctionExpression: checkFunction,
|
|
252
|
+
MethodDefinition: checkMethod,
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
module.exports = rule;
|
|
257
|
+
//# sourceMappingURL=max-method-lines.js.map
|
|
@@ -0,0 +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"]}
|