@webpieces/eslint-rules 0.4.515 → 0.4.517
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 +29 -1
- package/package.json +2 -2
- package/src/index.d.ts +2 -1
- package/src/index.js +2 -1
- package/src/index.js.map +1 -1
- package/src/rules/max-method-lines.d.ts +21 -0
- package/src/rules/max-method-lines.js +44 -1
- package/src/rules/max-method-lines.js.map +1 -1
- package/src/test-container-policy.d.ts +30 -0
- package/src/test-container-policy.js +68 -0
- package/src/test-container-policy.js.map +1 -0
package/README.md
CHANGED
|
@@ -6,9 +6,37 @@ ESLint rules for WebPieces code patterns and architecture enforcement.
|
|
|
6
6
|
|
|
7
7
|
- `catch-error-pattern` - Enforce toError() usage in catch blocks
|
|
8
8
|
- `no-unmanaged-exceptions` - Discourage try-catch outside tests
|
|
9
|
-
- `max-method-lines` - Enforce maximum method length
|
|
9
|
+
- `max-method-lines` - Enforce maximum method length (test-framework container callbacks such as `describe(...)` are exempt in `*.spec.ts` / `*.test.ts` — see below)
|
|
10
10
|
- `max-file-lines` - Enforce maximum file length
|
|
11
11
|
- `enforce-architecture` - Enforce architecture dependency boundaries
|
|
12
12
|
- `no-json-property-primitive-type` - Ban @JsonProperty({ type: String/Number/Boolean })
|
|
13
13
|
- `require-typed-template` - Require [templateClassType] on ng-template with let- variables (Angular)
|
|
14
14
|
- `no-mat-cell-def` - Ban *matCellDef/*matHeaderCellDef — use div-grid tables (Angular)
|
|
15
|
+
|
|
16
|
+
## `max-method-lines` and test files
|
|
17
|
+
|
|
18
|
+
A `describe('...', () => { ... })` block is a **container**, not a unit of logic — its length
|
|
19
|
+
only reflects how many cases a behaviour needs. Counting it as an anonymous method pushed
|
|
20
|
+
authors to split describe blocks purely to satisfy a limit written for production methods,
|
|
21
|
+
inventing structure that maps to nothing. So in **test files only**, the callback passed
|
|
22
|
+
directly to a test-container call is not counted.
|
|
23
|
+
|
|
24
|
+
Still counted everywhere, including in test files:
|
|
25
|
+
|
|
26
|
+
- every `it(...)` / `test(...)` body — a 200-line test case is a real finding
|
|
27
|
+
- `beforeEach` / `beforeAll` / `afterEach` / `afterAll` — hooks are logic, not containers
|
|
28
|
+
- named or assigned helper functions inside spec files
|
|
29
|
+
- **everything in production code** — the carve-out never applies outside a test file
|
|
30
|
+
|
|
31
|
+
Both parts are configurable:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
'@webpieces/max-method-lines': ['error', {
|
|
35
|
+
max: 70,
|
|
36
|
+
testContainers: ['describe', 'suite', 'context'], // default
|
|
37
|
+
testFilePattern: '\\.(spec|test)\\.[cm]?[jt]sx?$', // default
|
|
38
|
+
}],
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`describe.only`, `describe.skip` and `describe.each([...])(...)` all resolve to the base
|
|
42
|
+
container name, so they are covered by listing `describe` alone.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/eslint-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.517",
|
|
4
4
|
"description": "ESLint rules for WebPieces code patterns and architecture enforcement",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"directory": "packages/tooling/eslint-rules"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@webpieces/rules-config": "0.4.
|
|
25
|
+
"@webpieces/rules-config": "0.4.517"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"eslint": ">=8.0.0"
|
package/src/index.d.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* Available rules:
|
|
8
8
|
* - catch-error-pattern: Enforce toError() usage in catch blocks (HOW to handle)
|
|
9
9
|
* - no-unmanaged-exceptions: Discourage try-catch outside tests (WHERE to handle)
|
|
10
|
-
* - max-method-lines: Enforce maximum method length (default: 70 lines
|
|
10
|
+
* - max-method-lines: Enforce maximum method length (default: 70 lines; describe/suite/context
|
|
11
|
+
* callbacks in test files are containers, not methods, and are not counted)
|
|
11
12
|
* - max-file-lines: Enforce maximum file length (default: 700 lines)
|
|
12
13
|
* - enforce-architecture: Enforce architecture dependency boundaries
|
|
13
14
|
* - no-json-property-primitive-type: Ban @JsonProperty({ type: String/Number/Boolean })
|
package/src/index.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* Available rules:
|
|
9
9
|
* - catch-error-pattern: Enforce toError() usage in catch blocks (HOW to handle)
|
|
10
10
|
* - no-unmanaged-exceptions: Discourage try-catch outside tests (WHERE to handle)
|
|
11
|
-
* - max-method-lines: Enforce maximum method length (default: 70 lines
|
|
11
|
+
* - max-method-lines: Enforce maximum method length (default: 70 lines; describe/suite/context
|
|
12
|
+
* callbacks in test files are containers, not methods, and are not counted)
|
|
12
13
|
* - max-file-lines: Enforce maximum file length (default: 700 lines)
|
|
13
14
|
* - enforce-architecture: Enforce architecture dependency boundaries
|
|
14
15
|
* - no-json-property-primitive-type: Ban @JsonProperty({ type: String/Number/Boolean })
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/tooling/eslint-rules/src/index.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/tooling/eslint-rules/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;AAEH,8FAA4D;AAC5D,sGAAoE;AACpE,wFAAsD;AACtD,oFAAkD;AAClD,gGAA+D;AAC/D,sHAAkF;AAClF,oGAAkE;AAClE,sFAAmD;AAEnD,iBAAS;IACL,KAAK,EAAE;QACH,qBAAqB,EAAE,6BAAiB;QACxC,yBAAyB,EAAE,iCAAqB;QAChD,kBAAkB,EAAE,0BAAc;QAClC,gBAAgB,EAAE,wBAAY;QAC9B,sBAAsB,EAAE,8BAAmB;QAC3C,iCAAiC,EAAE,yCAA2B;QAC9D,wBAAwB,EAAE,gCAAoB;QAC9C,iBAAiB,EAAE,yBAAY;KAClC;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/nx-webpieces-rules\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; describe/suite/context\n * callbacks in test files are containers, not methods, and are not counted)\n * - max-file-lines: Enforce maximum file length (default: 700 lines)\n * - enforce-architecture: Enforce architecture dependency boundaries\n * - no-json-property-primitive-type: Ban @JsonProperty({ type: String/Number/Boolean })\n * - require-typed-template: Require [templateClassType] on ng-template with let- variables (Angular)\n * - no-mat-cell-def: Ban *matCellDef/*matHeaderCellDef — use div-grid tables (Angular)\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';\nimport noJsonPropertyPrimitiveType from './rules/no-json-property-primitive-type';\nimport requireTypedTemplate from './rules/require-typed-template';\nimport noMatCellDef from './rules/no-mat-cell-def';\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 'no-json-property-primitive-type': noJsonPropertyPrimitiveType,\n 'require-typed-template': requireTypedTemplate,\n 'no-mat-cell-def': noMatCellDef,\n },\n};\n"]}
|
|
@@ -6,6 +6,27 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Configuration:
|
|
8
8
|
* '@webpieces/max-method-lines': ['error', { max: 70 }]
|
|
9
|
+
*
|
|
10
|
+
* WHY the test-container carve-out exists:
|
|
11
|
+
* In a spec file, `describe('...', () => { ... })` is a CONTAINER, not a unit of logic. Its
|
|
12
|
+
* length only says "how many cases does this behaviour need", so counting it as an anonymous
|
|
13
|
+
* method made agents split describe blocks purely to satisfy a limit written for production
|
|
14
|
+
* methods - inventing structure that maps to nothing. So in TEST FILES ONLY, the callback
|
|
15
|
+
* passed directly to a test-container call (describe/suite/context, incl. .each/.only/.skip)
|
|
16
|
+
* is not counted.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately NOT exempt (the rule's real purpose is untouched):
|
|
19
|
+
* - every `it(...)` / `test(...)` body - a 200-line test case IS worth flagging
|
|
20
|
+
* - `beforeEach` / `beforeAll` / `afterEach` / `afterAll` - those are logic, not containers
|
|
21
|
+
* - named helper functions inside spec files
|
|
22
|
+
* - anything whatsoever in a non-test file - production code is unaffected
|
|
23
|
+
*
|
|
24
|
+
* Full configuration (both extras optional, defaults shown):
|
|
25
|
+
* '@webpieces/max-method-lines': ['error', {
|
|
26
|
+
* max: 70,
|
|
27
|
+
* testContainers: ['describe', 'suite', 'context'],
|
|
28
|
+
* testFilePattern: '\\.(spec|test)\\.[cm]?[jt]sx?$',
|
|
29
|
+
* }]
|
|
9
30
|
*/
|
|
10
31
|
import type { Rule } from 'eslint';
|
|
11
32
|
declare const rule: Rule.RuleModule;
|
|
@@ -7,10 +7,32 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Configuration:
|
|
9
9
|
* '@webpieces/max-method-lines': ['error', { max: 70 }]
|
|
10
|
+
*
|
|
11
|
+
* WHY the test-container carve-out exists:
|
|
12
|
+
* In a spec file, `describe('...', () => { ... })` is a CONTAINER, not a unit of logic. Its
|
|
13
|
+
* length only says "how many cases does this behaviour need", so counting it as an anonymous
|
|
14
|
+
* method made agents split describe blocks purely to satisfy a limit written for production
|
|
15
|
+
* methods - inventing structure that maps to nothing. So in TEST FILES ONLY, the callback
|
|
16
|
+
* passed directly to a test-container call (describe/suite/context, incl. .each/.only/.skip)
|
|
17
|
+
* is not counted.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately NOT exempt (the rule's real purpose is untouched):
|
|
20
|
+
* - every `it(...)` / `test(...)` body - a 200-line test case IS worth flagging
|
|
21
|
+
* - `beforeEach` / `beforeAll` / `afterEach` / `afterAll` - those are logic, not containers
|
|
22
|
+
* - named helper functions inside spec files
|
|
23
|
+
* - anything whatsoever in a non-test file - production code is unaffected
|
|
24
|
+
*
|
|
25
|
+
* Full configuration (both extras optional, defaults shown):
|
|
26
|
+
* '@webpieces/max-method-lines': ['error', {
|
|
27
|
+
* max: 70,
|
|
28
|
+
* testContainers: ['describe', 'suite', 'context'],
|
|
29
|
+
* testFilePattern: '\\.(spec|test)\\.[cm]?[jt]sx?$',
|
|
30
|
+
* }]
|
|
10
31
|
*/
|
|
11
32
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
12
33
|
const toError_1 = require("../toError");
|
|
13
34
|
const workspace_root_1 = require("../workspace-root");
|
|
35
|
+
const test_container_policy_1 = require("../test-container-policy");
|
|
14
36
|
const INSTRUCT_FILE = 'webpieces.methods.md';
|
|
15
37
|
const workspace = new workspace_root_1.EslintWorkspaceRoot();
|
|
16
38
|
// Module-level flag to prevent redundant file creation
|
|
@@ -60,6 +82,10 @@ function checkFunctionNode(ctx, node) {
|
|
|
60
82
|
}
|
|
61
83
|
if (!funcNode.loc || !funcNode.body)
|
|
62
84
|
return;
|
|
85
|
+
// describe(...)/suite(...) callbacks in spec files are containers, not units of logic.
|
|
86
|
+
if (ctx.testContainerPolicy.isExemptContainerCallback(ctx.filename, funcNode)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
63
89
|
const name = getFunctionName(funcNode);
|
|
64
90
|
const lineCount = funcNode.loc.end.line - funcNode.loc.start.line + 1;
|
|
65
91
|
if (lineCount > ctx.maxLines) {
|
|
@@ -98,6 +124,17 @@ const rule = {
|
|
|
98
124
|
type: 'integer',
|
|
99
125
|
minimum: 1,
|
|
100
126
|
},
|
|
127
|
+
// Test-framework CONTAINER calls whose callback is not counted, in test
|
|
128
|
+
// files only. Do NOT add 'it'/'test'/'beforeEach' here - those bodies are
|
|
129
|
+
// logic and a long one is a real finding.
|
|
130
|
+
testContainers: {
|
|
131
|
+
type: 'array',
|
|
132
|
+
items: { type: 'string' },
|
|
133
|
+
},
|
|
134
|
+
// Which files count as test files for the carve-out above.
|
|
135
|
+
testFilePattern: {
|
|
136
|
+
type: 'string',
|
|
137
|
+
},
|
|
101
138
|
},
|
|
102
139
|
additionalProperties: false,
|
|
103
140
|
},
|
|
@@ -105,7 +142,13 @@ const rule = {
|
|
|
105
142
|
},
|
|
106
143
|
create(context) {
|
|
107
144
|
const options = context.options[0];
|
|
108
|
-
const ctx = {
|
|
145
|
+
const ctx = {
|
|
146
|
+
context,
|
|
147
|
+
maxLines: options?.max ?? 70,
|
|
148
|
+
// ESLint 9 exposes context.filename; older versions only getFilename().
|
|
149
|
+
filename: context.filename ?? context.getFilename() ?? '',
|
|
150
|
+
testContainerPolicy: new test_container_policy_1.TestContainerPolicy(options?.testContainers ?? test_container_policy_1.DEFAULT_TEST_CONTAINERS, options?.testFilePattern ?? test_container_policy_1.DEFAULT_TEST_FILE_PATTERN),
|
|
151
|
+
};
|
|
109
152
|
return {
|
|
110
153
|
FunctionDeclaration: (node) => checkFunctionNode(ctx, node),
|
|
111
154
|
FunctionExpression: (node) => checkFunctionNode(ctx, node),
|
|
@@ -1 +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 { writeTemplateIfMissing } from '@webpieces/rules-config';\nimport { toError } from '../toError';\nimport { EslintWorkspaceRoot } from '../workspace-root';\n\nconst INSTRUCT_FILE = 'webpieces.methods.md';\nconst workspace = new EslintWorkspaceRoot();\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\n// Module-level flag to prevent redundant file creation\nlet methodDocCreated = false;\n\nfunction ensureMethodDoc(context: Rule.RuleContext): void {\n if (methodDocCreated) return;\n const workspaceRoot = workspace.workspaceRoot(context);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n writeTemplateIfMissing(workspaceRoot, INSTRUCT_FILE);\n methodDocCreated = true;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn('[webpieces] Could not write webpieces.methods.md', error);\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 (at the repo root) 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"]}
|
|
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 * WHY the test-container carve-out exists:\n * In a spec file, `describe('...', () => { ... })` is a CONTAINER, not a unit of logic. Its\n * length only says \"how many cases does this behaviour need\", so counting it as an anonymous\n * method made agents split describe blocks purely to satisfy a limit written for production\n * methods - inventing structure that maps to nothing. So in TEST FILES ONLY, the callback\n * passed directly to a test-container call (describe/suite/context, incl. .each/.only/.skip)\n * is not counted.\n *\n * Deliberately NOT exempt (the rule's real purpose is untouched):\n * - every `it(...)` / `test(...)` body - a 200-line test case IS worth flagging\n * - `beforeEach` / `beforeAll` / `afterEach` / `afterAll` - those are logic, not containers\n * - named helper functions inside spec files\n * - anything whatsoever in a non-test file - production code is unaffected\n *\n * Full configuration (both extras optional, defaults shown):\n * '@webpieces/max-method-lines': ['error', {\n * max: 70,\n * testContainers: ['describe', 'suite', 'context'],\n * testFilePattern: '\\\\.(spec|test)\\\\.[cm]?[jt]sx?$',\n * }]\n */\n\nimport type { Rule } from 'eslint';\nimport { writeTemplateIfMissing } from '@webpieces/rules-config';\nimport { toError } from '../toError';\nimport { EslintWorkspaceRoot } from '../workspace-root';\nimport { TestContainerPolicy, DEFAULT_TEST_CONTAINERS, DEFAULT_TEST_FILE_PATTERN } from '../test-container-policy';\n\nconst INSTRUCT_FILE = 'webpieces.methods.md';\nconst workspace = new EslintWorkspaceRoot();\n\ninterface MethodLinesOptions {\n max: number;\n testContainers?: string[];\n testFilePattern?: string;\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 testContainerPolicy: TestContainerPolicy;\n filename: string;\n}\n\n// Module-level flag to prevent redundant file creation\nlet methodDocCreated = false;\n\nfunction ensureMethodDoc(context: Rule.RuleContext): void {\n if (methodDocCreated) return;\n const workspaceRoot = workspace.workspaceRoot(context);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n writeTemplateIfMissing(workspaceRoot, INSTRUCT_FILE);\n methodDocCreated = true;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn('[webpieces] Could not write webpieces.methods.md', error);\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 // describe(...)/suite(...) callbacks in spec files are containers, not units of logic.\n if (ctx.testContainerPolicy.isExemptContainerCallback(ctx.filename, funcNode)) {\n return;\n }\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 (at the repo root) 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 // Test-framework CONTAINER calls whose callback is not counted, in test\n // files only. Do NOT add 'it'/'test'/'beforeEach' here - those bodies are\n // logic and a long one is a real finding.\n testContainers: {\n type: 'array',\n items: { type: 'string' },\n },\n // Which files count as test files for the carve-out above.\n testFilePattern: {\n type: 'string',\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 = {\n context,\n maxLines: options?.max ?? 70,\n // ESLint 9 exposes context.filename; older versions only getFilename().\n filename: context.filename ?? context.getFilename() ?? '',\n testContainerPolicy: new TestContainerPolicy(\n options?.testContainers ?? DEFAULT_TEST_CONTAINERS,\n options?.testFilePattern ?? DEFAULT_TEST_FILE_PATTERN\n ),\n };\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,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestContainerPolicy — decides whether a function node is a test-framework CONTAINER
|
|
3
|
+
* callback inside a test file, and therefore not a "method" worth measuring.
|
|
4
|
+
*
|
|
5
|
+
* A `describe('...', () => { ... })` block groups cases; its line count reflects how many
|
|
6
|
+
* cases a behaviour needs, not how much logic one unit carries. Splitting it to satisfy a
|
|
7
|
+
* production-method line limit invents structure that maps to nothing. Everything else in a
|
|
8
|
+
* spec file - `it` bodies, hooks, helper functions - is still measured, and NOTHING in a
|
|
9
|
+
* non-test file is ever exempt.
|
|
10
|
+
*/
|
|
11
|
+
export declare const DEFAULT_TEST_CONTAINERS: readonly string[];
|
|
12
|
+
/** Matches foo.spec.ts / foo.test.tsx / foo.spec.mjs etc. */
|
|
13
|
+
export declare const DEFAULT_TEST_FILE_PATTERN = "\\.(spec|test)\\.[cm]?[jt]sx?$";
|
|
14
|
+
export declare class TestContainerPolicy {
|
|
15
|
+
private readonly containers;
|
|
16
|
+
private readonly testFileRegex;
|
|
17
|
+
constructor(containers?: readonly string[], testFilePattern?: string);
|
|
18
|
+
isTestFile(filename: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* True only when `node` is a function passed as an argument DIRECTLY to a call whose
|
|
21
|
+
* base callee is a configured container name (describe, describe.only, describe.each(...)),
|
|
22
|
+
* inside a test file.
|
|
23
|
+
*/
|
|
24
|
+
isExemptContainerCallback(filename: string, node: any): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Walks `describe`, `describe.only`, `describe.each([...])` down to the root identifier
|
|
27
|
+
* name so every dialect of the same container is recognised.
|
|
28
|
+
*/
|
|
29
|
+
private baseCalleeName;
|
|
30
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* TestContainerPolicy — decides whether a function node is a test-framework CONTAINER
|
|
4
|
+
* callback inside a test file, and therefore not a "method" worth measuring.
|
|
5
|
+
*
|
|
6
|
+
* A `describe('...', () => { ... })` block groups cases; its line count reflects how many
|
|
7
|
+
* cases a behaviour needs, not how much logic one unit carries. Splitting it to satisfy a
|
|
8
|
+
* production-method line limit invents structure that maps to nothing. Everything else in a
|
|
9
|
+
* spec file - `it` bodies, hooks, helper functions - is still measured, and NOTHING in a
|
|
10
|
+
* non-test file is ever exempt.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.TestContainerPolicy = exports.DEFAULT_TEST_FILE_PATTERN = exports.DEFAULT_TEST_CONTAINERS = void 0;
|
|
14
|
+
exports.DEFAULT_TEST_CONTAINERS = ['describe', 'suite', 'context'];
|
|
15
|
+
/** Matches foo.spec.ts / foo.test.tsx / foo.spec.mjs etc. */
|
|
16
|
+
exports.DEFAULT_TEST_FILE_PATTERN = '\\.(spec|test)\\.[cm]?[jt]sx?$';
|
|
17
|
+
class TestContainerPolicy {
|
|
18
|
+
containers;
|
|
19
|
+
testFileRegex;
|
|
20
|
+
constructor(containers, testFilePattern) {
|
|
21
|
+
this.containers = new Set(containers ?? exports.DEFAULT_TEST_CONTAINERS);
|
|
22
|
+
this.testFileRegex = new RegExp(testFilePattern ?? exports.DEFAULT_TEST_FILE_PATTERN);
|
|
23
|
+
}
|
|
24
|
+
isTestFile(filename) {
|
|
25
|
+
return this.testFileRegex.test(filename.replace(/\\/g, '/'));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* True only when `node` is a function passed as an argument DIRECTLY to a call whose
|
|
29
|
+
* base callee is a configured container name (describe, describe.only, describe.each(...)),
|
|
30
|
+
* inside a test file.
|
|
31
|
+
*/
|
|
32
|
+
// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties
|
|
33
|
+
isExemptContainerCallback(filename, node) {
|
|
34
|
+
if (!this.isTestFile(filename))
|
|
35
|
+
return false;
|
|
36
|
+
const parent = node?.['parent'];
|
|
37
|
+
if (!parent || parent.type !== 'CallExpression')
|
|
38
|
+
return false;
|
|
39
|
+
if (!Array.isArray(parent.arguments) || !parent.arguments.includes(node))
|
|
40
|
+
return false;
|
|
41
|
+
const base = this.baseCalleeName(parent.callee);
|
|
42
|
+
return base !== null && this.containers.has(base);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Walks `describe`, `describe.only`, `describe.each([...])` down to the root identifier
|
|
46
|
+
* name so every dialect of the same container is recognised.
|
|
47
|
+
*/
|
|
48
|
+
// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties
|
|
49
|
+
baseCalleeName(callee) {
|
|
50
|
+
let current = callee;
|
|
51
|
+
while (current) {
|
|
52
|
+
if (current.type === 'Identifier')
|
|
53
|
+
return current.name ?? null;
|
|
54
|
+
if (current.type === 'MemberExpression') {
|
|
55
|
+
current = current.object;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (current.type === 'CallExpression' || current.type === 'TaggedTemplateExpression') {
|
|
59
|
+
current = current.callee ?? current.tag;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.TestContainerPolicy = TestContainerPolicy;
|
|
68
|
+
//# sourceMappingURL=test-container-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-container-policy.js","sourceRoot":"","sources":["../../../../../packages/tooling/eslint-rules/src/test-container-policy.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AAEU,QAAA,uBAAuB,GAAsB,CAAC,UAAU,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;AAE3F,6DAA6D;AAChD,QAAA,yBAAyB,GAAG,gCAAgC,CAAC;AAE1E,MAAa,mBAAmB;IACX,UAAU,CAAc;IACxB,aAAa,CAAS;IAEvC,YAAY,UAA8B,EAAE,eAAwB;QAChE,IAAI,CAAC,UAAU,GAAG,IAAI,GAAG,CAAC,UAAU,IAAI,+BAAuB,CAAC,CAAC;QACjE,IAAI,CAAC,aAAa,GAAG,IAAI,MAAM,CAAC,eAAe,IAAI,iCAAyB,CAAC,CAAC;IAClF,CAAC;IAED,UAAU,CAAC,QAAgB;QACvB,OAAO,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC;IACjE,CAAC;IAED;;;;OAIG;IACH,0FAA0F;IAC1F,yBAAyB,CAAC,QAAgB,EAAE,IAAS;QACjD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,KAAK,CAAC;QAE7C,MAAM,MAAM,GAAG,IAAI,EAAE,CAAC,QAAQ,CAAC,CAAC;QAChC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,gBAAgB;YAAE,OAAO,KAAK,CAAC;QAC9D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QAEvF,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAChD,OAAO,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACtD,CAAC;IAED;;;OAGG;IACH,0FAA0F;IAClF,cAAc,CAAC,MAAW;QAC9B,IAAI,OAAO,GAAG,MAAM,CAAC;QACrB,OAAO,OAAO,EAAE,CAAC;YACb,IAAI,OAAO,CAAC,IAAI,KAAK,YAAY;gBAAE,OAAO,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC;YAC/D,IAAI,OAAO,CAAC,IAAI,KAAK,kBAAkB,EAAE,CAAC;gBACtC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;gBACzB,SAAS;YACb,CAAC;YACD,IAAI,OAAO,CAAC,IAAI,KAAK,gBAAgB,IAAI,OAAO,CAAC,IAAI,KAAK,0BAA0B,EAAE,CAAC;gBACnF,OAAO,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC;gBACxC,SAAS;YACb,CAAC;YACD,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;CACJ;AAnDD,kDAmDC","sourcesContent":["/**\n * TestContainerPolicy — decides whether a function node is a test-framework CONTAINER\n * callback inside a test file, and therefore not a \"method\" worth measuring.\n *\n * A `describe('...', () => { ... })` block groups cases; its line count reflects how many\n * cases a behaviour needs, not how much logic one unit carries. Splitting it to satisfy a\n * production-method line limit invents structure that maps to nothing. Everything else in a\n * spec file - `it` bodies, hooks, helper functions - is still measured, and NOTHING in a\n * non-test file is ever exempt.\n */\n\nexport const DEFAULT_TEST_CONTAINERS: readonly string[] = ['describe', 'suite', 'context'];\n\n/** Matches foo.spec.ts / foo.test.tsx / foo.spec.mjs etc. */\nexport const DEFAULT_TEST_FILE_PATTERN = '\\\\.(spec|test)\\\\.[cm]?[jt]sx?$';\n\nexport class TestContainerPolicy {\n private readonly containers: Set<string>;\n private readonly testFileRegex: RegExp;\n\n constructor(containers?: readonly string[], testFilePattern?: string) {\n this.containers = new Set(containers ?? DEFAULT_TEST_CONTAINERS);\n this.testFileRegex = new RegExp(testFilePattern ?? DEFAULT_TEST_FILE_PATTERN);\n }\n\n isTestFile(filename: string): boolean {\n return this.testFileRegex.test(filename.replace(/\\\\/g, '/'));\n }\n\n /**\n * True only when `node` is a function passed as an argument DIRECTLY to a call whose\n * base callee is a configured container name (describe, describe.only, describe.each(...)),\n * inside a test file.\n */\n // webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\n isExemptContainerCallback(filename: string, node: any): boolean {\n if (!this.isTestFile(filename)) return false;\n\n const parent = node?.['parent'];\n if (!parent || parent.type !== 'CallExpression') return false;\n if (!Array.isArray(parent.arguments) || !parent.arguments.includes(node)) return false;\n\n const base = this.baseCalleeName(parent.callee);\n return base !== null && this.containers.has(base);\n }\n\n /**\n * Walks `describe`, `describe.only`, `describe.each([...])` down to the root identifier\n * name so every dialect of the same container is recognised.\n */\n // webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\n private baseCalleeName(callee: any): string | null {\n let current = callee;\n while (current) {\n if (current.type === 'Identifier') return current.name ?? null;\n if (current.type === 'MemberExpression') {\n current = current.object;\n continue;\n }\n if (current.type === 'CallExpression' || current.type === 'TaggedTemplateExpression') {\n current = current.callee ?? current.tag;\n continue;\n }\n return null;\n }\n return null;\n }\n}\n"]}
|