@webpieces/eslint-rules 0.0.1
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/.webpieces/instruct-ai/webpieces.exceptions.md +5 -0
- package/.webpieces/instruct-ai/webpieces.filesize.md +146 -0
- package/.webpieces/instruct-ai/webpieces.methods.md +97 -0
- package/LICENSE +373 -0
- package/README.md +14 -0
- package/jest.config.ts +16 -0
- package/package.json +20 -0
- package/project.json +22 -0
- package/src/__tests__/catch-error-pattern.test.ts +374 -0
- package/src/__tests__/max-file-lines.test.ts +207 -0
- package/src/__tests__/max-method-lines.test.ts +258 -0
- package/src/__tests__/no-unmanaged-exceptions.test.ts +359 -0
- package/src/index.ts +38 -0
- package/src/rules/catch-error-pattern.ts +256 -0
- package/src/rules/enforce-architecture.ts +550 -0
- package/src/rules/max-file-lines.ts +275 -0
- package/src/rules/max-method-lines.ts +296 -0
- package/src/rules/no-json-property-primitive-type.ts +85 -0
- package/src/rules/no-mat-cell-def.ts +62 -0
- package/src/rules/no-unmanaged-exceptions.ts +194 -0
- package/src/rules/require-typed-template.ts +80 -0
- package/src/toError.ts +36 -0
- package/tmp/webpieces/webpieces.exceptions.md +5 -0
- package/tsconfig.json +22 -0
- package/tsconfig.lib.json +10 -0
- package/tsconfig.spec.json +14 -0
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
|
|
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
|
+
const BANNED_DIRECTIVES = ['matCellDef', 'matHeaderCellDef'];
|
|
26
|
+
|
|
27
|
+
const rule: Rule.RuleModule = {
|
|
28
|
+
meta: {
|
|
29
|
+
type: 'problem',
|
|
30
|
+
docs: {
|
|
31
|
+
description: 'Ban *matCellDef and *matHeaderCellDef — use div-grid tables instead',
|
|
32
|
+
},
|
|
33
|
+
messages: {
|
|
34
|
+
noMatCellDef:
|
|
35
|
+
'*{{ directive }} is banned in new files. Use the div-grid table pattern instead. ' +
|
|
36
|
+
'Div-grid tables are inherently type-safe with @for loops + strictTemplates.',
|
|
37
|
+
},
|
|
38
|
+
schema: [],
|
|
39
|
+
},
|
|
40
|
+
create(context: Rule.RuleContext): Rule.RuleListener {
|
|
41
|
+
return {
|
|
42
|
+
Template(node: AngularTemplateNode): void {
|
|
43
|
+
// Structural directives (*matCellDef) are desugared into Template nodes.
|
|
44
|
+
// The directive name appears in node.templateAttrs as either a
|
|
45
|
+
// BoundAttribute or TextAttribute.
|
|
46
|
+
const attrs = node.templateAttrs || [];
|
|
47
|
+
for (const attr of attrs) {
|
|
48
|
+
if (BANNED_DIRECTIVES.includes(attr.name)) {
|
|
49
|
+
context.report({
|
|
50
|
+
// webpieces-disable no-any-unknown -- ESTree AST cast for ESLint report
|
|
51
|
+
node: node as unknown as Rule.Node,
|
|
52
|
+
messageId: 'noMatCellDef',
|
|
53
|
+
data: { directive: attr.name },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export = rule;
|
|
@@ -0,0 +1,194 @@
|
|
|
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
|
+
|
|
21
|
+
import type { Rule } from 'eslint';
|
|
22
|
+
import * as fs from 'fs';
|
|
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
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Determines if a file is a test file based on naming conventions
|
|
33
|
+
* Test files are auto-allowed to use try-catch blocks
|
|
34
|
+
*/
|
|
35
|
+
function isTestFile(filename: string): boolean {
|
|
36
|
+
const normalizedPath = filename.toLowerCase();
|
|
37
|
+
|
|
38
|
+
// Check file extensions
|
|
39
|
+
if (normalizedPath.endsWith('.test.ts') || normalizedPath.endsWith('.spec.ts')) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check directory names (cross-platform)
|
|
44
|
+
if (normalizedPath.includes('/__tests__/') || normalizedPath.includes('\\__tests__\\')) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Finds the workspace root by walking up the directory tree
|
|
53
|
+
* Looks for package.json with workspaces or name === 'webpieces-ts'
|
|
54
|
+
*/
|
|
55
|
+
function getWorkspaceRoot(context: Rule.RuleContext): string {
|
|
56
|
+
const filename = context.filename || context.getFilename();
|
|
57
|
+
let dir = path.dirname(filename);
|
|
58
|
+
|
|
59
|
+
// Walk up directory tree
|
|
60
|
+
for (let i = 0; i < 10; i++) {
|
|
61
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
62
|
+
if (fs.existsSync(pkgPath)) {
|
|
63
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
64
|
+
try {
|
|
65
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
66
|
+
// Check if this is the root workspace
|
|
67
|
+
if (pkg.workspaces || pkg.name === 'webpieces-ts') {
|
|
68
|
+
return dir;
|
|
69
|
+
}
|
|
70
|
+
} catch (err: unknown) {
|
|
71
|
+
//const error = toError(err);
|
|
72
|
+
void err; // Invalid JSON, keep searching
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const parentDir = path.dirname(dir);
|
|
77
|
+
if (parentDir === dir) break; // Reached filesystem root
|
|
78
|
+
dir = parentDir;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Fallback: return current directory
|
|
82
|
+
return process.cwd();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Ensures a documentation file exists at the given path
|
|
87
|
+
* Creates parent directories if needed
|
|
88
|
+
*/
|
|
89
|
+
function ensureDocFile(docPath: string, content: string): boolean {
|
|
90
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
91
|
+
try {
|
|
92
|
+
const dir = path.dirname(docPath);
|
|
93
|
+
if (!fs.existsSync(dir)) {
|
|
94
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Only write if file doesn't exist or is empty
|
|
98
|
+
if (!fs.existsSync(docPath) || fs.readFileSync(docPath, 'utf-8').trim() === '') {
|
|
99
|
+
fs.writeFileSync(docPath, content, 'utf-8');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return true;
|
|
103
|
+
} catch (err: unknown) {
|
|
104
|
+
//const error = toError(err);
|
|
105
|
+
void err; // Silently fail - don't break linting if file creation fails
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Ensures the exception documentation markdown file exists
|
|
112
|
+
* Only creates file once per lint run using module-level flag
|
|
113
|
+
*
|
|
114
|
+
* Reads from the template file packaged with @webpieces/webpieces-rules
|
|
115
|
+
* and copies it to .webpieces/instruct-ai/ for AI agents to read.
|
|
116
|
+
*/
|
|
117
|
+
function ensureExceptionDoc(context: Rule.RuleContext): void {
|
|
118
|
+
if (exceptionDocCreated) return;
|
|
119
|
+
|
|
120
|
+
const workspaceRoot = getWorkspaceRoot(context);
|
|
121
|
+
const docPath = path.join(workspaceRoot, '.webpieces', 'instruct-ai', 'webpieces.exceptions.md');
|
|
122
|
+
|
|
123
|
+
// Read from the template file packaged with the npm module
|
|
124
|
+
// Path: from eslint-plugin/rules/ -> ../../templates/
|
|
125
|
+
const templatePath = path.join(__dirname, '..', '..', 'templates', 'webpieces.exceptions.md');
|
|
126
|
+
|
|
127
|
+
let content: string;
|
|
128
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
129
|
+
try {
|
|
130
|
+
content = fs.readFileSync(templatePath, 'utf-8');
|
|
131
|
+
} catch (err: unknown) {
|
|
132
|
+
//const error = toError(err);
|
|
133
|
+
void err;
|
|
134
|
+
// Fallback message if template not found (shouldn't happen in published package)
|
|
135
|
+
content = `# Exception Documentation Not Found\n\nTemplate file not found at: ${templatePath}\n\nPlease ensure @webpieces/webpieces-rules is properly installed.`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (ensureDocFile(docPath, content)) {
|
|
139
|
+
exceptionDocCreated = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Module-level flag to prevent redundant markdown file creation
|
|
144
|
+
let exceptionDocCreated = false;
|
|
145
|
+
|
|
146
|
+
// NOTE: Documentation content moved to templates/webpieces.exceptions.md
|
|
147
|
+
// The ensureExceptionDoc function reads from that file at runtime.
|
|
148
|
+
|
|
149
|
+
const rule: Rule.RuleModule = {
|
|
150
|
+
meta: {
|
|
151
|
+
type: 'problem',
|
|
152
|
+
docs: {
|
|
153
|
+
description: 'Discourage try-catch blocks outside test files - use global error handlers',
|
|
154
|
+
category: 'Best Practices',
|
|
155
|
+
recommended: true,
|
|
156
|
+
url: 'https://github.com/deanhiller/webpieces-ts/blob/main/CLAUDE.md#exception-handling-philosophy',
|
|
157
|
+
},
|
|
158
|
+
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.',
|
|
161
|
+
},
|
|
162
|
+
fixable: undefined,
|
|
163
|
+
schema: [],
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
create(context: Rule.RuleContext): Rule.RuleListener {
|
|
167
|
+
return {
|
|
168
|
+
// webpieces-disable no-any-unknown -- ESLint visitor callback parameter type
|
|
169
|
+
TryStatement(node: unknown): void {
|
|
170
|
+
// Skip try..finally blocks (no catch handler, no exception handling)
|
|
171
|
+
// webpieces-disable no-any-unknown -- ESTree AST node type assertion
|
|
172
|
+
if (!(node as TryStatementNode).handler) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Auto-allow in test files
|
|
177
|
+
const filename = context.filename || context.getFilename();
|
|
178
|
+
if (isTestFile(filename)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Has catch block outside test file - report violation
|
|
183
|
+
ensureExceptionDoc(context);
|
|
184
|
+
context.report({
|
|
185
|
+
node: node as Rule.Node,
|
|
186
|
+
messageId: 'noUnmanagedExceptions',
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export = rule;
|
|
194
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
|
|
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 = {
|
|
31
|
+
meta: {
|
|
32
|
+
type: 'problem',
|
|
33
|
+
docs: {
|
|
34
|
+
description:
|
|
35
|
+
'Require [templateClassType] on ng-template elements that use let- variables',
|
|
36
|
+
},
|
|
37
|
+
messages: {
|
|
38
|
+
missingTypedTemplate:
|
|
39
|
+
'ng-template with let- variables must include ' +
|
|
40
|
+
'[templateClassType]="YourDtoClass" to preserve type safety. ' +
|
|
41
|
+
'Fix: (1) Add [templateClassType]="YourDtoClass" to this ng-template, ' +
|
|
42
|
+
'(2) Add TypedTemplateOutletDirective to component imports array, ' +
|
|
43
|
+
'(3) Expose the DTO class: protected readonly YourDtoClass = YourDtoClass. ' +
|
|
44
|
+
'See @fuse/directives/typed-template-outlet/.',
|
|
45
|
+
},
|
|
46
|
+
schema: [],
|
|
47
|
+
},
|
|
48
|
+
create(context: Rule.RuleContext): Rule.RuleListener {
|
|
49
|
+
return {
|
|
50
|
+
Template(node: AngularTemplateNode): void {
|
|
51
|
+
// Only match explicit <ng-template>, not desugared structural directives
|
|
52
|
+
// (*ngFor, *ngIf, etc.) which also produce Template AST nodes
|
|
53
|
+
if (node.tagName !== 'ng-template') {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const hasLetVariables = node.variables && node.variables.length > 0;
|
|
58
|
+
if (!hasLetVariables) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const hasTemplateClassType =
|
|
63
|
+
(node.inputs &&
|
|
64
|
+
node.inputs.some((input) => input.name === 'templateClassType')) ||
|
|
65
|
+
(node.attributes &&
|
|
66
|
+
node.attributes.some((attr) => attr.name === 'templateClassType'));
|
|
67
|
+
|
|
68
|
+
if (!hasTemplateClassType) {
|
|
69
|
+
context.report({
|
|
70
|
+
// webpieces-disable no-any-unknown -- ESTree AST cast for ESLint report
|
|
71
|
+
node: node as unknown as Rule.Node,
|
|
72
|
+
messageId: 'missingTypedTemplate',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export = rule;
|
package/src/toError.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight duplicate of @webpieces/core-util toError for use in eslint-plugin.
|
|
3
|
+
* eslint-plugin is Level 0 and cannot depend on core-util (also Level 0).
|
|
4
|
+
*/
|
|
5
|
+
// webpieces-disable no-any-unknown -- toError intentionally accepts unknown to safely convert any thrown value to Error
|
|
6
|
+
export function toError(err: unknown): Error {
|
|
7
|
+
if (err instanceof Error) {
|
|
8
|
+
return err;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (err && typeof err === 'object') {
|
|
12
|
+
if ('message' in err) {
|
|
13
|
+
const error = new Error(String(err.message));
|
|
14
|
+
|
|
15
|
+
if ('stack' in err && typeof err.stack === 'string') {
|
|
16
|
+
error.stack = err.stack;
|
|
17
|
+
}
|
|
18
|
+
if ('name' in err && typeof err.name === 'string') {
|
|
19
|
+
error.name = err.name;
|
|
20
|
+
}
|
|
21
|
+
return error;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- must handle circular references without recursion
|
|
25
|
+
try {
|
|
26
|
+
return new Error(`Non-Error object thrown: ${JSON.stringify(err)}`);
|
|
27
|
+
} catch (err: unknown) {
|
|
28
|
+
//const error = toError(err);
|
|
29
|
+
void err;
|
|
30
|
+
return new Error('Non-Error object thrown (unable to stringify)');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const message = err == null ? 'Null or undefined thrown' : String(err);
|
|
35
|
+
return new Error(message);
|
|
36
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"forceConsistentCasingInFileNames": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noImplicitOverride": true,
|
|
8
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
9
|
+
"noImplicitReturns": true,
|
|
10
|
+
"noFallthroughCasesInSwitch": true
|
|
11
|
+
},
|
|
12
|
+
"files": [],
|
|
13
|
+
"include": [],
|
|
14
|
+
"references": [
|
|
15
|
+
{
|
|
16
|
+
"path": "./tsconfig.lib.json"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "./tsconfig.spec.json"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../../dist/out-tsc",
|
|
5
|
+
"module": "commonjs",
|
|
6
|
+
"types": ["jest", "node"]
|
|
7
|
+
},
|
|
8
|
+
"include": [
|
|
9
|
+
"jest.config.ts",
|
|
10
|
+
"src/**/*.test.ts",
|
|
11
|
+
"src/**/*.spec.ts",
|
|
12
|
+
"src/__tests__/**/*.ts"
|
|
13
|
+
]
|
|
14
|
+
}
|