@webpieces/ai-hooks 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/README.md +43 -0
- package/bin/setup-ai-hooks.sh +137 -0
- package/openclaw.plugin.json +15 -0
- package/package.json +35 -0
- package/src/adapters/claude-code-hook.d.ts +1 -0
- package/src/adapters/claude-code-hook.js +97 -0
- package/src/adapters/claude-code-hook.js.map +1 -0
- package/src/adapters/openclaw-plugin.d.ts +14 -0
- package/src/adapters/openclaw-plugin.js +73 -0
- package/src/adapters/openclaw-plugin.js.map +1 -0
- package/src/core/build-context.d.ts +7 -0
- package/src/core/build-context.js +58 -0
- package/src/core/build-context.js.map +1 -0
- package/src/core/configs/default.d.ts +2 -0
- package/src/core/configs/default.js +22 -0
- package/src/core/configs/default.js.map +1 -0
- package/src/core/disable-directives.d.ts +9 -0
- package/src/core/disable-directives.js +92 -0
- package/src/core/disable-directives.js.map +1 -0
- package/src/core/load-config.d.ts +3 -0
- package/src/core/load-config.js +81 -0
- package/src/core/load-config.js.map +1 -0
- package/src/core/load-rules.d.ts +3 -0
- package/src/core/load-rules.js +131 -0
- package/src/core/load-rules.js.map +1 -0
- package/src/core/rejection-log.d.ts +2 -0
- package/src/core/rejection-log.js +146 -0
- package/src/core/rejection-log.js.map +1 -0
- package/src/core/report.d.ts +2 -0
- package/src/core/report.js +34 -0
- package/src/core/report.js.map +1 -0
- package/src/core/rules/catch-error-pattern.d.ts +3 -0
- package/src/core/rules/catch-error-pattern.js +91 -0
- package/src/core/rules/catch-error-pattern.js.map +1 -0
- package/src/core/rules/file-location.d.ts +3 -0
- package/src/core/rules/file-location.js +73 -0
- package/src/core/rules/file-location.js.map +1 -0
- package/src/core/rules/index.d.ts +1 -0
- package/src/core/rules/index.js +13 -0
- package/src/core/rules/index.js.map +1 -0
- package/src/core/rules/max-file-lines.d.ts +3 -0
- package/src/core/rules/max-file-lines.js +131 -0
- package/src/core/rules/max-file-lines.js.map +1 -0
- package/src/core/rules/no-any-unknown.d.ts +3 -0
- package/src/core/rules/no-any-unknown.js +30 -0
- package/src/core/rules/no-any-unknown.js.map +1 -0
- package/src/core/rules/no-destructure.d.ts +3 -0
- package/src/core/rules/no-destructure.js +30 -0
- package/src/core/rules/no-destructure.js.map +1 -0
- package/src/core/rules/no-unmanaged-exceptions.d.ts +3 -0
- package/src/core/rules/no-unmanaged-exceptions.js +41 -0
- package/src/core/rules/no-unmanaged-exceptions.js.map +1 -0
- package/src/core/rules/require-return-type.d.ts +3 -0
- package/src/core/rules/require-return-type.js +52 -0
- package/src/core/rules/require-return-type.js.map +1 -0
- package/src/core/runner.d.ts +2 -0
- package/src/core/runner.js +127 -0
- package/src/core/runner.js.map +1 -0
- package/src/core/strip-ts-noise.d.ts +1 -0
- package/src/core/strip-ts-noise.js +178 -0
- package/src/core/strip-ts-noise.js.map +1 -0
- package/src/core/to-error.d.ts +5 -0
- package/src/core/to-error.js +38 -0
- package/src/core/to-error.js.map +1 -0
- package/src/core/types.d.ts +89 -0
- package/src/core/types.js +90 -0
- package/src/core/types.js.map +1 -0
- package/src/index.d.ts +5 -0
- package/src/index.js +24 -0
- package/src/index.js.map +1 -0
- package/templates/claude-settings-hook.json +15 -0
- package/templates/webpieces.ai-hooks.seed.json +16 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const types_1 = require("../types");
|
|
4
|
+
/**
|
|
5
|
+
* Matches a catch clause opening: } catch (paramName: typeAnnotation) {
|
|
6
|
+
* Captures: group 1 = param name, group 2 = type annotation (if present)
|
|
7
|
+
*/
|
|
8
|
+
const CATCH_PATTERN = /\bcatch\s*\(\s*(\w+)(?:\s*:\s*(\w+))?\s*\)/;
|
|
9
|
+
/**
|
|
10
|
+
* Matches the required toError first statement (with or without comment-out).
|
|
11
|
+
* Group 1 = variable name, group 2 = param passed to toError
|
|
12
|
+
*/
|
|
13
|
+
const TO_ERROR_PATTERN = /^\s*(?:\/\/\s*)?const\s+(\w+)\s*=\s*toError\(\s*(\w+)\s*\)\s*;?\s*$/;
|
|
14
|
+
const catchErrorPatternRule = {
|
|
15
|
+
name: 'catch-error-pattern',
|
|
16
|
+
description: 'Catch blocks must use: catch (err: unknown) { const error = toError(err); }',
|
|
17
|
+
scope: 'edit',
|
|
18
|
+
files: ['**/*.ts', '**/*.tsx'],
|
|
19
|
+
defaultOptions: {},
|
|
20
|
+
fixHint: [
|
|
21
|
+
'catch (err: unknown) { const error = toError(err); ... }',
|
|
22
|
+
'Or to explicitly ignore: catch (err: unknown) { //const error = toError(err); }',
|
|
23
|
+
'For nested catches: catch (err2: unknown) { const error2 = toError(err2); }',
|
|
24
|
+
'// webpieces-disable catch-error-pattern -- <reason>',
|
|
25
|
+
],
|
|
26
|
+
check(ctx) {
|
|
27
|
+
const violations = [];
|
|
28
|
+
const lines = ctx.strippedLines;
|
|
29
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
30
|
+
const stripped = lines[i];
|
|
31
|
+
const catchMatch = CATCH_PATTERN.exec(stripped);
|
|
32
|
+
if (!catchMatch)
|
|
33
|
+
continue;
|
|
34
|
+
const lineNum = i + 1;
|
|
35
|
+
if (ctx.isLineDisabled(lineNum, 'catch-error-pattern'))
|
|
36
|
+
continue;
|
|
37
|
+
const actualParam = catchMatch[1];
|
|
38
|
+
const typeAnnotation = catchMatch[2];
|
|
39
|
+
// Determine expected names from suffix on the actual param (err, err2, err3...)
|
|
40
|
+
const suffixMatch = actualParam.match(/^err(\d*)$/);
|
|
41
|
+
const suffix = suffixMatch ? suffixMatch[1] : '';
|
|
42
|
+
const expectedParam = 'err' + suffix;
|
|
43
|
+
const expectedVar = 'error' + suffix;
|
|
44
|
+
// Check parameter name
|
|
45
|
+
if (actualParam !== expectedParam) {
|
|
46
|
+
violations.push(new types_1.Violation(lineNum, ctx.lines[i].trim(), `Catch parameter must be named "${expectedParam}" (or "err2", "err3" for nested catches), got "${actualParam}"`));
|
|
47
|
+
}
|
|
48
|
+
// Check type annotation is unknown
|
|
49
|
+
if (typeAnnotation !== 'unknown') {
|
|
50
|
+
const msg = typeAnnotation
|
|
51
|
+
? `Catch parameter must be typed as "unknown": catch (${expectedParam}: unknown), got "${typeAnnotation}"`
|
|
52
|
+
: `Catch parameter must be typed as "unknown": catch (${expectedParam}: unknown)`;
|
|
53
|
+
violations.push(new types_1.Violation(lineNum, ctx.lines[i].trim(), msg));
|
|
54
|
+
}
|
|
55
|
+
// Find next non-blank line after the catch opening to check for toError
|
|
56
|
+
const toErrorResult = findToErrorStatement(lines, i + 1);
|
|
57
|
+
if (toErrorResult === 'not-found') {
|
|
58
|
+
violations.push(new types_1.Violation(lineNum, ctx.lines[i].trim(), `Catch block must call toError(${actualParam}) as first statement: const ${expectedVar} = toError(${actualParam}); or //const ${expectedVar} = toError(${actualParam});`));
|
|
59
|
+
}
|
|
60
|
+
else if (toErrorResult !== 'end-of-content') {
|
|
61
|
+
// Validate variable name and param match
|
|
62
|
+
if (toErrorResult.varName !== expectedVar) {
|
|
63
|
+
const toErrorLineNum = toErrorResult.lineIndex + 1;
|
|
64
|
+
violations.push(new types_1.Violation(toErrorLineNum, ctx.lines[toErrorResult.lineIndex].trim(), `Error variable must be named "${expectedVar}", got "${toErrorResult.varName}"`));
|
|
65
|
+
}
|
|
66
|
+
if (toErrorResult.paramName !== actualParam) {
|
|
67
|
+
const toErrorLineNum = toErrorResult.lineIndex + 1;
|
|
68
|
+
violations.push(new types_1.Violation(toErrorLineNum, ctx.lines[toErrorResult.lineIndex].trim(), `toError() must be called with "${actualParam}", got "${toErrorResult.paramName}"`));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return violations;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
function findToErrorStatement(lines, startIndex) {
|
|
76
|
+
for (let j = startIndex; j < lines.length; j += 1) {
|
|
77
|
+
const line = lines[j].trim();
|
|
78
|
+
if (line === '' || line === '{')
|
|
79
|
+
continue;
|
|
80
|
+
const match = TO_ERROR_PATTERN.exec(line);
|
|
81
|
+
if (match) {
|
|
82
|
+
return { varName: match[1], paramName: match[2], lineIndex: j };
|
|
83
|
+
}
|
|
84
|
+
// First non-blank line is not a toError call
|
|
85
|
+
return 'not-found';
|
|
86
|
+
}
|
|
87
|
+
// Ran off the end of the edit content — can't validate further
|
|
88
|
+
return 'end-of-content';
|
|
89
|
+
}
|
|
90
|
+
exports.default = catchErrorPatternRule;
|
|
91
|
+
//# sourceMappingURL=catch-error-pattern.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catch-error-pattern.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hooks/src/core/rules/catch-error-pattern.ts"],"names":[],"mappings":"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","sourcesContent":["import type { EditRule, EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\n/**\n * Matches a catch clause opening: } catch (paramName: typeAnnotation) {\n * Captures: group 1 = param name, group 2 = type annotation (if present)\n */\nconst CATCH_PATTERN = /\\bcatch\\s*\\(\\s*(\\w+)(?:\\s*:\\s*(\\w+))?\\s*\\)/;\n\n/**\n * Matches the required toError first statement (with or without comment-out).\n * Group 1 = variable name, group 2 = param passed to toError\n */\nconst TO_ERROR_PATTERN = /^\\s*(?:\\/\\/\\s*)?const\\s+(\\w+)\\s*=\\s*toError\\(\\s*(\\w+)\\s*\\)\\s*;?\\s*$/;\n\nconst catchErrorPatternRule: EditRule = {\n name: 'catch-error-pattern',\n description: 'Catch blocks must use: catch (err: unknown) { const error = toError(err); }',\n scope: 'edit',\n files: ['**/*.ts', '**/*.tsx'],\n defaultOptions: {},\n fixHint: [\n 'catch (err: unknown) { const error = toError(err); ... }',\n 'Or to explicitly ignore: catch (err: unknown) { //const error = toError(err); }',\n 'For nested catches: catch (err2: unknown) { const error2 = toError(err2); }',\n '// webpieces-disable catch-error-pattern -- <reason>',\n ],\n\n check(ctx: EditContext): readonly Violation[] {\n const violations: V[] = [];\n const lines = ctx.strippedLines;\n\n for (let i = 0; i < lines.length; i += 1) {\n const stripped = lines[i];\n const catchMatch = CATCH_PATTERN.exec(stripped);\n if (!catchMatch) continue;\n\n const lineNum = i + 1;\n if (ctx.isLineDisabled(lineNum, 'catch-error-pattern')) continue;\n\n const actualParam = catchMatch[1];\n const typeAnnotation = catchMatch[2];\n\n // Determine expected names from suffix on the actual param (err, err2, err3...)\n const suffixMatch = actualParam.match(/^err(\\d*)$/);\n const suffix = suffixMatch ? suffixMatch[1] : '';\n const expectedParam = 'err' + suffix;\n const expectedVar = 'error' + suffix;\n\n // Check parameter name\n if (actualParam !== expectedParam) {\n violations.push(new V(\n lineNum,\n ctx.lines[i].trim(),\n `Catch parameter must be named \"${expectedParam}\" (or \"err2\", \"err3\" for nested catches), got \"${actualParam}\"`,\n ));\n }\n\n // Check type annotation is unknown\n if (typeAnnotation !== 'unknown') {\n const msg = typeAnnotation\n ? `Catch parameter must be typed as \"unknown\": catch (${expectedParam}: unknown), got \"${typeAnnotation}\"`\n : `Catch parameter must be typed as \"unknown\": catch (${expectedParam}: unknown)`;\n violations.push(new V(lineNum, ctx.lines[i].trim(), msg));\n }\n\n // Find next non-blank line after the catch opening to check for toError\n const toErrorResult = findToErrorStatement(lines, i + 1);\n if (toErrorResult === 'not-found') {\n violations.push(new V(\n lineNum,\n ctx.lines[i].trim(),\n `Catch block must call toError(${actualParam}) as first statement: const ${expectedVar} = toError(${actualParam}); or //const ${expectedVar} = toError(${actualParam});`,\n ));\n } else if (toErrorResult !== 'end-of-content') {\n // Validate variable name and param match\n if (toErrorResult.varName !== expectedVar) {\n const toErrorLineNum = toErrorResult.lineIndex + 1;\n violations.push(new V(\n toErrorLineNum,\n ctx.lines[toErrorResult.lineIndex].trim(),\n `Error variable must be named \"${expectedVar}\", got \"${toErrorResult.varName}\"`,\n ));\n }\n if (toErrorResult.paramName !== actualParam) {\n const toErrorLineNum = toErrorResult.lineIndex + 1;\n violations.push(new V(\n toErrorLineNum,\n ctx.lines[toErrorResult.lineIndex].trim(),\n `toError() must be called with \"${actualParam}\", got \"${toErrorResult.paramName}\"`,\n ));\n }\n }\n }\n\n return violations;\n },\n};\n\ninterface ToErrorMatch {\n varName: string;\n paramName: string;\n lineIndex: number;\n}\n\nfunction findToErrorStatement(lines: readonly string[], startIndex: number): ToErrorMatch | 'not-found' | 'end-of-content' {\n for (let j = startIndex; j < lines.length; j += 1) {\n const line = lines[j].trim();\n if (line === '' || line === '{') continue;\n\n const match = TO_ERROR_PATTERN.exec(line);\n if (match) {\n return { varName: match[1], paramName: match[2], lineIndex: j };\n }\n // First non-blank line is not a toError call\n return 'not-found';\n }\n // Ran off the end of the edit content — can't validate further\n return 'end-of-content';\n}\n\nexport default catchErrorPatternRule;\n"]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
5
|
+
const path = tslib_1.__importStar(require("path"));
|
|
6
|
+
const types_1 = require("../types");
|
|
7
|
+
const DEFAULT_EXCLUDE_PATHS = [
|
|
8
|
+
'node_modules', 'dist', '.nx', '.git',
|
|
9
|
+
'architecture', 'tmp', 'scripts',
|
|
10
|
+
];
|
|
11
|
+
const DEFAULT_ALLOWED_ROOT_FILES = ['jest.setup.ts'];
|
|
12
|
+
function isNodeModulesDir(name) {
|
|
13
|
+
return name === 'node_modules' || name.startsWith('node_modules_');
|
|
14
|
+
}
|
|
15
|
+
function shouldSkipDir(name, excludePaths) {
|
|
16
|
+
if (isNodeModulesDir(name))
|
|
17
|
+
return true;
|
|
18
|
+
return excludePaths.indexOf(name) >= 0;
|
|
19
|
+
}
|
|
20
|
+
function findProjectRoot(filePath, workspaceRoot) {
|
|
21
|
+
let dir = path.dirname(filePath);
|
|
22
|
+
while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {
|
|
23
|
+
if (fs.existsSync(path.join(dir, 'project.json')))
|
|
24
|
+
return dir;
|
|
25
|
+
dir = path.dirname(dir);
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const fileLocationRule = {
|
|
30
|
+
name: 'file-location',
|
|
31
|
+
description: 'Every .ts file must belong to a project\'s src/ directory.',
|
|
32
|
+
scope: 'file',
|
|
33
|
+
files: ['**/*.ts', '**/*.tsx'],
|
|
34
|
+
defaultOptions: {
|
|
35
|
+
excludePaths: DEFAULT_EXCLUDE_PATHS,
|
|
36
|
+
allowedRootFiles: DEFAULT_ALLOWED_ROOT_FILES,
|
|
37
|
+
},
|
|
38
|
+
fixHint: [
|
|
39
|
+
'Move the file into an existing project\'s src/ directory, or create a new project with project.json that owns the directory.',
|
|
40
|
+
'Add the dir to file-location.excludePaths in webpieces.ai-hooks.json',
|
|
41
|
+
],
|
|
42
|
+
check(ctx) {
|
|
43
|
+
if (ctx.tool !== 'Write')
|
|
44
|
+
return [];
|
|
45
|
+
const excludePaths = Array.isArray(ctx.options['excludePaths'])
|
|
46
|
+
? ctx.options['excludePaths']
|
|
47
|
+
: DEFAULT_EXCLUDE_PATHS;
|
|
48
|
+
const allowedRootFiles = Array.isArray(ctx.options['allowedRootFiles'])
|
|
49
|
+
? ctx.options['allowedRootFiles']
|
|
50
|
+
: DEFAULT_ALLOWED_ROOT_FILES;
|
|
51
|
+
const relParts = ctx.relativePath.split(path.sep);
|
|
52
|
+
const topDir = relParts[0];
|
|
53
|
+
if (topDir && shouldSkipDir(topDir, excludePaths))
|
|
54
|
+
return [];
|
|
55
|
+
if (relParts.length === 1 && allowedRootFiles.indexOf(relParts[0]) >= 0)
|
|
56
|
+
return [];
|
|
57
|
+
const projectRoot = findProjectRoot(ctx.filePath, ctx.workspaceRoot);
|
|
58
|
+
if (!projectRoot) {
|
|
59
|
+
return [new types_1.Violation(1, ctx.relativePath, 'File is not inside any Nx project. Move it into a project\'s src/ directory.')];
|
|
60
|
+
}
|
|
61
|
+
const relToProject = path.relative(projectRoot, ctx.filePath);
|
|
62
|
+
const fileName = path.basename(ctx.filePath);
|
|
63
|
+
if (fileName === 'jest.config.ts')
|
|
64
|
+
return [];
|
|
65
|
+
if (!relToProject.startsWith('src' + path.sep) && relToProject !== 'src') {
|
|
66
|
+
const projectName = path.relative(ctx.workspaceRoot, projectRoot);
|
|
67
|
+
return [new types_1.Violation(1, ctx.relativePath, `File is inside project \`${projectName}\` but outside its src/ directory. Move it into src/.`)];
|
|
68
|
+
}
|
|
69
|
+
return [];
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
exports.default = fileLocationRule;
|
|
73
|
+
//# sourceMappingURL=file-location.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-location.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hooks/src/core/rules/file-location.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport type { FileRule, FileContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst DEFAULT_EXCLUDE_PATHS = [\n 'node_modules', 'dist', '.nx', '.git',\n 'architecture', 'tmp', 'scripts',\n];\nconst DEFAULT_ALLOWED_ROOT_FILES = ['jest.setup.ts'];\n\nfunction isNodeModulesDir(name: string): boolean {\n return name === 'node_modules' || name.startsWith('node_modules_');\n}\n\nfunction shouldSkipDir(name: string, excludePaths: readonly string[]): boolean {\n if (isNodeModulesDir(name)) return true;\n return excludePaths.indexOf(name) >= 0;\n}\n\nfunction findProjectRoot(filePath: string, workspaceRoot: string): string | null {\n let dir = path.dirname(filePath);\n while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {\n if (fs.existsSync(path.join(dir, 'project.json'))) return dir;\n dir = path.dirname(dir);\n }\n return null;\n}\n\nconst fileLocationRule: FileRule = {\n name: 'file-location',\n description: 'Every .ts file must belong to a project\\'s src/ directory.',\n scope: 'file',\n files: ['**/*.ts', '**/*.tsx'],\n defaultOptions: {\n excludePaths: DEFAULT_EXCLUDE_PATHS,\n allowedRootFiles: DEFAULT_ALLOWED_ROOT_FILES,\n },\n fixHint: [\n 'Move the file into an existing project\\'s src/ directory, or create a new project with project.json that owns the directory.',\n 'Add the dir to file-location.excludePaths in webpieces.ai-hooks.json',\n ],\n\n check(ctx: FileContext): readonly Violation[] {\n if (ctx.tool !== 'Write') return [];\n\n const excludePaths = Array.isArray(ctx.options['excludePaths'])\n ? ctx.options['excludePaths'] as string[]\n : DEFAULT_EXCLUDE_PATHS;\n const allowedRootFiles = Array.isArray(ctx.options['allowedRootFiles'])\n ? ctx.options['allowedRootFiles'] as string[]\n : DEFAULT_ALLOWED_ROOT_FILES;\n\n const relParts = ctx.relativePath.split(path.sep);\n const topDir = relParts[0];\n\n if (topDir && shouldSkipDir(topDir, excludePaths)) return [];\n if (relParts.length === 1 && allowedRootFiles.indexOf(relParts[0]) >= 0) return [];\n\n const projectRoot = findProjectRoot(ctx.filePath, ctx.workspaceRoot);\n\n if (!projectRoot) {\n return [new V(\n 1,\n ctx.relativePath,\n 'File is not inside any Nx project. Move it into a project\\'s src/ directory.',\n )];\n }\n\n const relToProject = path.relative(projectRoot, ctx.filePath);\n const fileName = path.basename(ctx.filePath);\n if (fileName === 'jest.config.ts') return [];\n if (!relToProject.startsWith('src' + path.sep) && relToProject !== 'src') {\n const projectName = path.relative(ctx.workspaceRoot, projectRoot);\n return [new V(\n 1,\n ctx.relativePath,\n `File is inside project \\`${projectName}\\` but outside its src/ directory. Move it into src/.`,\n )];\n }\n\n return [];\n },\n};\n\nexport default fileLocationRule;\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const builtInRuleNames: readonly string[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.builtInRuleNames = void 0;
|
|
4
|
+
exports.builtInRuleNames = [
|
|
5
|
+
'no-any-unknown',
|
|
6
|
+
'max-file-lines',
|
|
7
|
+
'file-location',
|
|
8
|
+
'no-destructure',
|
|
9
|
+
'require-return-type',
|
|
10
|
+
'no-unmanaged-exceptions',
|
|
11
|
+
'catch-error-pattern',
|
|
12
|
+
];
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hooks/src/core/rules/index.ts"],"names":[],"mappings":";;;AAAa,QAAA,gBAAgB,GAAsB;IAC/C,gBAAgB;IAChB,gBAAgB;IAChB,eAAe;IACf,gBAAgB;IAChB,qBAAqB;IACrB,yBAAyB;IACzB,qBAAqB;CACxB,CAAC","sourcesContent":["export const builtInRuleNames: readonly string[] = [\n 'no-any-unknown',\n 'max-file-lines',\n 'file-location',\n 'no-destructure',\n 'require-return-type',\n 'no-unmanaged-exceptions',\n 'catch-error-pattern',\n];\n"]}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
5
|
+
const path = tslib_1.__importStar(require("path"));
|
|
6
|
+
const types_1 = require("../types");
|
|
7
|
+
const DEFAULT_LIMIT = 900;
|
|
8
|
+
const INSTRUCT_DIR = '.webpieces/instruct-ai';
|
|
9
|
+
const INSTRUCT_FILE = 'webpieces.filesize.md';
|
|
10
|
+
const maxFileLinesRule = {
|
|
11
|
+
name: 'max-file-lines',
|
|
12
|
+
description: 'Cap file length at a configured line limit.',
|
|
13
|
+
scope: 'file',
|
|
14
|
+
files: ['**/*.ts', '**/*.tsx'],
|
|
15
|
+
defaultOptions: { limit: DEFAULT_LIMIT },
|
|
16
|
+
fixHint: [
|
|
17
|
+
'READ .webpieces/instruct-ai/webpieces.filesize.md for step-by-step refactoring guidance.',
|
|
18
|
+
'// eslint-disable-next-line @webpieces/max-file-lines (also suppresses the eslint rule)',
|
|
19
|
+
],
|
|
20
|
+
check(ctx) {
|
|
21
|
+
const limit = typeof ctx.options['limit'] === 'number'
|
|
22
|
+
? ctx.options['limit']
|
|
23
|
+
: DEFAULT_LIMIT;
|
|
24
|
+
if (ctx.projectedFileLines <= limit)
|
|
25
|
+
return [];
|
|
26
|
+
writeInstructionFile(ctx.workspaceRoot);
|
|
27
|
+
return [new types_1.Violation(1, `(projected ${String(ctx.projectedFileLines)} lines)`, `File will be ${String(ctx.projectedFileLines)} lines, exceeding the ${String(limit)}-line limit. See .webpieces/instruct-ai/webpieces.filesize.md for detailed refactoring instructions.`)];
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
function writeInstructionFile(workspaceRoot) {
|
|
31
|
+
const dir = path.join(workspaceRoot, INSTRUCT_DIR);
|
|
32
|
+
const filePath = path.join(dir, INSTRUCT_FILE);
|
|
33
|
+
if (fs.existsSync(filePath))
|
|
34
|
+
return;
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
fs.writeFileSync(filePath, FILESIZE_DOC_CONTENT);
|
|
37
|
+
}
|
|
38
|
+
// eslint-disable-next-line @webpieces/max-file-lines
|
|
39
|
+
const FILESIZE_DOC_CONTENT = `# AI Agent Instructions: File Too Long
|
|
40
|
+
|
|
41
|
+
**READ THIS FILE to fix files that are too long**
|
|
42
|
+
|
|
43
|
+
## Core Principle
|
|
44
|
+
|
|
45
|
+
With **stateless systems + dependency injection, refactor is trivial**.
|
|
46
|
+
Pick a method or a few and move to new class XXXXX, then inject XXXXX
|
|
47
|
+
into all users of those methods via the constructor.
|
|
48
|
+
Delete those methods from original class.
|
|
49
|
+
|
|
50
|
+
**99% of files can be less than the configured max lines of code.**
|
|
51
|
+
|
|
52
|
+
Files should contain a SINGLE COHESIVE UNIT.
|
|
53
|
+
- One class per file (Java convention)
|
|
54
|
+
- If class is too large, extract child responsibilities
|
|
55
|
+
- Use dependency injection to compose functionality
|
|
56
|
+
|
|
57
|
+
## Command: Reduce File Size
|
|
58
|
+
|
|
59
|
+
### Step 1: Check for Multiple Classes
|
|
60
|
+
If the file contains multiple classes, **SEPARATE each class into its own file**.
|
|
61
|
+
|
|
62
|
+
### Step 2: Extract Child Responsibilities (if single class is too large)
|
|
63
|
+
|
|
64
|
+
#### Pattern: Create New Service Class with Dependency Injection
|
|
65
|
+
|
|
66
|
+
\`\`\`typescript
|
|
67
|
+
// BAD: UserController.ts (800 lines, single class)
|
|
68
|
+
@provideSingleton()
|
|
69
|
+
@Controller()
|
|
70
|
+
export class UserController {
|
|
71
|
+
// 200 lines: CRUD operations
|
|
72
|
+
// 300 lines: validation logic
|
|
73
|
+
// 200 lines: notification logic
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// GOOD: Extract validation service
|
|
77
|
+
// 1. Create UserValidationService.ts
|
|
78
|
+
@provideSingleton()
|
|
79
|
+
export class UserValidationService {
|
|
80
|
+
validateUserData(data: UserData): ValidationResult { /* ... */ }
|
|
81
|
+
validateEmail(email: string): boolean { /* ... */ }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 2. Inject into UserController.ts
|
|
85
|
+
@provideSingleton()
|
|
86
|
+
@Controller()
|
|
87
|
+
export class UserController {
|
|
88
|
+
constructor(
|
|
89
|
+
@inject(TYPES.UserValidationService)
|
|
90
|
+
private validator: UserValidationService
|
|
91
|
+
) {}
|
|
92
|
+
}
|
|
93
|
+
\`\`\`
|
|
94
|
+
|
|
95
|
+
## AI Agent Action Steps
|
|
96
|
+
|
|
97
|
+
1. **ANALYZE** the file structure:
|
|
98
|
+
- Count classes (if >1, separate immediately)
|
|
99
|
+
- Identify logical responsibilities within single class
|
|
100
|
+
|
|
101
|
+
2. **IDENTIFY** "child code" to extract:
|
|
102
|
+
- Validation logic -> ValidationService
|
|
103
|
+
- Notification logic -> NotificationService
|
|
104
|
+
- Data transformation -> TransformerService
|
|
105
|
+
- External API calls -> ApiService
|
|
106
|
+
- Business rules -> RulesEngine
|
|
107
|
+
|
|
108
|
+
3. **CREATE** new service file(s):
|
|
109
|
+
- Add \`@provideSingleton()\` decorator
|
|
110
|
+
- Move child methods to new class
|
|
111
|
+
|
|
112
|
+
4. **UPDATE** dependency injection:
|
|
113
|
+
- Inject new service into original class constructor
|
|
114
|
+
- Replace direct method calls with \`this.serviceName.method()\`
|
|
115
|
+
|
|
116
|
+
5. **VERIFY** file sizes:
|
|
117
|
+
- Original file should now be under the limit
|
|
118
|
+
- Each extracted file should be under the limit
|
|
119
|
+
|
|
120
|
+
## Escape Hatch
|
|
121
|
+
|
|
122
|
+
If refactoring is genuinely not feasible, add a disable comment:
|
|
123
|
+
|
|
124
|
+
\`\`\`typescript
|
|
125
|
+
// eslint-disable-next-line @webpieces/max-file-lines
|
|
126
|
+
\`\`\`
|
|
127
|
+
|
|
128
|
+
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.
|
|
129
|
+
`;
|
|
130
|
+
exports.default = maxFileLinesRule;
|
|
131
|
+
//# sourceMappingURL=max-file-lines.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"max-file-lines.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hooks/src/core/rules/max-file-lines.ts"],"names":[],"mappings":";;;AAAA,+CAAyB;AACzB,mDAA6B;AAG7B,oCAA0C;AAE1C,MAAM,aAAa,GAAG,GAAG,CAAC;AAC1B,MAAM,YAAY,GAAG,wBAAwB,CAAC;AAC9C,MAAM,aAAa,GAAG,uBAAuB,CAAC;AAE9C,MAAM,gBAAgB,GAAa;IAC/B,IAAI,EAAE,gBAAgB;IACtB,WAAW,EAAE,6CAA6C;IAC1D,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;IAC9B,cAAc,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE;IACxC,OAAO,EAAE;QACL,0FAA0F;QAC1F,0FAA0F;KAC7F;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,KAAK,GAAG,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,QAAQ;YAClD,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAW;YAChC,CAAC,CAAC,aAAa,CAAC;QACpB,IAAI,GAAG,CAAC,kBAAkB,IAAI,KAAK;YAAE,OAAO,EAAE,CAAC;QAC/C,oBAAoB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACxC,OAAO,CAAC,IAAI,iBAAC,CACT,CAAC,EACD,cAAc,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,SAAS,EACrD,gBAAgB,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,yBAAyB,MAAM,CAAC,KAAK,CAAC,sGAAsG,CAC7L,CAAC,CAAC;IACP,CAAC;CACJ,CAAC;AAEF,SAAS,oBAAoB,CAAC,aAAqB;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;IAC/C,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO;IACpC,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvC,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,oBAAoB,CAAC,CAAC;AACrD,CAAC;AAED,qDAAqD;AACrD,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0F5B,CAAC;AAEF,kBAAe,gBAAgB,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport type { FileRule, FileContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst DEFAULT_LIMIT = 900;\nconst INSTRUCT_DIR = '.webpieces/instruct-ai';\nconst INSTRUCT_FILE = 'webpieces.filesize.md';\n\nconst maxFileLinesRule: FileRule = {\n name: 'max-file-lines',\n description: 'Cap file length at a configured line limit.',\n scope: 'file',\n files: ['**/*.ts', '**/*.tsx'],\n defaultOptions: { limit: DEFAULT_LIMIT },\n fixHint: [\n 'READ .webpieces/instruct-ai/webpieces.filesize.md for step-by-step refactoring guidance.',\n '// eslint-disable-next-line @webpieces/max-file-lines (also suppresses the eslint rule)',\n ],\n\n check(ctx: FileContext): readonly Violation[] {\n const limit = typeof ctx.options['limit'] === 'number'\n ? ctx.options['limit'] as number\n : DEFAULT_LIMIT;\n if (ctx.projectedFileLines <= limit) return [];\n writeInstructionFile(ctx.workspaceRoot);\n return [new V(\n 1,\n `(projected ${String(ctx.projectedFileLines)} lines)`,\n `File will be ${String(ctx.projectedFileLines)} lines, exceeding the ${String(limit)}-line limit. See .webpieces/instruct-ai/webpieces.filesize.md for detailed refactoring instructions.`,\n )];\n },\n};\n\nfunction writeInstructionFile(workspaceRoot: string): void {\n const dir = path.join(workspaceRoot, INSTRUCT_DIR);\n const filePath = path.join(dir, INSTRUCT_FILE);\n if (fs.existsSync(filePath)) return;\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(filePath, FILESIZE_DOC_CONTENT);\n}\n\n// eslint-disable-next-line @webpieces/max-file-lines\nconst FILESIZE_DOC_CONTENT = `# AI Agent Instructions: File Too Long\n\n**READ THIS FILE to fix files that are too long**\n\n## Core Principle\n\nWith **stateless systems + dependency injection, refactor is trivial**.\nPick a method or a few and move to new class XXXXX, then inject XXXXX\ninto all users of those methods via the constructor.\nDelete those methods from original class.\n\n**99% of files can be less than the configured max lines of code.**\n\nFiles should contain a SINGLE COHESIVE UNIT.\n- One class per file (Java convention)\n- If class is too large, extract child responsibilities\n- Use dependency injection to compose functionality\n\n## Command: Reduce File Size\n\n### Step 1: Check for Multiple Classes\nIf the file contains multiple classes, **SEPARATE each class into its own file**.\n\n### Step 2: Extract Child Responsibilities (if single class is too large)\n\n#### Pattern: Create New Service Class with Dependency Injection\n\n\\`\\`\\`typescript\n// BAD: UserController.ts (800 lines, single class)\n@provideSingleton()\n@Controller()\nexport class UserController {\n // 200 lines: CRUD operations\n // 300 lines: validation logic\n // 200 lines: notification logic\n}\n\n// GOOD: Extract validation service\n// 1. Create UserValidationService.ts\n@provideSingleton()\nexport class UserValidationService {\n validateUserData(data: UserData): ValidationResult { /* ... */ }\n validateEmail(email: string): boolean { /* ... */ }\n}\n\n// 2. Inject into UserController.ts\n@provideSingleton()\n@Controller()\nexport class UserController {\n constructor(\n @inject(TYPES.UserValidationService)\n private validator: UserValidationService\n ) {}\n}\n\\`\\`\\`\n\n## AI Agent Action Steps\n\n1. **ANALYZE** the file structure:\n - Count classes (if >1, separate immediately)\n - Identify logical responsibilities within single class\n\n2. **IDENTIFY** \"child code\" to extract:\n - Validation logic -> ValidationService\n - Notification logic -> NotificationService\n - Data transformation -> TransformerService\n - External API calls -> ApiService\n - Business rules -> RulesEngine\n\n3. **CREATE** new service file(s):\n - Add \\`@provideSingleton()\\` decorator\n - Move child methods to new class\n\n4. **UPDATE** dependency injection:\n - Inject new service into original class constructor\n - Replace direct method calls with \\`this.serviceName.method()\\`\n\n5. **VERIFY** file sizes:\n - Original file should now be under the limit\n - Each extracted file should be under the limit\n\n## Escape Hatch\n\nIf refactoring is genuinely not feasible, add a disable comment:\n\n\\`\\`\\`typescript\n// eslint-disable-next-line @webpieces/max-file-lines\n\\`\\`\\`\n\nRemember: Find the \"child code\" and pull it down into a new class. Once moved, the code's purpose becomes clear, making it easy to rename to a logical name.\n`;\n\nexport default maxFileLinesRule;\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const types_1 = require("../types");
|
|
4
|
+
const ANY_PATTERN = /(?::\s*any\b|\bas\s+any\b|<any>|any\[\]|Array<any>|Promise<any>|Map<[^,<>]+,\s*any\s*>|Record<[^,<>]+,\s*any\s*>|Set<any>)/;
|
|
5
|
+
const noAnyRule = {
|
|
6
|
+
name: 'no-any-unknown',
|
|
7
|
+
description: 'Disallow the `any` keyword. Use concrete types or interfaces.',
|
|
8
|
+
scope: 'edit',
|
|
9
|
+
files: ['**/*.ts', '**/*.tsx'],
|
|
10
|
+
defaultOptions: {},
|
|
11
|
+
fixHint: [
|
|
12
|
+
'Prefer: interface MyData { ... } or class MyData { ... }',
|
|
13
|
+
'// webpieces-disable no-any-unknown -- <one-line reason>',
|
|
14
|
+
],
|
|
15
|
+
check(ctx) {
|
|
16
|
+
const violations = [];
|
|
17
|
+
for (let i = 0; i < ctx.strippedLines.length; i += 1) {
|
|
18
|
+
const stripped = ctx.strippedLines[i];
|
|
19
|
+
if (!ANY_PATTERN.test(stripped))
|
|
20
|
+
continue;
|
|
21
|
+
const lineNum = i + 1;
|
|
22
|
+
if (ctx.isLineDisabled(lineNum, 'no-any-unknown'))
|
|
23
|
+
continue;
|
|
24
|
+
violations.push(new types_1.Violation(lineNum, ctx.lines[i].trim(), '`any` erases type information. Use a concrete type, an interface, or `unknown` with type guards.'));
|
|
25
|
+
}
|
|
26
|
+
return violations;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
exports.default = noAnyRule;
|
|
30
|
+
//# sourceMappingURL=no-any-unknown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-any-unknown.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hooks/src/core/rules/no-any-unknown.ts"],"names":[],"mappings":";;AACA,oCAA0C;AAE1C,MAAM,WAAW,GACb,4HAA4H,CAAC;AAEjI,MAAM,SAAS,GAAa;IACxB,IAAI,EAAE,gBAAgB;IACtB,WAAW,EAAE,+DAA+D;IAC5E,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;IAC9B,cAAc,EAAE,EAAE;IAClB,OAAO,EAAE;QACL,8DAA8D;QAC9D,0DAA0D;KAC7D;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,UAAU,GAAQ,EAAE,CAAC;QAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACnD,MAAM,QAAQ,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;YACtC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAC1C,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;YACtB,IAAI,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,gBAAgB,CAAC;gBAAE,SAAS;YAC5D,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CACjB,OAAO,EACP,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EACnB,kGAAkG,CACrG,CAAC,CAAC;QACP,CAAC;QACD,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ,CAAC;AAEF,kBAAe,SAAS,CAAC","sourcesContent":["import type { EditRule, EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst ANY_PATTERN =\n /(?::\\s*any\\b|\\bas\\s+any\\b|<any>|any\\[\\]|Array<any>|Promise<any>|Map<[^,<>]+,\\s*any\\s*>|Record<[^,<>]+,\\s*any\\s*>|Set<any>)/;\n\nconst noAnyRule: EditRule = {\n name: 'no-any-unknown',\n description: 'Disallow the `any` keyword. Use concrete types or interfaces.',\n scope: 'edit',\n files: ['**/*.ts', '**/*.tsx'],\n defaultOptions: {},\n fixHint: [\n 'Prefer: interface MyData { ... } or class MyData { ... }',\n '// webpieces-disable no-any-unknown -- <one-line reason>',\n ],\n\n check(ctx: EditContext): readonly Violation[] {\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const stripped = ctx.strippedLines[i];\n if (!ANY_PATTERN.test(stripped)) continue;\n const lineNum = i + 1;\n if (ctx.isLineDisabled(lineNum, 'no-any-unknown')) continue;\n violations.push(new V(\n lineNum,\n ctx.lines[i].trim(),\n '`any` erases type information. Use a concrete type, an interface, or `unknown` with type guards.',\n ));\n }\n return violations;\n },\n};\n\nexport default noAnyRule;\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const types_1 = require("../types");
|
|
4
|
+
const VARIABLE_DESTRUCTURE = /\b(?:const|let|var)\s*\{/;
|
|
5
|
+
const noDestructureRule = {
|
|
6
|
+
name: 'no-destructure',
|
|
7
|
+
description: 'Disallow destructuring patterns. Assign the whole result and pass it around or access properties explicitly.',
|
|
8
|
+
scope: 'edit',
|
|
9
|
+
files: ['**/*.ts', '**/*.tsx'],
|
|
10
|
+
defaultOptions: { allowTopLevel: true },
|
|
11
|
+
fixHint: [
|
|
12
|
+
'Instead of: const { x, y } = methodCall(); prefer const obj = methodCall(); then pass obj to other methods or use obj.x',
|
|
13
|
+
'// webpieces-disable no-destructure -- <reason>',
|
|
14
|
+
],
|
|
15
|
+
check(ctx) {
|
|
16
|
+
const violations = [];
|
|
17
|
+
for (let i = 0; i < ctx.strippedLines.length; i += 1) {
|
|
18
|
+
const stripped = ctx.strippedLines[i];
|
|
19
|
+
if (!VARIABLE_DESTRUCTURE.test(stripped))
|
|
20
|
+
continue;
|
|
21
|
+
const lineNum = i + 1;
|
|
22
|
+
if (ctx.isLineDisabled(lineNum, 'no-destructure'))
|
|
23
|
+
continue;
|
|
24
|
+
violations.push(new types_1.Violation(lineNum, ctx.lines[i].trim(), 'Destructuring pattern. Assign the whole result instead: const obj = methodCall(); then pass obj around or use obj.x'));
|
|
25
|
+
}
|
|
26
|
+
return violations;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
exports.default = noDestructureRule;
|
|
30
|
+
//# sourceMappingURL=no-destructure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-destructure.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hooks/src/core/rules/no-destructure.ts"],"names":[],"mappings":";;AACA,oCAA0C;AAE1C,MAAM,oBAAoB,GAAG,0BAA0B,CAAC;AAExD,MAAM,iBAAiB,GAAa;IAChC,IAAI,EAAE,gBAAgB;IACtB,WAAW,EAAE,8GAA8G;IAC3H,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;IAC9B,cAAc,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE;IACvC,OAAO,EAAE;QACL,yHAAyH;QACzH,iDAAiD;KACpD;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,UAAU,GAAQ,EAAE,CAAC;QAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACnD,MAAM,QAAQ,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;YACtC,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACnD,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;YACtB,IAAI,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,gBAAgB,CAAC;gBAAE,SAAS;YAC5D,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CACjB,OAAO,EACP,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EACnB,qHAAqH,CACxH,CAAC,CAAC;QACP,CAAC;QACD,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ,CAAC;AAEF,kBAAe,iBAAiB,CAAC","sourcesContent":["import type { EditRule, EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst VARIABLE_DESTRUCTURE = /\\b(?:const|let|var)\\s*\\{/;\n\nconst noDestructureRule: EditRule = {\n name: 'no-destructure',\n description: 'Disallow destructuring patterns. Assign the whole result and pass it around or access properties explicitly.',\n scope: 'edit',\n files: ['**/*.ts', '**/*.tsx'],\n defaultOptions: { allowTopLevel: true },\n fixHint: [\n 'Instead of: const { x, y } = methodCall(); prefer const obj = methodCall(); then pass obj to other methods or use obj.x',\n '// webpieces-disable no-destructure -- <reason>',\n ],\n\n check(ctx: EditContext): readonly Violation[] {\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const stripped = ctx.strippedLines[i];\n if (!VARIABLE_DESTRUCTURE.test(stripped)) continue;\n const lineNum = i + 1;\n if (ctx.isLineDisabled(lineNum, 'no-destructure')) continue;\n violations.push(new V(\n lineNum,\n ctx.lines[i].trim(),\n 'Destructuring pattern. Assign the whole result instead: const obj = methodCall(); then pass obj around or use obj.x',\n ));\n }\n return violations;\n },\n};\n\nexport default noDestructureRule;\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const types_1 = require("../types");
|
|
4
|
+
const TRY_PATTERN = /\btry\s*\{/;
|
|
5
|
+
// Both webpieces-disable and the existing ESLint directive suppress this rule
|
|
6
|
+
const DISABLE_PATTERN = /@webpieces\/no-unmanaged-exceptions|webpieces-disable\s+(?:[\w-]+,\s*)*no-unmanaged-exceptions/;
|
|
7
|
+
const noUnmanagedExceptionsRule = {
|
|
8
|
+
name: 'no-unmanaged-exceptions',
|
|
9
|
+
description: 'try/catch is generally not allowed. Only allowed in chokepoints (filter, globalErrorHandler) or other rare locations.',
|
|
10
|
+
scope: 'edit',
|
|
11
|
+
files: ['**/*.ts', '**/*.tsx'],
|
|
12
|
+
defaultOptions: {},
|
|
13
|
+
fixHint: [
|
|
14
|
+
'Exceptions should bubble to a chokepoint (filter in node.js, globalErrorHandler in Angular). Most code should NOT catch exceptions.',
|
|
15
|
+
'// webpieces-disable no-unmanaged-exceptions -- <reason>',
|
|
16
|
+
'When try/catch IS used (after disabling), the catch block MUST use: catch (err: unknown) { const error = toError(err); ... } or //const error = toError(err); to explicitly ignore.',
|
|
17
|
+
],
|
|
18
|
+
check(ctx) {
|
|
19
|
+
const violations = [];
|
|
20
|
+
for (let i = 0; i < ctx.strippedLines.length; i += 1) {
|
|
21
|
+
const stripped = ctx.strippedLines[i];
|
|
22
|
+
if (!TRY_PATTERN.test(stripped))
|
|
23
|
+
continue;
|
|
24
|
+
const lineNum = i + 1;
|
|
25
|
+
if (ctx.isLineDisabled(lineNum, 'no-unmanaged-exceptions'))
|
|
26
|
+
continue;
|
|
27
|
+
if (hasPrecedingDisable(ctx.lines, i))
|
|
28
|
+
continue;
|
|
29
|
+
violations.push(new types_1.Violation(lineNum, ctx.lines[i].trim(), 'try/catch is generally not allowed. It is only allowed in chokepoints (filter, globalErrorHandler) or other rare locations.'));
|
|
30
|
+
}
|
|
31
|
+
return violations;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
function hasPrecedingDisable(lines, idx) {
|
|
35
|
+
if (idx === 0)
|
|
36
|
+
return false;
|
|
37
|
+
const prevLine = lines[idx - 1];
|
|
38
|
+
return DISABLE_PATTERN.test(prevLine);
|
|
39
|
+
}
|
|
40
|
+
exports.default = noUnmanagedExceptionsRule;
|
|
41
|
+
//# sourceMappingURL=no-unmanaged-exceptions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-unmanaged-exceptions.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hooks/src/core/rules/no-unmanaged-exceptions.ts"],"names":[],"mappings":";;AACA,oCAA0C;AAE1C,MAAM,WAAW,GAAG,YAAY,CAAC;AAEjC,8EAA8E;AAC9E,MAAM,eAAe,GAAG,gGAAgG,CAAC;AAEzH,MAAM,yBAAyB,GAAa;IACxC,IAAI,EAAE,yBAAyB;IAC/B,WAAW,EAAE,uHAAuH;IACpI,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;IAC9B,cAAc,EAAE,EAAE;IAClB,OAAO,EAAE;QACL,qIAAqI;QACrI,0DAA0D;QAC1D,qLAAqL;KACxL;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,UAAU,GAAQ,EAAE,CAAC;QAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACnD,MAAM,QAAQ,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;YACtC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAC1C,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;YACtB,IAAI,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,yBAAyB,CAAC;gBAAE,SAAS;YACrE,IAAI,mBAAmB,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;gBAAE,SAAS;YAChD,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CACjB,OAAO,EACP,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EACnB,6HAA6H,CAChI,CAAC,CAAC;QACP,CAAC;QACD,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ,CAAC;AAEF,SAAS,mBAAmB,CAAC,KAAwB,EAAE,GAAW;IAC9D,IAAI,GAAG,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5B,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IAChC,OAAO,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAED,kBAAe,yBAAyB,CAAC","sourcesContent":["import type { EditRule, EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst TRY_PATTERN = /\\btry\\s*\\{/;\n\n// Both webpieces-disable and the existing ESLint directive suppress this rule\nconst DISABLE_PATTERN = /@webpieces\\/no-unmanaged-exceptions|webpieces-disable\\s+(?:[\\w-]+,\\s*)*no-unmanaged-exceptions/;\n\nconst noUnmanagedExceptionsRule: EditRule = {\n name: 'no-unmanaged-exceptions',\n description: 'try/catch is generally not allowed. Only allowed in chokepoints (filter, globalErrorHandler) or other rare locations.',\n scope: 'edit',\n files: ['**/*.ts', '**/*.tsx'],\n defaultOptions: {},\n fixHint: [\n 'Exceptions should bubble to a chokepoint (filter in node.js, globalErrorHandler in Angular). Most code should NOT catch exceptions.',\n '// webpieces-disable no-unmanaged-exceptions -- <reason>',\n 'When try/catch IS used (after disabling), the catch block MUST use: catch (err: unknown) { const error = toError(err); ... } or //const error = toError(err); to explicitly ignore.',\n ],\n\n check(ctx: EditContext): readonly Violation[] {\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const stripped = ctx.strippedLines[i];\n if (!TRY_PATTERN.test(stripped)) continue;\n const lineNum = i + 1;\n if (ctx.isLineDisabled(lineNum, 'no-unmanaged-exceptions')) continue;\n if (hasPrecedingDisable(ctx.lines, i)) continue;\n violations.push(new V(\n lineNum,\n ctx.lines[i].trim(),\n 'try/catch is generally not allowed. It is only allowed in chokepoints (filter, globalErrorHandler) or other rare locations.',\n ));\n }\n return violations;\n },\n};\n\nfunction hasPrecedingDisable(lines: readonly string[], idx: number): boolean {\n if (idx === 0) return false;\n const prevLine = lines[idx - 1];\n return DISABLE_PATTERN.test(prevLine);\n}\n\nexport default noUnmanagedExceptionsRule;\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const types_1 = require("../types");
|
|
4
|
+
// Matches function/method signatures that don't have `: ReturnType` before the `{` body opener.
|
|
5
|
+
// Pattern: function name(<params>) { — missing `: Type` between `)` and `{`
|
|
6
|
+
const FUNC_DECL_MISSING = /\bfunction\s+\w+\s*(?:<[^>]*>)?\s*\([^)]*\)\s*\{/;
|
|
7
|
+
// Matches class method signatures: indented, optional async, name(<params>) {
|
|
8
|
+
const METHOD_MISSING = /^\s{2,}(?:async\s+)?\w+\s*(?:<[^>]*>)?\s*\([^)]*\)\s*\{/;
|
|
9
|
+
// Arrow function: const name = (async)? (<params>) => — missing `: Type` before `=>`
|
|
10
|
+
const ARROW_MISSING = /\bconst\s+\w+\s*=\s*(?:async\s+)?(?:<[^>]*>)?\s*\([^)]*\)\s*=>/;
|
|
11
|
+
// Lines that have ): ReturnType before { or => — these are COMPLIANT
|
|
12
|
+
const HAS_RETURN_TYPE = /\)\s*:\s*\S/;
|
|
13
|
+
// Skip constructors, getters, setters, and control flow keywords
|
|
14
|
+
const SKIP_PATTERN = /\b(?:constructor|get\s+\w+|set\s+\w+|if|else|while|for|switch|catch|return)\s*\(/;
|
|
15
|
+
const requireReturnTypeRule = {
|
|
16
|
+
name: 'require-return-type',
|
|
17
|
+
description: 'Every function and method must declare its return type.',
|
|
18
|
+
scope: 'edit',
|
|
19
|
+
files: ['**/*.ts', '**/*.tsx'],
|
|
20
|
+
defaultOptions: {},
|
|
21
|
+
fixHint: [
|
|
22
|
+
'Add return type: function foo(x: T): ReturnType { ... }',
|
|
23
|
+
'// webpieces-disable require-return-type -- <reason>',
|
|
24
|
+
],
|
|
25
|
+
check(ctx) {
|
|
26
|
+
const violations = [];
|
|
27
|
+
for (let i = 0; i < ctx.strippedLines.length; i += 1) {
|
|
28
|
+
const stripped = ctx.strippedLines[i];
|
|
29
|
+
if (!isMissingReturnType(stripped))
|
|
30
|
+
continue;
|
|
31
|
+
const lineNum = i + 1;
|
|
32
|
+
if (ctx.isLineDisabled(lineNum, 'require-return-type'))
|
|
33
|
+
continue;
|
|
34
|
+
violations.push(new types_1.Violation(lineNum, ctx.lines[i].trim(), 'Missing return type annotation.'));
|
|
35
|
+
}
|
|
36
|
+
return violations;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
function isMissingReturnType(line) {
|
|
40
|
+
if (SKIP_PATTERN.test(line))
|
|
41
|
+
return false;
|
|
42
|
+
const isFuncLike = FUNC_DECL_MISSING.test(line) ||
|
|
43
|
+
METHOD_MISSING.test(line) ||
|
|
44
|
+
ARROW_MISSING.test(line);
|
|
45
|
+
if (!isFuncLike)
|
|
46
|
+
return false;
|
|
47
|
+
if (HAS_RETURN_TYPE.test(line))
|
|
48
|
+
return false;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
exports.default = requireReturnTypeRule;
|
|
52
|
+
//# sourceMappingURL=require-return-type.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-return-type.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hooks/src/core/rules/require-return-type.ts"],"names":[],"mappings":";;AACA,oCAA0C;AAE1C,gGAAgG;AAChG,4EAA4E;AAC5E,MAAM,iBAAiB,GAAG,kDAAkD,CAAC;AAE7E,8EAA8E;AAC9E,MAAM,cAAc,GAAG,yDAAyD,CAAC;AAEjF,qFAAqF;AACrF,MAAM,aAAa,GAAG,gEAAgE,CAAC;AAEvF,qEAAqE;AACrE,MAAM,eAAe,GAAG,aAAa,CAAC;AAEtC,iEAAiE;AACjE,MAAM,YAAY,GAAG,kFAAkF,CAAC;AAExG,MAAM,qBAAqB,GAAa;IACpC,IAAI,EAAE,qBAAqB;IAC3B,WAAW,EAAE,yDAAyD;IACtE,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;IAC9B,cAAc,EAAE,EAAE;IAClB,OAAO,EAAE;QACL,yDAAyD;QACzD,sDAAsD;KACzD;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,UAAU,GAAQ,EAAE,CAAC;QAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACnD,MAAM,QAAQ,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;YACtC,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAC7C,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;YACtB,IAAI,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,qBAAqB,CAAC;gBAAE,SAAS;YACjE,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CACjB,OAAO,EACP,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EACnB,iCAAiC,CACpC,CAAC,CAAC;QACP,CAAC;QACD,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ,CAAC;AAEF,SAAS,mBAAmB,CAAC,IAAY;IACrC,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1C,MAAM,UAAU,GACZ,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;QAC5B,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;QACzB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC;IAC9B,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7C,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,kBAAe,qBAAqB,CAAC","sourcesContent":["import type { EditRule, EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\n// Matches function/method signatures that don't have `: ReturnType` before the `{` body opener.\n// Pattern: function name(<params>) { — missing `: Type` between `)` and `{`\nconst FUNC_DECL_MISSING = /\\bfunction\\s+\\w+\\s*(?:<[^>]*>)?\\s*\\([^)]*\\)\\s*\\{/;\n\n// Matches class method signatures: indented, optional async, name(<params>) {\nconst METHOD_MISSING = /^\\s{2,}(?:async\\s+)?\\w+\\s*(?:<[^>]*>)?\\s*\\([^)]*\\)\\s*\\{/;\n\n// Arrow function: const name = (async)? (<params>) => — missing `: Type` before `=>`\nconst ARROW_MISSING = /\\bconst\\s+\\w+\\s*=\\s*(?:async\\s+)?(?:<[^>]*>)?\\s*\\([^)]*\\)\\s*=>/;\n\n// Lines that have ): ReturnType before { or => — these are COMPLIANT\nconst HAS_RETURN_TYPE = /\\)\\s*:\\s*\\S/;\n\n// Skip constructors, getters, setters, and control flow keywords\nconst SKIP_PATTERN = /\\b(?:constructor|get\\s+\\w+|set\\s+\\w+|if|else|while|for|switch|catch|return)\\s*\\(/;\n\nconst requireReturnTypeRule: EditRule = {\n name: 'require-return-type',\n description: 'Every function and method must declare its return type.',\n scope: 'edit',\n files: ['**/*.ts', '**/*.tsx'],\n defaultOptions: {},\n fixHint: [\n 'Add return type: function foo(x: T): ReturnType { ... }',\n '// webpieces-disable require-return-type -- <reason>',\n ],\n\n check(ctx: EditContext): readonly Violation[] {\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const stripped = ctx.strippedLines[i];\n if (!isMissingReturnType(stripped)) continue;\n const lineNum = i + 1;\n if (ctx.isLineDisabled(lineNum, 'require-return-type')) continue;\n violations.push(new V(\n lineNum,\n ctx.lines[i].trim(),\n 'Missing return type annotation.',\n ));\n }\n return violations;\n },\n};\n\nfunction isMissingReturnType(line: string): boolean {\n if (SKIP_PATTERN.test(line)) return false;\n const isFuncLike =\n FUNC_DECL_MISSING.test(line) ||\n METHOD_MISSING.test(line) ||\n ARROW_MISSING.test(line);\n if (!isFuncLike) return false;\n if (HAS_RETURN_TYPE.test(line)) return false;\n return true;\n}\n\nexport default requireReturnTypeRule;\n"]}
|