@webpieces/eslint-rules 0.0.1 → 0.2.113
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/package.json +3 -2
- package/src/index.d.ts +29 -0
- package/src/index.js +39 -0
- package/src/index.js.map +1 -0
- package/src/rules/catch-error-pattern.d.ts +11 -0
- package/src/rules/{catch-error-pattern.ts → catch-error-pattern.js} +30 -142
- package/src/rules/catch-error-pattern.js.map +1 -0
- package/src/rules/enforce-architecture.d.ts +15 -0
- package/src/rules/{enforce-architecture.ts → enforce-architecture.js} +61 -128
- package/src/rules/enforce-architecture.js.map +1 -0
- package/src/rules/max-file-lines.d.ts +12 -0
- package/src/rules/{max-file-lines.ts → max-file-lines.js} +22 -37
- package/src/rules/max-file-lines.js.map +1 -0
- package/src/rules/max-method-lines.d.ts +12 -0
- package/src/rules/{max-method-lines.ts → max-method-lines.js} +31 -81
- package/src/rules/max-method-lines.js.map +1 -0
- package/src/rules/no-json-property-primitive-type.d.ts +17 -0
- package/src/rules/no-json-property-primitive-type.js +57 -0
- package/src/rules/no-json-property-primitive-type.js.map +1 -0
- package/src/rules/no-mat-cell-def.d.ts +15 -0
- package/src/rules/{no-mat-cell-def.ts → no-mat-cell-def.js} +8 -21
- package/src/rules/no-mat-cell-def.js.map +1 -0
- package/src/rules/no-unmanaged-exceptions.d.ts +22 -0
- package/src/rules/{no-unmanaged-exceptions.ts → no-unmanaged-exceptions.js} +27 -52
- package/src/rules/no-unmanaged-exceptions.js.map +1 -0
- package/src/rules/require-typed-template.d.ts +17 -0
- package/src/rules/{require-typed-template.ts → require-typed-template.js} +11 -31
- package/src/rules/require-typed-template.js.map +1 -0
- package/src/toError.d.ts +5 -0
- package/src/{toError.ts → toError.js} +7 -6
- package/src/toError.js.map +1 -0
- package/.webpieces/instruct-ai/webpieces.exceptions.md +0 -5
- package/.webpieces/instruct-ai/webpieces.filesize.md +0 -146
- package/.webpieces/instruct-ai/webpieces.methods.md +0 -97
- package/LICENSE +0 -373
- package/jest.config.ts +0 -16
- package/project.json +0 -22
- package/src/__tests__/catch-error-pattern.test.ts +0 -374
- package/src/__tests__/max-file-lines.test.ts +0 -207
- package/src/__tests__/max-method-lines.test.ts +0 -258
- package/src/__tests__/no-unmanaged-exceptions.test.ts +0 -359
- package/src/index.ts +0 -38
- package/src/rules/no-json-property-primitive-type.ts +0 -85
- package/tmp/webpieces/webpieces.exceptions.md +0 -5
- package/tsconfig.json +0 -22
- package/tsconfig.lib.json +0 -10
- package/tsconfig.spec.json +0 -14
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"max-file-lines.js","sourceRoot":"","sources":["../../../../../../packages/tooling/eslint-rules/src/rules/max-file-lines.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESLint rule to enforce maximum file length\n *\n * Enforces a configurable maximum line count for files.\n * Default: 700 lines\n *\n * Configuration:\n * '@webpieces/max-file-lines': ['error', { max: 700 }]\n */\n\nimport type { Rule } from 'eslint';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { toError } from '../toError';\n\ninterface FileLinesOptions {\n max: number;\n}\n\nconst FILE_DOC_CONTENT = `# AI Agent Instructions: File Too Long\n\n**READ THIS FILE to fix files that are too long**\n\n## Core Principle\nFiles should contain a SINGLE COHESIVE UNIT.\n- One class per file (Java convention)\n- If class is too large, extract child responsibilities\n- Use dependency injection to compose functionality\n\n## Command: Reduce File Size\n\n### Step 1: Check for Multiple Classes\nIf the file contains multiple classes, **SEPARATE each class into its own file**.\n\n\\`\\`\\`typescript\n// BAD: UserController.ts (multiple classes)\nexport class UserController { /* ... */ }\nexport class UserValidator { /* ... */ }\nexport class UserNotifier { /* ... */ }\n\n// GOOD: Three separate files\n// UserController.ts\nexport class UserController { /* ... */ }\n\n// UserValidator.ts\nexport class UserValidator { /* ... */ }\n\n// UserNotifier.ts\nexport class UserNotifier { /* ... */ }\n\\`\\`\\`\n\n### Step 2: Extract Child Responsibilities (if single class is too large)\n\n#### Pattern: Create New Service Class with Dependency Injection\n\n\\`\\`\\`typescript\n// BAD: UserController.ts (800 lines, single class)\n@provideSingleton()\n@Controller()\nexport class UserController {\n // 200 lines: CRUD operations\n // 300 lines: validation logic\n // 200 lines: notification logic\n // 100 lines: analytics logic\n}\n\n// GOOD: Extract validation service\n// 1. Create UserValidationService.ts\n@provideSingleton()\nexport class UserValidationService {\n validateUserData(data: UserData): ValidationResult {\n // 300 lines of validation logic moved here\n }\n\n validateEmail(email: string): boolean { /* ... */ }\n validatePassword(password: string): boolean { /* ... */ }\n}\n\n// 2. Inject into UserController.ts\n@provideSingleton()\n@Controller()\nexport class UserController {\n constructor(\n @inject(TYPES.UserValidationService)\n private validator: UserValidationService\n ) {}\n\n async createUser(data: UserData): Promise<User> {\n const validation = this.validator.validateUserData(data);\n if (!validation.isValid) {\n throw new ValidationError(validation.errors);\n }\n // ... rest of logic\n }\n}\n\\`\\`\\`\n\n## AI Agent Action Steps\n\n1. **ANALYZE** the file structure:\n - Count classes (if >1, separate immediately)\n - Identify logical responsibilities within single class\n\n2. **IDENTIFY** \"child code\" to extract:\n - Validation logic -> ValidationService\n - Notification logic -> NotificationService\n - Data transformation -> TransformerService\n - External API calls -> ApiService\n - Business rules -> RulesEngine\n\n3. **CREATE** new service file(s):\n - Start with temporary name: \\`XXXX.ts\\` or \\`ChildService.ts\\`\n - Add \\`@provideSingleton()\\` decorator\n - Move child methods to new class\n\n4. **UPDATE** dependency injection:\n - Add to \\`TYPES\\` constants (if using symbol-based DI)\n - Inject new service into original class constructor\n - Replace direct method calls with \\`this.serviceName.method()\\`\n\n5. **RENAME** extracted file:\n - Read the extracted code to understand its purpose\n - Rename \\`XXXX.ts\\` to logical name (e.g., \\`UserValidationService.ts\\`)\n\n6. **VERIFY** file sizes:\n - Original file should now be <700 lines\n - Each extracted file should be <700 lines\n - If still too large, extract more services\n\n## Examples of Child Responsibilities to Extract\n\n| If File Contains | Extract To | Pattern |\n|-----------------|------------|---------|\n| Validation logic (200+ lines) | \\`XValidator.ts\\` or \\`XValidationService.ts\\` | Singleton service |\n| Notification logic (150+ lines) | \\`XNotifier.ts\\` or \\`XNotificationService.ts\\` | Singleton service |\n| Data transformation (200+ lines) | \\`XTransformer.ts\\` | Singleton service |\n| External API calls (200+ lines) | \\`XApiClient.ts\\` | Singleton service |\n| Complex business rules (300+ lines) | \\`XRulesEngine.ts\\` | Singleton service |\n| Database queries (200+ lines) | \\`XRepository.ts\\` | Singleton service |\n\n## WebPieces Dependency Injection Pattern\n\n\\`\\`\\`typescript\n// 1. Define service with @provideSingleton\nimport { provideSingleton } from '@webpieces/http-routing';\n\n@provideSingleton()\nexport class MyService {\n doSomething(): void { /* ... */ }\n}\n\n// 2. Inject into consumer\nimport { inject } from 'inversify';\nimport { TYPES } from './types';\n\n@provideSingleton()\n@Controller()\nexport class MyController {\n constructor(\n @inject(TYPES.MyService) private service: MyService\n ) {}\n}\n\\`\\`\\`\n\nRemember: 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.\n`;\n\n// Module-level flag to prevent redundant file creation\nlet fileDocCreated = 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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\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: unknown) {\n //const error = toError(err);\n void err; // 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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.mkdirSync(path.dirname(docPath), { recursive: true });\n fs.writeFileSync(docPath, content, 'utf-8');\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`[webpieces] Could not create doc file: ${docPath}`, error);\n return false;\n }\n}\n\nfunction ensureFileDoc(context: Rule.RuleContext): void {\n if (fileDocCreated) return; // Performance: only create once per lint run\n\n const workspaceRoot = getWorkspaceRoot(context);\n const docPath = path.join(workspaceRoot, '.webpieces', 'instruct-ai', 'webpieces.filesize.md');\n\n if (ensureDocFile(docPath, FILE_DOC_CONTENT)) {\n fileDocCreated = true;\n }\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Enforce maximum file 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 .webpieces/instruct-ai/webpieces.filesize.md for fix instructions. File 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 FileLinesOptions | undefined;\n const maxLines = options?.max ?? 700;\n\n return {\n // webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\n Program(node: any): void {\n ensureFileDoc(context);\n\n const sourceCode = context.sourceCode || context.getSourceCode();\n const lines = sourceCode.lines;\n const lineCount = lines.length;\n\n if (lineCount > maxLines) {\n context.report({\n node,\n messageId: 'tooLong',\n data: {\n actual: String(lineCount),\n max: String(maxLines),\n },\n });\n }\n },\n };\n },\n};\n\nexport = rule;\n"]}
|
|
@@ -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;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* ESLint rule to enforce maximum method length
|
|
3
4
|
*
|
|
@@ -7,44 +8,10 @@
|
|
|
7
8
|
* Configuration:
|
|
8
9
|
* '@webpieces/max-method-lines': ['error', { max: 70 }]
|
|
9
10
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import { toError } from '../toError';
|
|
15
|
-
|
|
16
|
-
interface MethodLinesOptions {
|
|
17
|
-
max: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties
|
|
21
|
-
interface FunctionNode {
|
|
22
|
-
type:
|
|
23
|
-
| 'FunctionDeclaration'
|
|
24
|
-
| 'FunctionExpression'
|
|
25
|
-
| 'ArrowFunctionExpression'
|
|
26
|
-
| 'MethodDefinition';
|
|
27
|
-
// webpieces-disable no-any-unknown -- ESTree AST dynamic body
|
|
28
|
-
body?: any;
|
|
29
|
-
loc?: {
|
|
30
|
-
start: { line: number };
|
|
31
|
-
end: { line: number };
|
|
32
|
-
};
|
|
33
|
-
key?: {
|
|
34
|
-
name?: string;
|
|
35
|
-
};
|
|
36
|
-
id?: {
|
|
37
|
-
name?: string;
|
|
38
|
-
};
|
|
39
|
-
// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties
|
|
40
|
-
[key: string]: any;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface CheckerContext {
|
|
44
|
-
context: Rule.RuleContext;
|
|
45
|
-
maxLines: number;
|
|
46
|
-
}
|
|
47
|
-
|
|
11
|
+
const tslib_1 = require("tslib");
|
|
12
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
13
|
+
const path = tslib_1.__importStar(require("path"));
|
|
14
|
+
const toError_1 = require("../toError");
|
|
48
15
|
const METHOD_DOC_CONTENT = `# AI Agent Instructions: Method Too Long
|
|
49
16
|
|
|
50
17
|
**READ THIS FILE to fix methods that are too long**
|
|
@@ -143,14 +110,11 @@ if (isAdminWithWriteAccess(user)) {
|
|
|
143
110
|
|
|
144
111
|
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.
|
|
145
112
|
`;
|
|
146
|
-
|
|
147
113
|
// Module-level flag to prevent redundant file creation
|
|
148
114
|
let methodDocCreated = false;
|
|
149
|
-
|
|
150
|
-
function getWorkspaceRoot(context: Rule.RuleContext): string {
|
|
115
|
+
function getWorkspaceRoot(context) {
|
|
151
116
|
const filename = context.filename || context.getFilename();
|
|
152
117
|
let dir = path.dirname(filename);
|
|
153
|
-
|
|
154
118
|
while (dir !== path.dirname(dir)) {
|
|
155
119
|
const pkgPath = path.join(dir, 'package.json');
|
|
156
120
|
if (fs.existsSync(pkgPath)) {
|
|
@@ -160,7 +124,8 @@ function getWorkspaceRoot(context: Rule.RuleContext): string {
|
|
|
160
124
|
if (pkg.workspaces || pkg.name === 'webpieces-ts') {
|
|
161
125
|
return dir;
|
|
162
126
|
}
|
|
163
|
-
}
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
164
129
|
//const error = toError(err);
|
|
165
130
|
void err;
|
|
166
131
|
}
|
|
@@ -169,33 +134,30 @@ function getWorkspaceRoot(context: Rule.RuleContext): string {
|
|
|
169
134
|
}
|
|
170
135
|
return process.cwd();
|
|
171
136
|
}
|
|
172
|
-
|
|
173
|
-
function ensureDocFile(docPath: string, content: string): boolean {
|
|
137
|
+
function ensureDocFile(docPath, content) {
|
|
174
138
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
175
139
|
try {
|
|
176
140
|
fs.mkdirSync(path.dirname(docPath), { recursive: true });
|
|
177
141
|
fs.writeFileSync(docPath, content, 'utf-8');
|
|
178
142
|
return true;
|
|
179
|
-
}
|
|
180
|
-
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const error = (0, toError_1.toError)(err);
|
|
181
146
|
console.warn(`[webpieces] Could not create doc file: ${docPath}`, error);
|
|
182
147
|
return false;
|
|
183
148
|
}
|
|
184
149
|
}
|
|
185
|
-
|
|
186
|
-
function ensureMethodDoc(context: Rule.RuleContext): void {
|
|
150
|
+
function ensureMethodDoc(context) {
|
|
187
151
|
const workspaceRoot = getWorkspaceRoot(context);
|
|
188
152
|
const docPath = path.join(workspaceRoot, '.webpieces', 'instruct-ai', 'webpieces.methods.md');
|
|
189
|
-
|
|
190
153
|
// Check if file exists AND flag is true - if both, skip
|
|
191
|
-
if (methodDocCreated && fs.existsSync(docPath))
|
|
192
|
-
|
|
154
|
+
if (methodDocCreated && fs.existsSync(docPath))
|
|
155
|
+
return;
|
|
193
156
|
if (ensureDocFile(docPath, METHOD_DOC_CONTENT)) {
|
|
194
157
|
methodDocCreated = true;
|
|
195
158
|
}
|
|
196
159
|
}
|
|
197
|
-
|
|
198
|
-
function getFunctionName(funcNode: FunctionNode): string {
|
|
160
|
+
function getFunctionName(funcNode) {
|
|
199
161
|
if (funcNode.type === 'FunctionDeclaration' && funcNode.id?.name) {
|
|
200
162
|
return funcNode.id.name;
|
|
201
163
|
}
|
|
@@ -204,9 +166,8 @@ function getFunctionName(funcNode: FunctionNode): string {
|
|
|
204
166
|
}
|
|
205
167
|
return 'anonymous';
|
|
206
168
|
}
|
|
207
|
-
|
|
208
169
|
// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties
|
|
209
|
-
function reportTooLong(ctx
|
|
170
|
+
function reportTooLong(ctx, node, name, lineCount) {
|
|
210
171
|
ctx.context.report({
|
|
211
172
|
node,
|
|
212
173
|
messageId: 'tooLong',
|
|
@@ -217,42 +178,34 @@ function reportTooLong(ctx: CheckerContext, node: any, name: string, lineCount:
|
|
|
217
178
|
},
|
|
218
179
|
});
|
|
219
180
|
}
|
|
220
|
-
|
|
221
181
|
// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties
|
|
222
|
-
function checkFunctionNode(ctx
|
|
182
|
+
function checkFunctionNode(ctx, node) {
|
|
223
183
|
ensureMethodDoc(ctx.context);
|
|
224
|
-
const funcNode = node
|
|
225
|
-
|
|
184
|
+
const funcNode = node;
|
|
226
185
|
// Skip function expressions inside method definitions
|
|
227
186
|
if (funcNode.type === 'FunctionExpression' && funcNode['parent']?.type === 'MethodDefinition') {
|
|
228
187
|
return;
|
|
229
188
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
189
|
+
if (!funcNode.loc || !funcNode.body)
|
|
190
|
+
return;
|
|
233
191
|
const name = getFunctionName(funcNode);
|
|
234
192
|
const lineCount = funcNode.loc.end.line - funcNode.loc.start.line + 1;
|
|
235
|
-
|
|
236
193
|
if (lineCount > ctx.maxLines) {
|
|
237
194
|
reportTooLong(ctx, funcNode, name, lineCount);
|
|
238
195
|
}
|
|
239
196
|
}
|
|
240
|
-
|
|
241
197
|
// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties
|
|
242
|
-
function checkMethodNode(ctx
|
|
198
|
+
function checkMethodNode(ctx, node) {
|
|
243
199
|
ensureMethodDoc(ctx.context);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
200
|
+
if (!node.loc || !node.value)
|
|
201
|
+
return;
|
|
247
202
|
const name = node.key?.name || 'anonymous';
|
|
248
203
|
const lineCount = node.loc.end.line - node.loc.start.line + 1;
|
|
249
|
-
|
|
250
204
|
if (lineCount > ctx.maxLines) {
|
|
251
205
|
reportTooLong(ctx, node, name, lineCount);
|
|
252
206
|
}
|
|
253
207
|
}
|
|
254
|
-
|
|
255
|
-
const rule: Rule.RuleModule = {
|
|
208
|
+
const rule = {
|
|
256
209
|
meta: {
|
|
257
210
|
type: 'suggestion',
|
|
258
211
|
docs: {
|
|
@@ -262,8 +215,7 @@ const rule: Rule.RuleModule = {
|
|
|
262
215
|
url: 'https://github.com/deanhiller/webpieces-ts',
|
|
263
216
|
},
|
|
264
217
|
messages: {
|
|
265
|
-
tooLong:
|
|
266
|
-
'AI Agent: READ .webpieces/instruct-ai/webpieces.methods.md for fix instructions. Method "{{name}}" has {{actual}} lines (max: {{max}})',
|
|
218
|
+
tooLong: 'AI Agent: READ .webpieces/instruct-ai/webpieces.methods.md for fix instructions. Method "{{name}}" has {{actual}} lines (max: {{max}})',
|
|
267
219
|
},
|
|
268
220
|
fixable: undefined,
|
|
269
221
|
schema: [
|
|
@@ -279,11 +231,9 @@ const rule: Rule.RuleModule = {
|
|
|
279
231
|
},
|
|
280
232
|
],
|
|
281
233
|
},
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
const ctx: CheckerContext = { context, maxLines: options?.max ?? 70 };
|
|
286
|
-
|
|
234
|
+
create(context) {
|
|
235
|
+
const options = context.options[0];
|
|
236
|
+
const ctx = { context, maxLines: options?.max ?? 70 };
|
|
287
237
|
return {
|
|
288
238
|
FunctionDeclaration: (node) => checkFunctionNode(ctx, node),
|
|
289
239
|
FunctionExpression: (node) => checkFunctionNode(ctx, node),
|
|
@@ -292,5 +242,5 @@ const rule: Rule.RuleModule = {
|
|
|
292
242
|
};
|
|
293
243
|
},
|
|
294
244
|
};
|
|
295
|
-
|
|
296
|
-
|
|
245
|
+
module.exports = rule;
|
|
246
|
+
//# sourceMappingURL=max-method-lines.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"max-method-lines.js","sourceRoot":"","sources":["../../../../../../packages/tooling/eslint-rules/src/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';\nimport { toError } from '../toError';\n\ninterface MethodLinesOptions {\n max: number;\n}\n\n// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\ninterface FunctionNode {\n type:\n | 'FunctionDeclaration'\n | 'FunctionExpression'\n | 'ArrowFunctionExpression'\n | 'MethodDefinition';\n // webpieces-disable no-any-unknown -- ESTree AST dynamic body\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 // webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\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: unknown) {\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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.mkdirSync(path.dirname(docPath), { recursive: true });\n fs.writeFileSync(docPath, content, 'utf-8');\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`[webpieces] Could not create doc file: ${docPath}`, error);\n return false;\n }\n}\n\nfunction ensureMethodDoc(context: Rule.RuleContext): void {\n const workspaceRoot = getWorkspaceRoot(context);\n const docPath = path.join(workspaceRoot, '.webpieces', 'instruct-ai', '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\n// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\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\n// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\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\n// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\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 .webpieces/instruct-ai/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"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-json-property-primitive-type
|
|
3
|
+
*
|
|
4
|
+
* Bans @JsonProperty({ type: String }), @JsonProperty({ type: Number }),
|
|
5
|
+
* and @JsonProperty({ type: Boolean }).
|
|
6
|
+
*
|
|
7
|
+
* These pass the TypeScript build but break production deserialization.
|
|
8
|
+
* The typescript-json-serializer `type` option expects class constructors,
|
|
9
|
+
* not JavaScript primitive constructors.
|
|
10
|
+
*
|
|
11
|
+
* Correct usage:
|
|
12
|
+
* @JsonProperty() - for primitive arrays (string[], number[], boolean[])
|
|
13
|
+
* @JsonProperty({ type: MyDtoClass }) - for class types only
|
|
14
|
+
*/
|
|
15
|
+
import type { Rule } from 'eslint';
|
|
16
|
+
declare const rule: Rule.RuleModule;
|
|
17
|
+
export = rule;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ESLint rule: no-json-property-primitive-type
|
|
4
|
+
*
|
|
5
|
+
* Bans @JsonProperty({ type: String }), @JsonProperty({ type: Number }),
|
|
6
|
+
* and @JsonProperty({ type: Boolean }).
|
|
7
|
+
*
|
|
8
|
+
* These pass the TypeScript build but break production deserialization.
|
|
9
|
+
* The typescript-json-serializer `type` option expects class constructors,
|
|
10
|
+
* not JavaScript primitive constructors.
|
|
11
|
+
*
|
|
12
|
+
* Correct usage:
|
|
13
|
+
* @JsonProperty() - for primitive arrays (string[], number[], boolean[])
|
|
14
|
+
* @JsonProperty({ type: MyDtoClass }) - for class types only
|
|
15
|
+
*/
|
|
16
|
+
const BANNED_PRIMITIVES = ['String', 'Number', 'Boolean'];
|
|
17
|
+
const rule = {
|
|
18
|
+
meta: {
|
|
19
|
+
type: 'problem',
|
|
20
|
+
docs: {
|
|
21
|
+
description: 'Ban @JsonProperty({ type: String/Number/Boolean }) — breaks production deserialization',
|
|
22
|
+
},
|
|
23
|
+
messages: {
|
|
24
|
+
noPrimitiveType: '@JsonProperty({ type: {{ primitive }} }) breaks production deserialization. ' +
|
|
25
|
+
'For primitive arrays (string[], number[], boolean[]), use @JsonProperty() with ' +
|
|
26
|
+
'no type parameter. The type option is only for class types: ' +
|
|
27
|
+
'@JsonProperty({ type: MyDtoClass }).',
|
|
28
|
+
},
|
|
29
|
+
schema: [],
|
|
30
|
+
},
|
|
31
|
+
create(context) {
|
|
32
|
+
return {
|
|
33
|
+
CallExpression(node) {
|
|
34
|
+
if (node.callee.name !== 'JsonProperty')
|
|
35
|
+
return;
|
|
36
|
+
const arg = node.arguments[0];
|
|
37
|
+
if (!arg || arg.type !== 'ObjectExpression')
|
|
38
|
+
return;
|
|
39
|
+
const objArg = arg;
|
|
40
|
+
for (const prop of objArg.properties) {
|
|
41
|
+
if (prop.key?.name === 'type' &&
|
|
42
|
+
prop.value?.type === 'Identifier' &&
|
|
43
|
+
BANNED_PRIMITIVES.includes(prop.value.name)) {
|
|
44
|
+
context.report({
|
|
45
|
+
// webpieces-disable no-any-unknown -- ESTree AST cast for ESLint report
|
|
46
|
+
node: prop,
|
|
47
|
+
messageId: 'noPrimitiveType',
|
|
48
|
+
data: { primitive: prop.value.name },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
module.exports = rule;
|
|
57
|
+
//# sourceMappingURL=no-json-property-primitive-type.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-json-property-primitive-type.js","sourceRoot":"","sources":["../../../../../../packages/tooling/eslint-rules/src/rules/no-json-property-primitive-type.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;AA2BH,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;AAE1D,MAAM,IAAI,GAAoB;IAC1B,IAAI,EAAE;QACF,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACF,WAAW,EACP,wFAAwF;SAC/F;QACD,QAAQ,EAAE;YACN,eAAe,EACX,8EAA8E;gBAC9E,iFAAiF;gBACjF,8DAA8D;gBAC9D,sCAAsC;SAC7C;QACD,MAAM,EAAE,EAAE;KACb;IACD,MAAM,CAAC,OAAyB;QAC5B,OAAO;YACH,cAAc,CAAC,IAAwB;gBACnC,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,cAAc;oBAAE,OAAO;gBAChD,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;gBAC9B,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,KAAK,kBAAkB;oBAAE,OAAO;gBACpD,MAAM,MAAM,GAAG,GAA2B,CAAC;gBAC3C,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;oBACnC,IACI,IAAI,CAAC,GAAG,EAAE,IAAI,KAAK,MAAM;wBACzB,IAAI,CAAC,KAAK,EAAE,IAAI,KAAK,YAAY;wBACjC,iBAAiB,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,IAAK,CAAC,EAC9C,CAAC;wBACC,OAAO,CAAC,MAAM,CAAC;4BACX,wEAAwE;4BACxE,IAAI,EAAE,IAA4B;4BAClC,SAAS,EAAE,iBAAiB;4BAC5B,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,IAAK,EAAE;yBACxC,CAAC,CAAC;oBACP,CAAC;gBACL,CAAC;YACL,CAAC;SACJ,CAAC;IACN,CAAC;CACJ,CAAC;AAEF,iBAAS,IAAI,CAAC","sourcesContent":["/**\n * ESLint rule: no-json-property-primitive-type\n *\n * Bans @JsonProperty({ type: String }), @JsonProperty({ type: Number }),\n * and @JsonProperty({ type: Boolean }).\n *\n * These pass the TypeScript build but break production deserialization.\n * The typescript-json-serializer `type` option expects class constructors,\n * not JavaScript primitive constructors.\n *\n * Correct usage:\n * @JsonProperty() - for primitive arrays (string[], number[], boolean[])\n * @JsonProperty({ type: MyDtoClass }) - for class types only\n */\n\nimport type { Rule } from 'eslint';\n\n// webpieces-disable no-any-unknown -- ESTree AST node interfaces require any for dynamic properties\ninterface CallExpressionNode {\n type: 'CallExpression';\n // webpieces-disable no-any-unknown -- ESTree AST dynamic callee\n callee: { name?: string; [key: string]: any };\n // webpieces-disable no-any-unknown -- ESTree AST dynamic arguments array\n arguments: any[];\n // webpieces-disable no-any-unknown -- ESTree AST index signature\n [key: string]: any;\n}\n\ninterface ObjectExpressionNode {\n type: 'ObjectExpression';\n properties: PropertyNode[];\n}\n\ninterface PropertyNode {\n key?: { name?: string };\n value?: { type?: string; name?: string };\n // webpieces-disable no-any-unknown -- ESTree AST index signature\n [key: string]: any;\n}\n\nconst BANNED_PRIMITIVES = ['String', 'Number', 'Boolean'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Ban @JsonProperty({ type: String/Number/Boolean }) — breaks production deserialization',\n },\n messages: {\n noPrimitiveType:\n '@JsonProperty({ type: {{ primitive }} }) breaks production deserialization. ' +\n 'For primitive arrays (string[], number[], boolean[]), use @JsonProperty() with ' +\n 'no type parameter. The type option is only for class types: ' +\n '@JsonProperty({ type: MyDtoClass }).',\n },\n schema: [],\n },\n create(context: Rule.RuleContext): Rule.RuleListener {\n return {\n CallExpression(node: CallExpressionNode): void {\n if (node.callee.name !== 'JsonProperty') return;\n const arg = node.arguments[0];\n if (!arg || arg.type !== 'ObjectExpression') return;\n const objArg = arg as ObjectExpressionNode;\n for (const prop of objArg.properties) {\n if (\n prop.key?.name === 'type' &&\n prop.value?.type === 'Identifier' &&\n BANNED_PRIMITIVES.includes(prop.value.name!)\n ) {\n context.report({\n // webpieces-disable no-any-unknown -- ESTree AST cast for ESLint report\n node: prop as unknown as Rule.Node,\n messageId: 'noPrimitiveType',\n data: { primitive: prop.value.name! },\n });\n }\n }\n },\n };\n },\n};\n\nexport = rule;\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-mat-cell-def
|
|
3
|
+
*
|
|
4
|
+
* Bans *matCellDef and *matHeaderCellDef in Angular HTML templates.
|
|
5
|
+
* New files should use the div-grid table pattern instead of mat-table.
|
|
6
|
+
*
|
|
7
|
+
* Works with @angular-eslint/template-parser AST where structural directives
|
|
8
|
+
* (*matCellDef) are desugared into Template nodes with templateAttrs[].
|
|
9
|
+
*
|
|
10
|
+
* NOTE: This rule only works when files are parsed with @angular-eslint/template-parser.
|
|
11
|
+
* It is intended for Angular HTML template files (**.html).
|
|
12
|
+
*/
|
|
13
|
+
import type { Rule } from 'eslint';
|
|
14
|
+
declare const rule: Rule.RuleModule;
|
|
15
|
+
export = rule;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* ESLint rule: no-mat-cell-def
|
|
3
4
|
*
|
|
@@ -10,36 +11,22 @@
|
|
|
10
11
|
* NOTE: This rule only works when files are parsed with @angular-eslint/template-parser.
|
|
11
12
|
* It is intended for Angular HTML template files (**.html).
|
|
12
13
|
*/
|
|
13
|
-
|
|
14
|
-
import type { Rule } from 'eslint';
|
|
15
|
-
|
|
16
|
-
// webpieces-disable no-any-unknown -- Angular template AST node interfaces
|
|
17
|
-
// These interfaces represent the Template node shape from @angular-eslint/template-parser.
|
|
18
|
-
// We define them inline since the parser is not a dependency of this plugin.
|
|
19
|
-
interface AngularTemplateNode {
|
|
20
|
-
templateAttrs?: Array<{ name: string }>;
|
|
21
|
-
// webpieces-disable no-any-unknown -- ESTree AST index signature
|
|
22
|
-
[key: string]: any;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
14
|
const BANNED_DIRECTIVES = ['matCellDef', 'matHeaderCellDef'];
|
|
26
|
-
|
|
27
|
-
const rule: Rule.RuleModule = {
|
|
15
|
+
const rule = {
|
|
28
16
|
meta: {
|
|
29
17
|
type: 'problem',
|
|
30
18
|
docs: {
|
|
31
19
|
description: 'Ban *matCellDef and *matHeaderCellDef — use div-grid tables instead',
|
|
32
20
|
},
|
|
33
21
|
messages: {
|
|
34
|
-
noMatCellDef:
|
|
35
|
-
'*{{ directive }} is banned in new files. Use the div-grid table pattern instead. ' +
|
|
22
|
+
noMatCellDef: '*{{ directive }} is banned in new files. Use the div-grid table pattern instead. ' +
|
|
36
23
|
'Div-grid tables are inherently type-safe with @for loops + strictTemplates.',
|
|
37
24
|
},
|
|
38
25
|
schema: [],
|
|
39
26
|
},
|
|
40
|
-
create(context
|
|
27
|
+
create(context) {
|
|
41
28
|
return {
|
|
42
|
-
Template(node
|
|
29
|
+
Template(node) {
|
|
43
30
|
// Structural directives (*matCellDef) are desugared into Template nodes.
|
|
44
31
|
// The directive name appears in node.templateAttrs as either a
|
|
45
32
|
// BoundAttribute or TextAttribute.
|
|
@@ -48,7 +35,7 @@ const rule: Rule.RuleModule = {
|
|
|
48
35
|
if (BANNED_DIRECTIVES.includes(attr.name)) {
|
|
49
36
|
context.report({
|
|
50
37
|
// webpieces-disable no-any-unknown -- ESTree AST cast for ESLint report
|
|
51
|
-
node: node
|
|
38
|
+
node: node,
|
|
52
39
|
messageId: 'noMatCellDef',
|
|
53
40
|
data: { directive: attr.name },
|
|
54
41
|
});
|
|
@@ -58,5 +45,5 @@ const rule: Rule.RuleModule = {
|
|
|
58
45
|
};
|
|
59
46
|
},
|
|
60
47
|
};
|
|
61
|
-
|
|
62
|
-
|
|
48
|
+
module.exports = rule;
|
|
49
|
+
//# sourceMappingURL=no-mat-cell-def.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-mat-cell-def.js","sourceRoot":"","sources":["../../../../../../packages/tooling/eslint-rules/src/rules/no-mat-cell-def.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;AAaH,MAAM,iBAAiB,GAAG,CAAC,YAAY,EAAE,kBAAkB,CAAC,CAAC;AAE7D,MAAM,IAAI,GAAoB;IAC1B,IAAI,EAAE;QACF,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACF,WAAW,EAAE,qEAAqE;SACrF;QACD,QAAQ,EAAE;YACN,YAAY,EACR,mFAAmF;gBACnF,6EAA6E;SACpF;QACD,MAAM,EAAE,EAAE;KACb;IACD,MAAM,CAAC,OAAyB;QAC5B,OAAO;YACH,QAAQ,CAAC,IAAyB;gBAC9B,yEAAyE;gBACzE,+DAA+D;gBAC/D,mCAAmC;gBACnC,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC;gBACvC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;oBACvB,IAAI,iBAAiB,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;wBACxC,OAAO,CAAC,MAAM,CAAC;4BACX,wEAAwE;4BACxE,IAAI,EAAE,IAA4B;4BAClC,SAAS,EAAE,cAAc;4BACzB,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE;yBACjC,CAAC,CAAC;oBACP,CAAC;gBACL,CAAC;YACL,CAAC;SACJ,CAAC;IACN,CAAC;CACJ,CAAC;AAEF,iBAAS,IAAI,CAAC","sourcesContent":["/**\n * ESLint rule: no-mat-cell-def\n *\n * Bans *matCellDef and *matHeaderCellDef in Angular HTML templates.\n * New files should use the div-grid table pattern instead of mat-table.\n *\n * Works with @angular-eslint/template-parser AST where structural directives\n * (*matCellDef) are desugared into Template nodes with templateAttrs[].\n *\n * NOTE: This rule only works when files are parsed with @angular-eslint/template-parser.\n * It is intended for Angular HTML template files (**.html).\n */\n\nimport type { Rule } from 'eslint';\n\n// webpieces-disable no-any-unknown -- Angular template AST node interfaces\n// These interfaces represent the Template node shape from @angular-eslint/template-parser.\n// We define them inline since the parser is not a dependency of this plugin.\ninterface AngularTemplateNode {\n templateAttrs?: Array<{ name: string }>;\n // webpieces-disable no-any-unknown -- ESTree AST index signature\n [key: string]: any;\n}\n\nconst BANNED_DIRECTIVES = ['matCellDef', 'matHeaderCellDef'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Ban *matCellDef and *matHeaderCellDef — use div-grid tables instead',\n },\n messages: {\n noMatCellDef:\n '*{{ directive }} is banned in new files. Use the div-grid table pattern instead. ' +\n 'Div-grid tables are inherently type-safe with @for loops + strictTemplates.',\n },\n schema: [],\n },\n create(context: Rule.RuleContext): Rule.RuleListener {\n return {\n Template(node: AngularTemplateNode): void {\n // Structural directives (*matCellDef) are desugared into Template nodes.\n // The directive name appears in node.templateAttrs as either a\n // BoundAttribute or TextAttribute.\n const attrs = node.templateAttrs || [];\n for (const attr of attrs) {\n if (BANNED_DIRECTIVES.includes(attr.name)) {\n context.report({\n // webpieces-disable no-any-unknown -- ESTree AST cast for ESLint report\n node: node as unknown as Rule.Node,\n messageId: 'noMatCellDef',\n data: { directive: attr.name },\n });\n }\n }\n },\n };\n },\n};\n\nexport = rule;\n"]}
|
|
@@ -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;
|