@webpieces/eslint-plugin 0.0.0-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @webpieces/eslint-plugin
2
+
3
+ ESLint plugin for WebPieces code patterns and architecture enforcement.
4
+
5
+ ## Rules
6
+
7
+ - `catch-error-pattern` - Enforce toError() usage in catch blocks
8
+ - `no-unmanaged-exceptions` - Discourage try-catch outside tests
9
+ - `max-method-lines` - Enforce maximum method length
10
+ - `max-file-lines` - Enforce maximum file length
11
+ - `enforce-architecture` - Enforce architecture dependency boundaries
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@webpieces/eslint-plugin",
3
+ "version": "0.0.0-dev",
4
+ "description": "ESLint plugin for WebPieces code patterns and architecture enforcement",
5
+ "type": "commonjs",
6
+ "main": "./src/index.js",
7
+ "author": "Dean Hiller",
8
+ "license": "Apache-2.0",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/deanhiller/webpieces-ts.git",
12
+ "directory": "packages/tooling/eslint-plugin"
13
+ },
14
+ "peerDependencies": {
15
+ "eslint": ">=8.0.0"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "types": "./src/index.d.ts"
21
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * ESLint plugin for WebPieces
3
+ * Provides rules for enforcing WebPieces code patterns
4
+ *
5
+ * This plugin is automatically included in @webpieces/dev-config
6
+ *
7
+ * Available rules:
8
+ * - catch-error-pattern: Enforce toError() usage in catch blocks (HOW to handle)
9
+ * - no-unmanaged-exceptions: Discourage try-catch outside tests (WHERE to handle)
10
+ * - max-method-lines: Enforce maximum method length (default: 70 lines)
11
+ * - max-file-lines: Enforce maximum file length (default: 700 lines)
12
+ * - enforce-architecture: Enforce architecture dependency boundaries
13
+ */
14
+ declare const _default: {
15
+ rules: {
16
+ 'catch-error-pattern': import("eslint").Rule.RuleModule;
17
+ 'no-unmanaged-exceptions': import("eslint").Rule.RuleModule;
18
+ 'max-method-lines': import("eslint").Rule.RuleModule;
19
+ 'max-file-lines': import("eslint").Rule.RuleModule;
20
+ 'enforce-architecture': import("eslint").Rule.RuleModule;
21
+ };
22
+ };
23
+ export = _default;
package/src/index.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /**
3
+ * ESLint plugin for WebPieces
4
+ * Provides rules for enforcing WebPieces code patterns
5
+ *
6
+ * This plugin is automatically included in @webpieces/dev-config
7
+ *
8
+ * Available rules:
9
+ * - catch-error-pattern: Enforce toError() usage in catch blocks (HOW to handle)
10
+ * - no-unmanaged-exceptions: Discourage try-catch outside tests (WHERE to handle)
11
+ * - max-method-lines: Enforce maximum method length (default: 70 lines)
12
+ * - max-file-lines: Enforce maximum file length (default: 700 lines)
13
+ * - enforce-architecture: Enforce architecture dependency boundaries
14
+ */
15
+ const tslib_1 = require("tslib");
16
+ const catch_error_pattern_1 = tslib_1.__importDefault(require("./rules/catch-error-pattern"));
17
+ const no_unmanaged_exceptions_1 = tslib_1.__importDefault(require("./rules/no-unmanaged-exceptions"));
18
+ const max_method_lines_1 = tslib_1.__importDefault(require("./rules/max-method-lines"));
19
+ const max_file_lines_1 = tslib_1.__importDefault(require("./rules/max-file-lines"));
20
+ const enforce_architecture_1 = tslib_1.__importDefault(require("./rules/enforce-architecture"));
21
+ module.exports = {
22
+ rules: {
23
+ 'catch-error-pattern': catch_error_pattern_1.default,
24
+ 'no-unmanaged-exceptions': no_unmanaged_exceptions_1.default,
25
+ 'max-method-lines': max_method_lines_1.default,
26
+ 'max-file-lines': max_file_lines_1.default,
27
+ 'enforce-architecture': enforce_architecture_1.default,
28
+ },
29
+ };
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/tooling/eslint-plugin/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;AAEH,8FAA4D;AAC5D,sGAAoE;AACpE,wFAAsD;AACtD,oFAAkD;AAClD,gGAA+D;AAE/D,iBAAS;IACL,KAAK,EAAE;QACH,qBAAqB,EAAE,6BAAiB;QACxC,yBAAyB,EAAE,iCAAqB;QAChD,kBAAkB,EAAE,0BAAc;QAClC,gBAAgB,EAAE,wBAAY;QAC9B,sBAAsB,EAAE,8BAAmB;KAC9C;CACJ,CAAC","sourcesContent":["/**\n * ESLint plugin for WebPieces\n * Provides rules for enforcing WebPieces code patterns\n *\n * This plugin is automatically included in @webpieces/dev-config\n *\n * Available rules:\n * - catch-error-pattern: Enforce toError() usage in catch blocks (HOW to handle)\n * - no-unmanaged-exceptions: Discourage try-catch outside tests (WHERE to handle)\n * - max-method-lines: Enforce maximum method length (default: 70 lines)\n * - max-file-lines: Enforce maximum file length (default: 700 lines)\n * - enforce-architecture: Enforce architecture dependency boundaries\n */\n\nimport catchErrorPattern from './rules/catch-error-pattern';\nimport noUnmanagedExceptions from './rules/no-unmanaged-exceptions';\nimport maxMethodLines from './rules/max-method-lines';\nimport maxFileLines from './rules/max-file-lines';\nimport enforceArchitecture from './rules/enforce-architecture';\n\nexport = {\n rules: {\n 'catch-error-pattern': catchErrorPattern,\n 'no-unmanaged-exceptions': noUnmanagedExceptions,\n 'max-method-lines': maxMethodLines,\n 'max-file-lines': maxFileLines,\n 'enforce-architecture': enforceArchitecture,\n },\n};\n"]}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ESLint rule to enforce standardized catch block error handling patterns
3
+ *
4
+ * Enforces three approved patterns:
5
+ * 1. Standard: catch (err: unknown) { const error = toError(err); }
6
+ * 2. Ignored: catch (err: unknown) { //const error = toError(err); }
7
+ * 3. Nested: catch (err2: unknown) { const error2 = toError(err2); }
8
+ */
9
+ import type { Rule } from 'eslint';
10
+ declare const rule: Rule.RuleModule;
11
+ export = rule;
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ /**
3
+ * ESLint rule to enforce standardized catch block error handling patterns
4
+ *
5
+ * Enforces three approved patterns:
6
+ * 1. Standard: catch (err: unknown) { const error = toError(err); }
7
+ * 2. Ignored: catch (err: unknown) { //const error = toError(err); }
8
+ * 3. Nested: catch (err2: unknown) { const error2 = toError(err2); }
9
+ */
10
+ function validateParamName(context, param, expectedParamName) {
11
+ if (param.type === 'Identifier' && param.name !== expectedParamName) {
12
+ context.report({
13
+ node: param,
14
+ messageId: 'wrongParameterName',
15
+ data: { actual: param.name },
16
+ });
17
+ }
18
+ }
19
+ function validateTypeAnnotation(context, param, expectedParamName) {
20
+ if (!param.typeAnnotation ||
21
+ !param.typeAnnotation.typeAnnotation ||
22
+ param.typeAnnotation.typeAnnotation.type !== 'TSUnknownKeyword') {
23
+ context.report({
24
+ node: param,
25
+ messageId: 'missingTypeAnnotation',
26
+ data: { param: param.name || expectedParamName },
27
+ });
28
+ }
29
+ }
30
+ function hasIgnoreComment(catchNode, sourceCode, expectedVarName, actualParamName) {
31
+ const catchBlockStart = catchNode.body.range[0];
32
+ const catchBlockEnd = catchNode.body.range[1];
33
+ const catchBlockText = sourceCode.text.substring(catchBlockStart, catchBlockEnd);
34
+ const ignorePattern = new RegExp(`//\\s*const\\s+${expectedVarName}\\s*=\\s*toError\\(${actualParamName}\\)`);
35
+ return ignorePattern.test(catchBlockText);
36
+ }
37
+ function reportMissingToError(context,
38
+ // webpieces-disable no-any-unknown -- ESLint AST node param requires any type
39
+ node, paramName) {
40
+ context.report({
41
+ node,
42
+ messageId: 'missingToError',
43
+ data: { param: paramName },
44
+ });
45
+ }
46
+ function validateToErrorCall(context,
47
+ // webpieces-disable no-any-unknown -- ESLint AST node param requires any type
48
+ firstStatement, expectedParamName, expectedVarName, actualParamName) {
49
+ if (firstStatement.type !== 'VariableDeclaration') {
50
+ reportMissingToError(context, firstStatement, expectedParamName);
51
+ return;
52
+ }
53
+ const varDecl = firstStatement;
54
+ const declaration = varDecl.declarations[0];
55
+ if (!declaration) {
56
+ reportMissingToError(context, firstStatement, expectedParamName);
57
+ return;
58
+ }
59
+ if (declaration.id.type !== 'Identifier' || declaration.id.name !== expectedVarName) {
60
+ context.report({
61
+ node: declaration.id,
62
+ messageId: 'wrongVariableName',
63
+ data: { expected: expectedVarName, actual: declaration.id.name || 'unknown' },
64
+ });
65
+ return;
66
+ }
67
+ if (!declaration.init || declaration.init.type !== 'CallExpression') {
68
+ reportMissingToError(context, declaration.init || declaration, expectedParamName);
69
+ return;
70
+ }
71
+ const callExpr = declaration.init;
72
+ const callee = callExpr.callee;
73
+ if (callee.type !== 'Identifier' || callee.name !== 'toError') {
74
+ reportMissingToError(context, callee, expectedParamName);
75
+ return;
76
+ }
77
+ const args = callExpr.arguments;
78
+ if (args.length !== 1 ||
79
+ args[0].type !== 'Identifier' ||
80
+ args[0].name !== actualParamName) {
81
+ reportMissingToError(context, callExpr, actualParamName);
82
+ }
83
+ }
84
+ const rule = {
85
+ meta: {
86
+ type: 'problem',
87
+ docs: {
88
+ description: 'Enforce standardized catch block error handling patterns',
89
+ category: 'Best Practices',
90
+ recommended: true,
91
+ url: 'https://github.com/deanhiller/webpieces-ts/blob/main/claude.patterns.md#error-handling-pattern',
92
+ },
93
+ messages: {
94
+ missingToError: 'Catch block must call toError({{param}}) as first statement or comment it out to explicitly ignore errors',
95
+ wrongVariableName: 'Error variable must be named "{{expected}}", got "{{actual}}"',
96
+ missingTypeAnnotation: 'Catch parameter must be typed as "unknown": catch ({{param}}: unknown)',
97
+ wrongParameterName: 'Catch parameter must be named "err" (or "err2", "err3" for nested catches), got "{{actual}}"',
98
+ toErrorNotFirst: 'toError({{param}}) must be the first statement in the catch block',
99
+ },
100
+ fixable: undefined,
101
+ schema: [],
102
+ },
103
+ create(context) {
104
+ const catchStack = [];
105
+ return {
106
+ // webpieces-disable no-any-unknown -- ESLint visitor callback receives untyped AST node
107
+ CatchClause(node) {
108
+ const catchNode = node;
109
+ const depth = catchStack.length + 1;
110
+ catchStack.push(catchNode);
111
+ const suffix = depth === 1 ? '' : String(depth);
112
+ const expectedParamName = 'err' + suffix;
113
+ const expectedVarName = 'error' + suffix;
114
+ const param = catchNode.param;
115
+ if (!param) {
116
+ context.report({
117
+ node: catchNode,
118
+ messageId: 'missingTypeAnnotation',
119
+ data: { param: expectedParamName },
120
+ });
121
+ return;
122
+ }
123
+ const actualParamName = param.type === 'Identifier' ? param.name : expectedParamName;
124
+ validateParamName(context, param, expectedParamName);
125
+ validateTypeAnnotation(context, param, expectedParamName);
126
+ const sourceCode = context.sourceCode || context.getSourceCode();
127
+ if (hasIgnoreComment(catchNode, sourceCode, expectedVarName, actualParamName)) {
128
+ return;
129
+ }
130
+ const body = catchNode.body.body;
131
+ if (body.length === 0) {
132
+ reportMissingToError(context, catchNode.body, expectedParamName);
133
+ return;
134
+ }
135
+ validateToErrorCall(context, body[0], expectedParamName, expectedVarName, actualParamName);
136
+ },
137
+ 'CatchClause:exit'() {
138
+ catchStack.pop();
139
+ },
140
+ };
141
+ },
142
+ };
143
+ module.exports = rule;
144
+ //# sourceMappingURL=catch-error-pattern.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catch-error-pattern.js","sourceRoot":"","sources":["../../../../../../packages/tooling/eslint-plugin/src/rules/catch-error-pattern.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;AA8DH,SAAS,iBAAiB,CACtB,OAAyB,EACzB,KAAqB,EACrB,iBAAyB;IAEzB,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,IAAI,KAAK,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;QAClE,OAAO,CAAC,MAAM,CAAC;YACX,IAAI,EAAE,KAAK;YACX,SAAS,EAAE,oBAAoB;YAC/B,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,EAAE;SAC/B,CAAC,CAAC;IACP,CAAC;AACL,CAAC;AAED,SAAS,sBAAsB,CAC3B,OAAyB,EACzB,KAAqB,EACrB,iBAAyB;IAEzB,IACI,CAAC,KAAK,CAAC,cAAc;QACrB,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc;QACpC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,IAAI,KAAK,kBAAkB,EACjE,CAAC;QACC,OAAO,CAAC,MAAM,CAAC;YACX,IAAI,EAAE,KAAK;YACX,SAAS,EAAE,uBAAuB;YAClC,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,iBAAiB,EAAE;SACnD,CAAC,CAAC;IACP,CAAC;AACL,CAAC;AAED,SAAS,gBAAgB,CACrB,SAA0B,EAC1B,UAA0C,EAC1C,eAAuB,EACvB,eAAuB;IAEvB,MAAM,eAAe,GAAG,SAAS,CAAC,IAAI,CAAC,KAAM,CAAC,CAAC,CAAC,CAAC;IACjD,MAAM,aAAa,GAAG,SAAS,CAAC,IAAI,CAAC,KAAM,CAAC,CAAC,CAAC,CAAC;IAC/C,MAAM,cAAc,GAAG,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,eAAe,EAAE,aAAa,CAAC,CAAC;IAEjF,MAAM,aAAa,GAAG,IAAI,MAAM,CAC5B,kBAAkB,eAAe,sBAAsB,eAAe,KAAK,CAC9E,CAAC;IAEF,OAAO,aAAa,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;AAC9C,CAAC;AAED,SAAS,oBAAoB,CACzB,OAAyB;AACzB,8EAA8E;AAC9E,IAAS,EACT,SAAiB;IAEjB,OAAO,CAAC,MAAM,CAAC;QACX,IAAI;QACJ,SAAS,EAAE,gBAAgB;QAC3B,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE;KAC7B,CAAC,CAAC;AACP,CAAC;AAED,SAAS,mBAAmB,CACxB,OAAyB;AACzB,8EAA8E;AAC9E,cAAmB,EACnB,iBAAyB,EACzB,eAAuB,EACvB,eAAuB;IAEvB,IAAI,cAAc,CAAC,IAAI,KAAK,qBAAqB,EAAE,CAAC;QAChD,oBAAoB,CAAC,OAAO,EAAE,cAAc,EAAE,iBAAiB,CAAC,CAAC;QACjE,OAAO;IACX,CAAC;IAED,MAAM,OAAO,GAAG,cAAyC,CAAC;IAC1D,MAAM,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAC5C,IAAI,CAAC,WAAW,EAAE,CAAC;QACf,oBAAoB,CAAC,OAAO,EAAE,cAAc,EAAE,iBAAiB,CAAC,CAAC;QACjE,OAAO;IACX,CAAC;IAED,IAAI,WAAW,CAAC,EAAE,CAAC,IAAI,KAAK,YAAY,IAAI,WAAW,CAAC,EAAE,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;QAClF,OAAO,CAAC,MAAM,CAAC;YACX,IAAI,EAAE,WAAW,CAAC,EAAE;YACpB,SAAS,EAAE,mBAAmB;YAC9B,IAAI,EAAE,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,CAAC,IAAI,IAAI,SAAS,EAAE;SAChF,CAAC,CAAC;QACH,OAAO;IACX,CAAC;IAED,IAAI,CAAC,WAAW,CAAC,IAAI,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;QAClE,oBAAoB,CAAC,OAAO,EAAE,WAAW,CAAC,IAAI,IAAI,WAAW,EAAE,iBAAiB,CAAC,CAAC;QAClF,OAAO;IACX,CAAC;IAED,MAAM,QAAQ,GAAG,WAAW,CAAC,IAA0B,CAAC;IACxD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;IAC/B,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5D,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,iBAAiB,CAAC,CAAC;QACzD,OAAO;IACX,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,SAAS,CAAC;IAChC,IACI,IAAI,CAAC,MAAM,KAAK,CAAC;QACjB,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,YAAY;QAC5B,IAAI,CAAC,CAAC,CAAoB,CAAC,IAAI,KAAK,eAAe,EACtD,CAAC;QACC,oBAAoB,CAAC,OAAO,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC;IAC7D,CAAC;AACL,CAAC;AAED,MAAM,IAAI,GAAoB;IAC1B,IAAI,EAAE;QACF,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACF,WAAW,EAAE,0DAA0D;YACvE,QAAQ,EAAE,gBAAgB;YAC1B,WAAW,EAAE,IAAI;YACjB,GAAG,EAAE,gGAAgG;SACxG;QACD,QAAQ,EAAE;YACN,cAAc,EACV,2GAA2G;YAC/G,iBAAiB,EAAE,+DAA+D;YAClF,qBAAqB,EAAE,wEAAwE;YAC/F,kBAAkB,EACd,8FAA8F;YAClG,eAAe,EAAE,mEAAmE;SACvF;QACD,OAAO,EAAE,SAAS;QAClB,MAAM,EAAE,EAAE;KACb;IAED,MAAM,CAAC,OAAyB;QAC5B,MAAM,UAAU,GAAsB,EAAE,CAAC;QAEzC,OAAO;YACH,wFAAwF;YACxF,WAAW,CAAC,IAAS;gBACjB,MAAM,SAAS,GAAG,IAAuB,CAAC;gBAC1C,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC;gBACpC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBAE3B,MAAM,MAAM,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAChD,MAAM,iBAAiB,GAAG,KAAK,GAAG,MAAM,CAAC;gBACzC,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,CAAC;gBAEzC,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC;gBAC9B,IAAI,CAAC,KAAK,EAAE,CAAC;oBACT,OAAO,CAAC,MAAM,CAAC;wBACX,IAAI,EAAE,SAAS;wBACf,SAAS,EAAE,uBAAuB;wBAClC,IAAI,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE;qBACrC,CAAC,CAAC;oBACH,OAAO;gBACX,CAAC;gBAED,MAAM,eAAe,GACjB,KAAK,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,iBAAiB,CAAC;gBAEjE,iBAAiB,CAAC,OAAO,EAAE,KAAK,EAAE,iBAAiB,CAAC,CAAC;gBACrD,sBAAsB,CAAC,OAAO,EAAE,KAAK,EAAE,iBAAiB,CAAC,CAAC;gBAE1D,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;gBACjE,IAAI,gBAAgB,CAAC,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,CAAC,EAAE,CAAC;oBAC5E,OAAO;gBACX,CAAC;gBAED,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;gBACjC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBACpB,oBAAoB,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;oBACjE,OAAO;gBACX,CAAC;gBAED,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,eAAe,EAAE,eAAe,CAAC,CAAC;YAC/F,CAAC;YAED,kBAAkB;gBACd,UAAU,CAAC,GAAG,EAAE,CAAC;YACrB,CAAC;SACJ,CAAC;IACN,CAAC;CACJ,CAAC;AAEF,iBAAS,IAAI,CAAC","sourcesContent":["/**\n * ESLint rule to enforce standardized catch block error handling patterns\n *\n * Enforces three approved patterns:\n * 1. Standard: catch (err: unknown) { const error = toError(err); }\n * 2. Ignored: catch (err: unknown) { //const error = toError(err); }\n * 3. Nested: catch (err2: unknown) { const error2 = toError(err2); }\n */\n\nimport type { Rule } from 'eslint';\n\n// webpieces-disable no-any-unknown -- ESTree AST node interfaces require any for dynamic properties\n// Using any for ESTree nodes to avoid complex type gymnastics\n// ESLint rules work with dynamic AST nodes anyway\ninterface CatchClauseNode {\n type: 'CatchClause';\n param?: IdentifierNode | null;\n body: BlockStatementNode;\n // webpieces-disable no-any-unknown -- ESTree AST index signature\n [key: string]: any;\n}\n\ninterface IdentifierNode {\n type: 'Identifier';\n name: string;\n typeAnnotation?: TypeAnnotationNode;\n // webpieces-disable no-any-unknown -- ESTree AST index signature\n [key: string]: any;\n}\n\ninterface TypeAnnotationNode {\n typeAnnotation?: {\n type: string;\n };\n}\n\ninterface BlockStatementNode {\n type: 'BlockStatement';\n // webpieces-disable no-any-unknown -- ESTree AST dynamic body array\n body: any[];\n range: [number, number];\n // webpieces-disable no-any-unknown -- ESTree AST index signature\n [key: string]: any;\n}\n\ninterface VariableDeclarationNode {\n type: 'VariableDeclaration';\n declarations: VariableDeclaratorNode[];\n // webpieces-disable no-any-unknown -- ESTree AST index signature\n [key: string]: any;\n}\n\ninterface VariableDeclaratorNode {\n type: 'VariableDeclarator';\n id: IdentifierNode;\n init?: CallExpressionNode | null;\n // webpieces-disable no-any-unknown -- ESTree AST index signature\n [key: string]: any;\n}\n\ninterface CallExpressionNode {\n type: 'CallExpression';\n callee: IdentifierNode;\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\nfunction validateParamName(\n context: Rule.RuleContext,\n param: IdentifierNode,\n expectedParamName: string,\n): void {\n if (param.type === 'Identifier' && param.name !== expectedParamName) {\n context.report({\n node: param,\n messageId: 'wrongParameterName',\n data: { actual: param.name },\n });\n }\n}\n\nfunction validateTypeAnnotation(\n context: Rule.RuleContext,\n param: IdentifierNode,\n expectedParamName: string,\n): void {\n if (\n !param.typeAnnotation ||\n !param.typeAnnotation.typeAnnotation ||\n param.typeAnnotation.typeAnnotation.type !== 'TSUnknownKeyword'\n ) {\n context.report({\n node: param,\n messageId: 'missingTypeAnnotation',\n data: { param: param.name || expectedParamName },\n });\n }\n}\n\nfunction hasIgnoreComment(\n catchNode: CatchClauseNode,\n sourceCode: Rule.RuleContext['sourceCode'],\n expectedVarName: string,\n actualParamName: string,\n): boolean {\n const catchBlockStart = catchNode.body.range![0];\n const catchBlockEnd = catchNode.body.range![1];\n const catchBlockText = sourceCode.text.substring(catchBlockStart, catchBlockEnd);\n\n const ignorePattern = new RegExp(\n `//\\\\s*const\\\\s+${expectedVarName}\\\\s*=\\\\s*toError\\\\(${actualParamName}\\\\)`,\n );\n\n return ignorePattern.test(catchBlockText);\n}\n\nfunction reportMissingToError(\n context: Rule.RuleContext,\n // webpieces-disable no-any-unknown -- ESLint AST node param requires any type\n node: any,\n paramName: string,\n): void {\n context.report({\n node,\n messageId: 'missingToError',\n data: { param: paramName },\n });\n}\n\nfunction validateToErrorCall(\n context: Rule.RuleContext,\n // webpieces-disable no-any-unknown -- ESLint AST node param requires any type\n firstStatement: any,\n expectedParamName: string,\n expectedVarName: string,\n actualParamName: string,\n): void {\n if (firstStatement.type !== 'VariableDeclaration') {\n reportMissingToError(context, firstStatement, expectedParamName);\n return;\n }\n\n const varDecl = firstStatement as VariableDeclarationNode;\n const declaration = varDecl.declarations[0];\n if (!declaration) {\n reportMissingToError(context, firstStatement, expectedParamName);\n return;\n }\n\n if (declaration.id.type !== 'Identifier' || declaration.id.name !== expectedVarName) {\n context.report({\n node: declaration.id,\n messageId: 'wrongVariableName',\n data: { expected: expectedVarName, actual: declaration.id.name || 'unknown' },\n });\n return;\n }\n\n if (!declaration.init || declaration.init.type !== 'CallExpression') {\n reportMissingToError(context, declaration.init || declaration, expectedParamName);\n return;\n }\n\n const callExpr = declaration.init as CallExpressionNode;\n const callee = callExpr.callee;\n if (callee.type !== 'Identifier' || callee.name !== 'toError') {\n reportMissingToError(context, callee, expectedParamName);\n return;\n }\n\n const args = callExpr.arguments;\n if (\n args.length !== 1 ||\n args[0].type !== 'Identifier' ||\n (args[0] as IdentifierNode).name !== actualParamName\n ) {\n reportMissingToError(context, callExpr, actualParamName);\n }\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce standardized catch block error handling patterns',\n category: 'Best Practices',\n recommended: true,\n url: 'https://github.com/deanhiller/webpieces-ts/blob/main/claude.patterns.md#error-handling-pattern',\n },\n messages: {\n missingToError:\n 'Catch block must call toError({{param}}) as first statement or comment it out to explicitly ignore errors',\n wrongVariableName: 'Error variable must be named \"{{expected}}\", got \"{{actual}}\"',\n missingTypeAnnotation: 'Catch parameter must be typed as \"unknown\": catch ({{param}}: unknown)',\n wrongParameterName:\n 'Catch parameter must be named \"err\" (or \"err2\", \"err3\" for nested catches), got \"{{actual}}\"',\n toErrorNotFirst: 'toError({{param}}) must be the first statement in the catch block',\n },\n fixable: undefined,\n schema: [],\n },\n\n create(context: Rule.RuleContext): Rule.RuleListener {\n const catchStack: CatchClauseNode[] = [];\n\n return {\n // webpieces-disable no-any-unknown -- ESLint visitor callback receives untyped AST node\n CatchClause(node: any): void {\n const catchNode = node as CatchClauseNode;\n const depth = catchStack.length + 1;\n catchStack.push(catchNode);\n\n const suffix = depth === 1 ? '' : String(depth);\n const expectedParamName = 'err' + suffix;\n const expectedVarName = 'error' + suffix;\n\n const param = catchNode.param;\n if (!param) {\n context.report({\n node: catchNode,\n messageId: 'missingTypeAnnotation',\n data: { param: expectedParamName },\n });\n return;\n }\n\n const actualParamName =\n param.type === 'Identifier' ? param.name : expectedParamName;\n\n validateParamName(context, param, expectedParamName);\n validateTypeAnnotation(context, param, expectedParamName);\n\n const sourceCode = context.sourceCode || context.getSourceCode();\n if (hasIgnoreComment(catchNode, sourceCode, expectedVarName, actualParamName)) {\n return;\n }\n\n const body = catchNode.body.body;\n if (body.length === 0) {\n reportMissingToError(context, catchNode.body, expectedParamName);\n return;\n }\n\n validateToErrorCall(context, body[0], expectedParamName, expectedVarName, actualParamName);\n },\n\n 'CatchClause:exit'(): void {\n catchStack.pop();\n },\n };\n },\n};\n\nexport = rule;\n"]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * ESLint rule to enforce architecture boundaries
3
+ *
4
+ * Validates that imports from @webpieces/* packages comply with the
5
+ * blessed dependency graph in .graphs/dependencies.json
6
+ *
7
+ * Supports transitive dependencies: if A depends on B and B depends on C,
8
+ * then A can import from C.
9
+ *
10
+ * Configuration:
11
+ * '@webpieces/enforce-architecture': 'error'
12
+ */
13
+ import type { Rule } from 'eslint';
14
+ declare const rule: Rule.RuleModule;
15
+ export = rule;