@webpieces/eslint-rules 0.0.1 → 0.2.114
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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* ESLint rule to discourage try-catch blocks outside test files
|
|
3
4
|
*
|
|
@@ -17,45 +18,32 @@
|
|
|
17
18
|
* - Batch processing where partial failure is expected
|
|
18
19
|
* - Resource cleanup (with approval)
|
|
19
20
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import * as path from 'path';
|
|
24
|
-
import { toError } from '../toError';
|
|
25
|
-
|
|
26
|
-
// webpieces-disable no-any-unknown -- ESTree AST node interface
|
|
27
|
-
interface TryStatementNode {
|
|
28
|
-
handler?: unknown;
|
|
29
|
-
}
|
|
30
|
-
|
|
21
|
+
const tslib_1 = require("tslib");
|
|
22
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
23
|
+
const path = tslib_1.__importStar(require("path"));
|
|
31
24
|
/**
|
|
32
25
|
* Determines if a file is a test file based on naming conventions
|
|
33
26
|
* Test files are auto-allowed to use try-catch blocks
|
|
34
27
|
*/
|
|
35
|
-
function isTestFile(filename
|
|
28
|
+
function isTestFile(filename) {
|
|
36
29
|
const normalizedPath = filename.toLowerCase();
|
|
37
|
-
|
|
38
30
|
// Check file extensions
|
|
39
31
|
if (normalizedPath.endsWith('.test.ts') || normalizedPath.endsWith('.spec.ts')) {
|
|
40
32
|
return true;
|
|
41
33
|
}
|
|
42
|
-
|
|
43
34
|
// Check directory names (cross-platform)
|
|
44
35
|
if (normalizedPath.includes('/__tests__/') || normalizedPath.includes('\\__tests__\\')) {
|
|
45
36
|
return true;
|
|
46
37
|
}
|
|
47
|
-
|
|
48
38
|
return false;
|
|
49
39
|
}
|
|
50
|
-
|
|
51
40
|
/**
|
|
52
41
|
* Finds the workspace root by walking up the directory tree
|
|
53
42
|
* Looks for package.json with workspaces or name === 'webpieces-ts'
|
|
54
43
|
*/
|
|
55
|
-
function getWorkspaceRoot(context
|
|
44
|
+
function getWorkspaceRoot(context) {
|
|
56
45
|
const filename = context.filename || context.getFilename();
|
|
57
46
|
let dir = path.dirname(filename);
|
|
58
|
-
|
|
59
47
|
// Walk up directory tree
|
|
60
48
|
for (let i = 0; i < 10; i++) {
|
|
61
49
|
const pkgPath = path.join(dir, 'package.json');
|
|
@@ -67,46 +55,43 @@ function getWorkspaceRoot(context: Rule.RuleContext): string {
|
|
|
67
55
|
if (pkg.workspaces || pkg.name === 'webpieces-ts') {
|
|
68
56
|
return dir;
|
|
69
57
|
}
|
|
70
|
-
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
71
60
|
//const error = toError(err);
|
|
72
61
|
void err; // Invalid JSON, keep searching
|
|
73
62
|
}
|
|
74
63
|
}
|
|
75
|
-
|
|
76
64
|
const parentDir = path.dirname(dir);
|
|
77
|
-
if (parentDir === dir)
|
|
65
|
+
if (parentDir === dir)
|
|
66
|
+
break; // Reached filesystem root
|
|
78
67
|
dir = parentDir;
|
|
79
68
|
}
|
|
80
|
-
|
|
81
69
|
// Fallback: return current directory
|
|
82
70
|
return process.cwd();
|
|
83
71
|
}
|
|
84
|
-
|
|
85
72
|
/**
|
|
86
73
|
* Ensures a documentation file exists at the given path
|
|
87
74
|
* Creates parent directories if needed
|
|
88
75
|
*/
|
|
89
|
-
function ensureDocFile(docPath
|
|
76
|
+
function ensureDocFile(docPath, content) {
|
|
90
77
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
91
78
|
try {
|
|
92
79
|
const dir = path.dirname(docPath);
|
|
93
80
|
if (!fs.existsSync(dir)) {
|
|
94
81
|
fs.mkdirSync(dir, { recursive: true });
|
|
95
82
|
}
|
|
96
|
-
|
|
97
83
|
// Only write if file doesn't exist or is empty
|
|
98
84
|
if (!fs.existsSync(docPath) || fs.readFileSync(docPath, 'utf-8').trim() === '') {
|
|
99
85
|
fs.writeFileSync(docPath, content, 'utf-8');
|
|
100
86
|
}
|
|
101
|
-
|
|
102
87
|
return true;
|
|
103
|
-
}
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
104
90
|
//const error = toError(err);
|
|
105
91
|
void err; // Silently fail - don't break linting if file creation fails
|
|
106
92
|
return false;
|
|
107
93
|
}
|
|
108
94
|
}
|
|
109
|
-
|
|
110
95
|
/**
|
|
111
96
|
* Ensures the exception documentation markdown file exists
|
|
112
97
|
* Only creates file once per lint run using module-level flag
|
|
@@ -114,39 +99,34 @@ function ensureDocFile(docPath: string, content: string): boolean {
|
|
|
114
99
|
* Reads from the template file packaged with @webpieces/webpieces-rules
|
|
115
100
|
* and copies it to .webpieces/instruct-ai/ for AI agents to read.
|
|
116
101
|
*/
|
|
117
|
-
function ensureExceptionDoc(context
|
|
118
|
-
if (exceptionDocCreated)
|
|
119
|
-
|
|
102
|
+
function ensureExceptionDoc(context) {
|
|
103
|
+
if (exceptionDocCreated)
|
|
104
|
+
return;
|
|
120
105
|
const workspaceRoot = getWorkspaceRoot(context);
|
|
121
106
|
const docPath = path.join(workspaceRoot, '.webpieces', 'instruct-ai', 'webpieces.exceptions.md');
|
|
122
|
-
|
|
123
107
|
// Read from the template file packaged with the npm module
|
|
124
108
|
// Path: from eslint-plugin/rules/ -> ../../templates/
|
|
125
109
|
const templatePath = path.join(__dirname, '..', '..', 'templates', 'webpieces.exceptions.md');
|
|
126
|
-
|
|
127
|
-
let content: string;
|
|
110
|
+
let content;
|
|
128
111
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
129
112
|
try {
|
|
130
113
|
content = fs.readFileSync(templatePath, 'utf-8');
|
|
131
|
-
}
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
132
116
|
//const error = toError(err);
|
|
133
117
|
void err;
|
|
134
118
|
// Fallback message if template not found (shouldn't happen in published package)
|
|
135
119
|
content = `# Exception Documentation Not Found\n\nTemplate file not found at: ${templatePath}\n\nPlease ensure @webpieces/webpieces-rules is properly installed.`;
|
|
136
120
|
}
|
|
137
|
-
|
|
138
121
|
if (ensureDocFile(docPath, content)) {
|
|
139
122
|
exceptionDocCreated = true;
|
|
140
123
|
}
|
|
141
124
|
}
|
|
142
|
-
|
|
143
125
|
// Module-level flag to prevent redundant markdown file creation
|
|
144
126
|
let exceptionDocCreated = false;
|
|
145
|
-
|
|
146
127
|
// NOTE: Documentation content moved to templates/webpieces.exceptions.md
|
|
147
128
|
// The ensureExceptionDoc function reads from that file at runtime.
|
|
148
|
-
|
|
149
|
-
const rule: Rule.RuleModule = {
|
|
129
|
+
const rule = {
|
|
150
130
|
meta: {
|
|
151
131
|
type: 'problem',
|
|
152
132
|
docs: {
|
|
@@ -156,39 +136,34 @@ const rule: Rule.RuleModule = {
|
|
|
156
136
|
url: 'https://github.com/deanhiller/webpieces-ts/blob/main/CLAUDE.md#exception-handling-philosophy',
|
|
157
137
|
},
|
|
158
138
|
messages: {
|
|
159
|
-
noUnmanagedExceptions:
|
|
160
|
-
'AI Agent: READ .webpieces/instruct-ai/webpieces.exceptions.md for context. Try-catch blocks are discouraged - use global error handlers instead. Only allowed in test files or with eslint-disable comment.',
|
|
139
|
+
noUnmanagedExceptions: 'AI Agent: READ .webpieces/instruct-ai/webpieces.exceptions.md for context. Try-catch blocks are discouraged - use global error handlers instead. Only allowed in test files or with eslint-disable comment.',
|
|
161
140
|
},
|
|
162
141
|
fixable: undefined,
|
|
163
142
|
schema: [],
|
|
164
143
|
},
|
|
165
|
-
|
|
166
|
-
create(context: Rule.RuleContext): Rule.RuleListener {
|
|
144
|
+
create(context) {
|
|
167
145
|
return {
|
|
168
146
|
// webpieces-disable no-any-unknown -- ESLint visitor callback parameter type
|
|
169
|
-
TryStatement(node
|
|
147
|
+
TryStatement(node) {
|
|
170
148
|
// Skip try..finally blocks (no catch handler, no exception handling)
|
|
171
149
|
// webpieces-disable no-any-unknown -- ESTree AST node type assertion
|
|
172
|
-
if (!
|
|
150
|
+
if (!node.handler) {
|
|
173
151
|
return;
|
|
174
152
|
}
|
|
175
|
-
|
|
176
153
|
// Auto-allow in test files
|
|
177
154
|
const filename = context.filename || context.getFilename();
|
|
178
155
|
if (isTestFile(filename)) {
|
|
179
156
|
return;
|
|
180
157
|
}
|
|
181
|
-
|
|
182
158
|
// Has catch block outside test file - report violation
|
|
183
159
|
ensureExceptionDoc(context);
|
|
184
160
|
context.report({
|
|
185
|
-
node: node
|
|
161
|
+
node: node,
|
|
186
162
|
messageId: 'noUnmanagedExceptions',
|
|
187
163
|
});
|
|
188
164
|
},
|
|
189
165
|
};
|
|
190
166
|
},
|
|
191
167
|
};
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
168
|
+
module.exports = rule;
|
|
169
|
+
//# sourceMappingURL=no-unmanaged-exceptions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-unmanaged-exceptions.js","sourceRoot":"","sources":["../../../../../../packages/tooling/eslint-rules/src/rules/no-unmanaged-exceptions.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESLint rule to discourage try-catch blocks outside test files\n *\n * Works alongside catch-error-pattern rule:\n * - catch-error-pattern: Enforces HOW to handle exceptions (with toError())\n * - no-unmanaged-exceptions: Enforces WHERE try-catch is allowed (tests only by default)\n *\n * Philosophy: Exceptions should bubble to global error handlers where they are logged\n * with traceId and stored for debugging via /debugLocal and /debugCloud endpoints.\n * Local try-catch blocks break this architecture and create blind spots in production.\n *\n * Auto-allowed in:\n * - Test files (.test.ts, .spec.ts, __tests__/)\n *\n * Requires eslint-disable comment in:\n * - Retry loops with exponential backoff\n * - Batch processing where partial failure is expected\n * - Resource cleanup (with approval)\n */\n\nimport type { Rule } from 'eslint';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { toError } from '../toError';\n\n// webpieces-disable no-any-unknown -- ESTree AST node interface\ninterface TryStatementNode {\n handler?: unknown;\n}\n\n/**\n * Determines if a file is a test file based on naming conventions\n * Test files are auto-allowed to use try-catch blocks\n */\nfunction isTestFile(filename: string): boolean {\n const normalizedPath = filename.toLowerCase();\n\n // Check file extensions\n if (normalizedPath.endsWith('.test.ts') || normalizedPath.endsWith('.spec.ts')) {\n return true;\n }\n\n // Check directory names (cross-platform)\n if (normalizedPath.includes('/__tests__/') || normalizedPath.includes('\\\\__tests__\\\\')) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Finds the workspace root by walking up the directory tree\n * Looks for package.json with workspaces or name === 'webpieces-ts'\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\n for (let i = 0; i < 10; i++) {\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 // Check if this is the root workspace\n if (pkg.workspaces || pkg.name === 'webpieces-ts') {\n return dir;\n }\n } catch (err: unknown) {\n //const error = toError(err);\n void err; // Invalid JSON, keep searching\n }\n }\n\n const parentDir = path.dirname(dir);\n if (parentDir === dir) break; // Reached filesystem root\n dir = parentDir;\n }\n\n // Fallback: return current directory\n return process.cwd();\n}\n\n/**\n * Ensures a documentation file exists at the given path\n * Creates parent directories if needed\n */\nfunction ensureDocFile(docPath: string, content: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const dir = path.dirname(docPath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n // Only write if file doesn't exist or is empty\n if (!fs.existsSync(docPath) || fs.readFileSync(docPath, 'utf-8').trim() === '') {\n fs.writeFileSync(docPath, content, 'utf-8');\n }\n\n return true;\n } catch (err: unknown) {\n //const error = toError(err);\n void err; // Silently fail - don't break linting if file creation fails\n return false;\n }\n}\n\n/**\n * Ensures the exception documentation markdown file exists\n * Only creates file once per lint run using module-level flag\n *\n * Reads from the template file packaged with @webpieces/webpieces-rules\n * and copies it to .webpieces/instruct-ai/ for AI agents to read.\n */\nfunction ensureExceptionDoc(context: Rule.RuleContext): void {\n if (exceptionDocCreated) return;\n\n const workspaceRoot = getWorkspaceRoot(context);\n const docPath = path.join(workspaceRoot, '.webpieces', 'instruct-ai', 'webpieces.exceptions.md');\n\n // Read from the template file packaged with the npm module\n // Path: from eslint-plugin/rules/ -> ../../templates/\n const templatePath = path.join(__dirname, '..', '..', 'templates', 'webpieces.exceptions.md');\n\n let content: string;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n content = fs.readFileSync(templatePath, 'utf-8');\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n // Fallback message if template not found (shouldn't happen in published package)\n content = `# Exception Documentation Not Found\\n\\nTemplate file not found at: ${templatePath}\\n\\nPlease ensure @webpieces/webpieces-rules is properly installed.`;\n }\n\n if (ensureDocFile(docPath, content)) {\n exceptionDocCreated = true;\n }\n}\n\n// Module-level flag to prevent redundant markdown file creation\nlet exceptionDocCreated = false;\n\n// NOTE: Documentation content moved to templates/webpieces.exceptions.md\n// The ensureExceptionDoc function reads from that file at runtime.\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Discourage try-catch blocks outside test files - use global error handlers',\n category: 'Best Practices',\n recommended: true,\n url: 'https://github.com/deanhiller/webpieces-ts/blob/main/CLAUDE.md#exception-handling-philosophy',\n },\n messages: {\n noUnmanagedExceptions:\n 'AI Agent: READ .webpieces/instruct-ai/webpieces.exceptions.md for context. Try-catch blocks are discouraged - use global error handlers instead. Only allowed in test files or with eslint-disable comment.',\n },\n fixable: undefined,\n schema: [],\n },\n\n create(context: Rule.RuleContext): Rule.RuleListener {\n return {\n // webpieces-disable no-any-unknown -- ESLint visitor callback parameter type\n TryStatement(node: unknown): void {\n // Skip try..finally blocks (no catch handler, no exception handling)\n // webpieces-disable no-any-unknown -- ESTree AST node type assertion\n if (!(node as TryStatementNode).handler) {\n return;\n }\n\n // Auto-allow in test files\n const filename = context.filename || context.getFilename();\n if (isTestFile(filename)) {\n return;\n }\n\n // Has catch block outside test file - report violation\n ensureExceptionDoc(context);\n context.report({\n node: node as Rule.Node,\n messageId: 'noUnmanagedExceptions',\n });\n },\n };\n },\n};\n\nexport = rule;\n\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: require-typed-template
|
|
3
|
+
*
|
|
4
|
+
* Enforces that every <ng-template> with let- variables also has [templateClassType]
|
|
5
|
+
* to preserve type safety via TypedTemplateOutletDirective.
|
|
6
|
+
*
|
|
7
|
+
* Works with @angular-eslint/template-parser AST where:
|
|
8
|
+
* - ng-template variables (let-xxx) appear in node.variables[]
|
|
9
|
+
* - bound inputs ([templateClassType]) appear in node.inputs[]
|
|
10
|
+
* - static attributes appear in node.attributes[]
|
|
11
|
+
*
|
|
12
|
+
* NOTE: This rule only works when files are parsed with @angular-eslint/template-parser.
|
|
13
|
+
* It is intended for Angular HTML template files (**.html).
|
|
14
|
+
*/
|
|
15
|
+
import type { Rule } from 'eslint';
|
|
16
|
+
declare const rule: Rule.RuleModule;
|
|
17
|
+
export = rule;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* ESLint rule: require-typed-template
|
|
3
4
|
*
|
|
@@ -12,31 +13,14 @@
|
|
|
12
13
|
* NOTE: This rule only works when files are parsed with @angular-eslint/template-parser.
|
|
13
14
|
* It is intended for Angular HTML template files (**.html).
|
|
14
15
|
*/
|
|
15
|
-
|
|
16
|
-
import type { Rule } from 'eslint';
|
|
17
|
-
|
|
18
|
-
// webpieces-disable no-any-unknown -- Angular template AST node interfaces
|
|
19
|
-
// These interfaces represent the Template node shape from @angular-eslint/template-parser.
|
|
20
|
-
// We define them inline since the parser is not a dependency of this plugin.
|
|
21
|
-
interface AngularTemplateNode {
|
|
22
|
-
tagName?: string;
|
|
23
|
-
variables?: Array<{ name: string }>;
|
|
24
|
-
inputs?: Array<{ name: string }>;
|
|
25
|
-
attributes?: Array<{ name: string }>;
|
|
26
|
-
// webpieces-disable no-any-unknown -- ESTree AST index signature
|
|
27
|
-
[key: string]: any;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const rule: Rule.RuleModule = {
|
|
16
|
+
const rule = {
|
|
31
17
|
meta: {
|
|
32
18
|
type: 'problem',
|
|
33
19
|
docs: {
|
|
34
|
-
description:
|
|
35
|
-
'Require [templateClassType] on ng-template elements that use let- variables',
|
|
20
|
+
description: 'Require [templateClassType] on ng-template elements that use let- variables',
|
|
36
21
|
},
|
|
37
22
|
messages: {
|
|
38
|
-
missingTypedTemplate:
|
|
39
|
-
'ng-template with let- variables must include ' +
|
|
23
|
+
missingTypedTemplate: 'ng-template with let- variables must include ' +
|
|
40
24
|
'[templateClassType]="YourDtoClass" to preserve type safety. ' +
|
|
41
25
|
'Fix: (1) Add [templateClassType]="YourDtoClass" to this ng-template, ' +
|
|
42
26
|
'(2) Add TypedTemplateOutletDirective to component imports array, ' +
|
|
@@ -45,30 +29,26 @@ const rule: Rule.RuleModule = {
|
|
|
45
29
|
},
|
|
46
30
|
schema: [],
|
|
47
31
|
},
|
|
48
|
-
create(context
|
|
32
|
+
create(context) {
|
|
49
33
|
return {
|
|
50
|
-
Template(node
|
|
34
|
+
Template(node) {
|
|
51
35
|
// Only match explicit <ng-template>, not desugared structural directives
|
|
52
36
|
// (*ngFor, *ngIf, etc.) which also produce Template AST nodes
|
|
53
37
|
if (node.tagName !== 'ng-template') {
|
|
54
38
|
return;
|
|
55
39
|
}
|
|
56
|
-
|
|
57
40
|
const hasLetVariables = node.variables && node.variables.length > 0;
|
|
58
41
|
if (!hasLetVariables) {
|
|
59
42
|
return;
|
|
60
43
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
(node.inputs &&
|
|
64
|
-
node.inputs.some((input) => input.name === 'templateClassType')) ||
|
|
44
|
+
const hasTemplateClassType = (node.inputs &&
|
|
45
|
+
node.inputs.some((input) => input.name === 'templateClassType')) ||
|
|
65
46
|
(node.attributes &&
|
|
66
47
|
node.attributes.some((attr) => attr.name === 'templateClassType'));
|
|
67
|
-
|
|
68
48
|
if (!hasTemplateClassType) {
|
|
69
49
|
context.report({
|
|
70
50
|
// webpieces-disable no-any-unknown -- ESTree AST cast for ESLint report
|
|
71
|
-
node: node
|
|
51
|
+
node: node,
|
|
72
52
|
messageId: 'missingTypedTemplate',
|
|
73
53
|
});
|
|
74
54
|
}
|
|
@@ -76,5 +56,5 @@ const rule: Rule.RuleModule = {
|
|
|
76
56
|
};
|
|
77
57
|
},
|
|
78
58
|
};
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
module.exports = rule;
|
|
60
|
+
//# sourceMappingURL=require-typed-template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-typed-template.js","sourceRoot":"","sources":["../../../../../../packages/tooling/eslint-rules/src/rules/require-typed-template.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;AAgBH,MAAM,IAAI,GAAoB;IAC1B,IAAI,EAAE;QACF,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACF,WAAW,EACP,6EAA6E;SACpF;QACD,QAAQ,EAAE;YACN,oBAAoB,EAChB,+CAA+C;gBAC/C,8DAA8D;gBAC9D,uEAAuE;gBACvE,mEAAmE;gBACnE,4EAA4E;gBAC5E,8CAA8C;SACrD;QACD,MAAM,EAAE,EAAE;KACb;IACD,MAAM,CAAC,OAAyB;QAC5B,OAAO;YACH,QAAQ,CAAC,IAAyB;gBAC9B,yEAAyE;gBACzE,8DAA8D;gBAC9D,IAAI,IAAI,CAAC,OAAO,KAAK,aAAa,EAAE,CAAC;oBACjC,OAAO;gBACX,CAAC;gBAED,MAAM,eAAe,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;gBACpE,IAAI,CAAC,eAAe,EAAE,CAAC;oBACnB,OAAO;gBACX,CAAC;gBAED,MAAM,oBAAoB,GACtB,CAAC,IAAI,CAAC,MAAM;oBACR,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,mBAAmB,CAAC,CAAC;oBACpE,CAAC,IAAI,CAAC,UAAU;wBACZ,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,mBAAmB,CAAC,CAAC,CAAC;gBAE3E,IAAI,CAAC,oBAAoB,EAAE,CAAC;oBACxB,OAAO,CAAC,MAAM,CAAC;wBACX,wEAAwE;wBACxE,IAAI,EAAE,IAA4B;wBAClC,SAAS,EAAE,sBAAsB;qBACpC,CAAC,CAAC;gBACP,CAAC;YACL,CAAC;SACJ,CAAC;IACN,CAAC;CACJ,CAAC;AAEF,iBAAS,IAAI,CAAC","sourcesContent":["/**\n * ESLint rule: require-typed-template\n *\n * Enforces that every <ng-template> with let- variables also has [templateClassType]\n * to preserve type safety via TypedTemplateOutletDirective.\n *\n * Works with @angular-eslint/template-parser AST where:\n * - ng-template variables (let-xxx) appear in node.variables[]\n * - bound inputs ([templateClassType]) appear in node.inputs[]\n * - static attributes appear in node.attributes[]\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 tagName?: string;\n variables?: Array<{ name: string }>;\n inputs?: Array<{ name: string }>;\n attributes?: Array<{ name: string }>;\n // webpieces-disable no-any-unknown -- ESTree AST index signature\n [key: string]: any;\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Require [templateClassType] on ng-template elements that use let- variables',\n },\n messages: {\n missingTypedTemplate:\n 'ng-template with let- variables must include ' +\n '[templateClassType]=\"YourDtoClass\" to preserve type safety. ' +\n 'Fix: (1) Add [templateClassType]=\"YourDtoClass\" to this ng-template, ' +\n '(2) Add TypedTemplateOutletDirective to component imports array, ' +\n '(3) Expose the DTO class: protected readonly YourDtoClass = YourDtoClass. ' +\n 'See @fuse/directives/typed-template-outlet/.',\n },\n schema: [],\n },\n create(context: Rule.RuleContext): Rule.RuleListener {\n return {\n Template(node: AngularTemplateNode): void {\n // Only match explicit <ng-template>, not desugared structural directives\n // (*ngFor, *ngIf, etc.) which also produce Template AST nodes\n if (node.tagName !== 'ng-template') {\n return;\n }\n\n const hasLetVariables = node.variables && node.variables.length > 0;\n if (!hasLetVariables) {\n return;\n }\n\n const hasTemplateClassType =\n (node.inputs &&\n node.inputs.some((input) => input.name === 'templateClassType')) ||\n (node.attributes &&\n node.attributes.some((attr) => attr.name === 'templateClassType'));\n\n if (!hasTemplateClassType) {\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: 'missingTypedTemplate',\n });\n }\n },\n };\n },\n};\n\nexport = rule;\n"]}
|
package/src/toError.d.ts
ADDED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toError = toError;
|
|
1
4
|
/**
|
|
2
5
|
* Lightweight duplicate of @webpieces/core-util toError for use in eslint-plugin.
|
|
3
6
|
* eslint-plugin is Level 0 and cannot depend on core-util (also Level 0).
|
|
4
7
|
*/
|
|
5
8
|
// webpieces-disable no-any-unknown -- toError intentionally accepts unknown to safely convert any thrown value to Error
|
|
6
|
-
|
|
9
|
+
function toError(err) {
|
|
7
10
|
if (err instanceof Error) {
|
|
8
11
|
return err;
|
|
9
12
|
}
|
|
10
|
-
|
|
11
13
|
if (err && typeof err === 'object') {
|
|
12
14
|
if ('message' in err) {
|
|
13
15
|
const error = new Error(String(err.message));
|
|
14
|
-
|
|
15
16
|
if ('stack' in err && typeof err.stack === 'string') {
|
|
16
17
|
error.stack = err.stack;
|
|
17
18
|
}
|
|
@@ -20,17 +21,17 @@ export function toError(err: unknown): Error {
|
|
|
20
21
|
}
|
|
21
22
|
return error;
|
|
22
23
|
}
|
|
23
|
-
|
|
24
24
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- must handle circular references without recursion
|
|
25
25
|
try {
|
|
26
26
|
return new Error(`Non-Error object thrown: ${JSON.stringify(err)}`);
|
|
27
|
-
}
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
28
29
|
//const error = toError(err);
|
|
29
30
|
void err;
|
|
30
31
|
return new Error('Non-Error object thrown (unable to stringify)');
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
|
-
|
|
34
34
|
const message = err == null ? 'Null or undefined thrown' : String(err);
|
|
35
35
|
return new Error(message);
|
|
36
36
|
}
|
|
37
|
+
//# sourceMappingURL=toError.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toError.js","sourceRoot":"","sources":["../../../../../packages/tooling/eslint-rules/src/toError.ts"],"names":[],"mappings":";;AAKA,0BA8BC;AAnCD;;;GAGG;AACH,wHAAwH;AACxH,SAAgB,OAAO,CAAC,GAAY;IAChC,IAAI,GAAG,YAAY,KAAK,EAAE,CAAC;QACvB,OAAO,GAAG,CAAC;IACf,CAAC;IAED,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACjC,IAAI,SAAS,IAAI,GAAG,EAAE,CAAC;YACnB,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;YAE7C,IAAI,OAAO,IAAI,GAAG,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;gBAClD,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC;YAC5B,CAAC;YACD,IAAI,MAAM,IAAI,GAAG,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAChD,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;YAC1B,CAAC;YACD,OAAO,KAAK,CAAC;QACjB,CAAC;QAED,mHAAmH;QACnH,IAAI,CAAC;YACD,OAAO,IAAI,KAAK,CAAC,4BAA4B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACxE,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,6BAA6B;YAC7B,KAAK,GAAG,CAAC;YACT,OAAO,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;QACtE,CAAC;IACL,CAAC;IAED,MAAM,OAAO,GAAG,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACvE,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;AAC9B,CAAC","sourcesContent":["/**\n * Lightweight duplicate of @webpieces/core-util toError for use in eslint-plugin.\n * eslint-plugin is Level 0 and cannot depend on core-util (also Level 0).\n */\n// webpieces-disable no-any-unknown -- toError intentionally accepts unknown to safely convert any thrown value to Error\nexport function toError(err: unknown): Error {\n if (err instanceof Error) {\n return err;\n }\n\n if (err && typeof err === 'object') {\n if ('message' in err) {\n const error = new Error(String(err.message));\n\n if ('stack' in err && typeof err.stack === 'string') {\n error.stack = err.stack;\n }\n if ('name' in err && typeof err.name === 'string') {\n error.name = err.name;\n }\n return error;\n }\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- must handle circular references without recursion\n try {\n return new Error(`Non-Error object thrown: ${JSON.stringify(err)}`);\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n return new Error('Non-Error object thrown (unable to stringify)');\n }\n }\n\n const message = err == null ? 'Null or undefined thrown' : String(err);\n return new Error(message);\n}\n"]}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
# AI Agent Instructions: File Too Long
|
|
2
|
-
|
|
3
|
-
**READ THIS FILE to fix files that are too long**
|
|
4
|
-
|
|
5
|
-
## Core Principle
|
|
6
|
-
Files should contain a SINGLE COHESIVE UNIT.
|
|
7
|
-
- One class per file (Java convention)
|
|
8
|
-
- If class is too large, extract child responsibilities
|
|
9
|
-
- Use dependency injection to compose functionality
|
|
10
|
-
|
|
11
|
-
## Command: Reduce File Size
|
|
12
|
-
|
|
13
|
-
### Step 1: Check for Multiple Classes
|
|
14
|
-
If the file contains multiple classes, **SEPARATE each class into its own file**.
|
|
15
|
-
|
|
16
|
-
```typescript
|
|
17
|
-
// BAD: UserController.ts (multiple classes)
|
|
18
|
-
export class UserController { /* ... */ }
|
|
19
|
-
export class UserValidator { /* ... */ }
|
|
20
|
-
export class UserNotifier { /* ... */ }
|
|
21
|
-
|
|
22
|
-
// GOOD: Three separate files
|
|
23
|
-
// UserController.ts
|
|
24
|
-
export class UserController { /* ... */ }
|
|
25
|
-
|
|
26
|
-
// UserValidator.ts
|
|
27
|
-
export class UserValidator { /* ... */ }
|
|
28
|
-
|
|
29
|
-
// UserNotifier.ts
|
|
30
|
-
export class UserNotifier { /* ... */ }
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
### Step 2: Extract Child Responsibilities (if single class is too large)
|
|
34
|
-
|
|
35
|
-
#### Pattern: Create New Service Class with Dependency Injection
|
|
36
|
-
|
|
37
|
-
```typescript
|
|
38
|
-
// BAD: UserController.ts (800 lines, single class)
|
|
39
|
-
@provideSingleton()
|
|
40
|
-
@Controller()
|
|
41
|
-
export class UserController {
|
|
42
|
-
// 200 lines: CRUD operations
|
|
43
|
-
// 300 lines: validation logic
|
|
44
|
-
// 200 lines: notification logic
|
|
45
|
-
// 100 lines: analytics logic
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// GOOD: Extract validation service
|
|
49
|
-
// 1. Create UserValidationService.ts
|
|
50
|
-
@provideSingleton()
|
|
51
|
-
export class UserValidationService {
|
|
52
|
-
validateUserData(data: UserData): ValidationResult {
|
|
53
|
-
// 300 lines of validation logic moved here
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
validateEmail(email: string): boolean { /* ... */ }
|
|
57
|
-
validatePassword(password: string): boolean { /* ... */ }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// 2. Inject into UserController.ts
|
|
61
|
-
@provideSingleton()
|
|
62
|
-
@Controller()
|
|
63
|
-
export class UserController {
|
|
64
|
-
constructor(
|
|
65
|
-
@inject(TYPES.UserValidationService)
|
|
66
|
-
private validator: UserValidationService
|
|
67
|
-
) {}
|
|
68
|
-
|
|
69
|
-
async createUser(data: UserData): Promise<User> {
|
|
70
|
-
const validation = this.validator.validateUserData(data);
|
|
71
|
-
if (!validation.isValid) {
|
|
72
|
-
throw new ValidationError(validation.errors);
|
|
73
|
-
}
|
|
74
|
-
// ... rest of logic
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## AI Agent Action Steps
|
|
80
|
-
|
|
81
|
-
1. **ANALYZE** the file structure:
|
|
82
|
-
- Count classes (if >1, separate immediately)
|
|
83
|
-
- Identify logical responsibilities within single class
|
|
84
|
-
|
|
85
|
-
2. **IDENTIFY** "child code" to extract:
|
|
86
|
-
- Validation logic -> ValidationService
|
|
87
|
-
- Notification logic -> NotificationService
|
|
88
|
-
- Data transformation -> TransformerService
|
|
89
|
-
- External API calls -> ApiService
|
|
90
|
-
- Business rules -> RulesEngine
|
|
91
|
-
|
|
92
|
-
3. **CREATE** new service file(s):
|
|
93
|
-
- Start with temporary name: `XXXX.ts` or `ChildService.ts`
|
|
94
|
-
- Add `@provideSingleton()` decorator
|
|
95
|
-
- Move child methods to new class
|
|
96
|
-
|
|
97
|
-
4. **UPDATE** dependency injection:
|
|
98
|
-
- Add to `TYPES` constants (if using symbol-based DI)
|
|
99
|
-
- Inject new service into original class constructor
|
|
100
|
-
- Replace direct method calls with `this.serviceName.method()`
|
|
101
|
-
|
|
102
|
-
5. **RENAME** extracted file:
|
|
103
|
-
- Read the extracted code to understand its purpose
|
|
104
|
-
- Rename `XXXX.ts` to logical name (e.g., `UserValidationService.ts`)
|
|
105
|
-
|
|
106
|
-
6. **VERIFY** file sizes:
|
|
107
|
-
- Original file should now be <700 lines
|
|
108
|
-
- Each extracted file should be <700 lines
|
|
109
|
-
- If still too large, extract more services
|
|
110
|
-
|
|
111
|
-
## Examples of Child Responsibilities to Extract
|
|
112
|
-
|
|
113
|
-
| If File Contains | Extract To | Pattern |
|
|
114
|
-
|-----------------|------------|---------|
|
|
115
|
-
| Validation logic (200+ lines) | `XValidator.ts` or `XValidationService.ts` | Singleton service |
|
|
116
|
-
| Notification logic (150+ lines) | `XNotifier.ts` or `XNotificationService.ts` | Singleton service |
|
|
117
|
-
| Data transformation (200+ lines) | `XTransformer.ts` | Singleton service |
|
|
118
|
-
| External API calls (200+ lines) | `XApiClient.ts` | Singleton service |
|
|
119
|
-
| Complex business rules (300+ lines) | `XRulesEngine.ts` | Singleton service |
|
|
120
|
-
| Database queries (200+ lines) | `XRepository.ts` | Singleton service |
|
|
121
|
-
|
|
122
|
-
## WebPieces Dependency Injection Pattern
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
// 1. Define service with @provideSingleton
|
|
126
|
-
import { provideSingleton } from '@webpieces/http-routing';
|
|
127
|
-
|
|
128
|
-
@provideSingleton()
|
|
129
|
-
export class MyService {
|
|
130
|
-
doSomething(): void { /* ... */ }
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 2. Inject into consumer
|
|
134
|
-
import { inject } from 'inversify';
|
|
135
|
-
import { TYPES } from './types';
|
|
136
|
-
|
|
137
|
-
@provideSingleton()
|
|
138
|
-
@Controller()
|
|
139
|
-
export class MyController {
|
|
140
|
-
constructor(
|
|
141
|
-
@inject(TYPES.MyService) private service: MyService
|
|
142
|
-
) {}
|
|
143
|
-
}
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
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.
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# AI Agent Instructions: Method Too Long
|
|
2
|
-
|
|
3
|
-
**READ THIS FILE to fix methods that are too long**
|
|
4
|
-
|
|
5
|
-
## Core Principle
|
|
6
|
-
Every method should read like a TABLE OF CONTENTS of a book.
|
|
7
|
-
- Each method call is a "chapter"
|
|
8
|
-
- When you dive into a method, you find another table of contents
|
|
9
|
-
- Keeping methods under 70 lines is achievable with proper extraction
|
|
10
|
-
|
|
11
|
-
## Command: Extract Code into Named Methods
|
|
12
|
-
|
|
13
|
-
### Pattern 1: Extract Loop Bodies
|
|
14
|
-
```typescript
|
|
15
|
-
// BAD: 50 lines embedded in loop
|
|
16
|
-
for (const order of orders) {
|
|
17
|
-
// 20 lines of validation logic
|
|
18
|
-
// 15 lines of processing logic
|
|
19
|
-
// 10 lines of notification logic
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// GOOD: Extracted to named methods
|
|
23
|
-
for (const order of orders) {
|
|
24
|
-
validateOrder(order);
|
|
25
|
-
processOrderItems(order);
|
|
26
|
-
sendNotifications(order);
|
|
27
|
-
}
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
### Pattern 2: Try-Catch Wrapper for Exception Handling
|
|
31
|
-
```typescript
|
|
32
|
-
// GOOD: Separates success path from error handling
|
|
33
|
-
async function handleRequest(req: Request): Promise<Response> {
|
|
34
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
35
|
-
try {
|
|
36
|
-
return await executeRequest(req);
|
|
37
|
-
} catch (err: unknown) {
|
|
38
|
-
const error = toError(err);
|
|
39
|
-
return createErrorResponse(error);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### Pattern 3: Sequential Method Calls (Table of Contents)
|
|
45
|
-
```typescript
|
|
46
|
-
// GOOD: Self-documenting steps
|
|
47
|
-
function processOrder(order: Order): void {
|
|
48
|
-
validateOrderData(order);
|
|
49
|
-
calculateTotals(order);
|
|
50
|
-
applyDiscounts(order);
|
|
51
|
-
processPayment(order);
|
|
52
|
-
updateInventory(order);
|
|
53
|
-
sendConfirmation(order);
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### Pattern 4: Separate Data Object Creation
|
|
58
|
-
```typescript
|
|
59
|
-
// BAD: 15 lines of inline object creation
|
|
60
|
-
doSomething({ field1: ..., field2: ..., field3: ..., /* 15 more fields */ });
|
|
61
|
-
|
|
62
|
-
// GOOD: Extract to factory method
|
|
63
|
-
const request = createRequestObject(data);
|
|
64
|
-
doSomething(request);
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### Pattern 5: Extract Inline Logic to Named Functions
|
|
68
|
-
```typescript
|
|
69
|
-
// BAD: Complex inline logic
|
|
70
|
-
if (user.role === 'admin' && user.permissions.includes('write') && !user.suspended) {
|
|
71
|
-
// 30 lines of admin logic
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// GOOD: Extract to named methods
|
|
75
|
-
if (isAdminWithWriteAccess(user)) {
|
|
76
|
-
performAdminOperation(user);
|
|
77
|
-
}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## AI Agent Action Steps
|
|
81
|
-
|
|
82
|
-
1. **IDENTIFY** the long method in the error message
|
|
83
|
-
2. **READ** the method to understand its logical sections
|
|
84
|
-
3. **EXTRACT** logical units into separate methods with descriptive names
|
|
85
|
-
4. **REPLACE** inline code with method calls
|
|
86
|
-
5. **VERIFY** each extracted method is <70 lines
|
|
87
|
-
6. **TEST** that functionality remains unchanged
|
|
88
|
-
|
|
89
|
-
## Examples of "Logical Units" to Extract
|
|
90
|
-
- Validation logic -> `validateX()`
|
|
91
|
-
- Data transformation -> `transformXToY()`
|
|
92
|
-
- API calls -> `fetchXFromApi()`
|
|
93
|
-
- Object creation -> `createX()`
|
|
94
|
-
- Loop bodies -> `processItem()`
|
|
95
|
-
- Error handling -> `handleXError()`
|
|
96
|
-
|
|
97
|
-
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.
|