@webpieces/ai-hook-rules 0.0.1 → 0.2.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -3
- package/src/adapters/claude-code-hook.d.ts +1 -0
- package/src/adapters/claude-code-hook.js +112 -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 +8 -0
- package/src/core/build-context.js +62 -0
- package/src/core/build-context.js.map +1 -0
- package/src/core/configs/default.d.ts +2 -0
- package/src/core/configs/{default.ts → default.js} +6 -3
- 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/instruct-ai-writer.d.ts +1 -0
- package/src/core/instruct-ai-writer.js +18 -0
- package/src/core/instruct-ai-writer.js.map +1 -0
- package/src/core/load-config.d.ts +1 -0
- package/src/core/load-config.js +10 -0
- package/src/core/load-config.js.map +1 -0
- package/src/core/load-rules.d.ts +3 -0
- package/src/core/{load-rules.ts → load-rules.js} +33 -32
- package/src/core/load-rules.js.map +1 -0
- package/src/core/rejection-log.d.ts +2 -0
- package/src/core/{rejection-log.ts → rejection-log.js} +34 -51
- package/src/core/rejection-log.js.map +1 -0
- package/src/core/report.d.ts +2 -0
- package/src/core/{report.ts → report.js} +7 -8
- 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.ts → catch-error-pattern.js} +25 -54
- 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.ts → file-location.js} +29 -43
- package/src/core/rules/file-location.js.map +1 -0
- package/src/core/rules/index.d.ts +1 -0
- package/src/core/rules/{index.ts → index.js} +5 -1
- 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.ts → max-file-lines.js} +17 -23
- 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.ts → no-destructure.js} +13 -17
- package/src/core/rules/no-destructure.js.map +1 -0
- package/src/core/rules/no-implicit-any.d.ts +3 -0
- package/src/core/rules/{no-implicit-any.ts → no-implicit-any.js} +32 -30
- package/src/core/rules/no-implicit-any.js.map +1 -0
- package/src/core/rules/no-shell-substitution.d.ts +3 -0
- package/src/core/rules/no-shell-substitution.js +54 -0
- package/src/core/rules/no-shell-substitution.js.map +1 -0
- package/src/core/rules/no-unmanaged-exceptions.d.ts +3 -0
- package/src/core/rules/{no-unmanaged-exceptions.ts → no-unmanaged-exceptions.js} +21 -24
- 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.ts → require-return-type.js} +21 -28
- package/src/core/rules/require-return-type.js.map +1 -0
- package/src/core/runner.d.ts +3 -0
- package/src/core/runner.js +181 -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.ts → to-error.js} +7 -4
- package/src/core/to-error.js.map +1 -0
- package/src/core/types.d.ts +93 -0
- package/src/core/types.js +93 -0
- package/src/core/types.js.map +1 -0
- package/src/index.d.ts +5 -0
- package/src/index.js +25 -0
- package/src/index.js.map +1 -0
- package/LICENSE +0 -373
- package/src/adapters/claude-code-hook.ts +0 -117
- package/src/adapters/openclaw-plugin.ts +0 -88
- package/src/core/__tests__/disable-directives.test.ts +0 -114
- package/src/core/__tests__/rules/file-location.test.ts +0 -90
- package/src/core/__tests__/rules/max-file-lines.test.ts +0 -53
- package/src/core/__tests__/rules/no-any.test.ts +0 -68
- package/src/core/__tests__/rules/no-destructure.test.ts +0 -50
- package/src/core/__tests__/rules/no-shell-substitution.test.ts +0 -118
- package/src/core/__tests__/rules/no-unmanaged-exceptions.test.ts +0 -54
- package/src/core/__tests__/rules/require-return-type.test.ts +0 -79
- package/src/core/__tests__/runner.test.ts +0 -288
- package/src/core/__tests__/strip-ts-noise.test.ts +0 -109
- package/src/core/build-context.ts +0 -96
- package/src/core/disable-directives.ts +0 -90
- package/src/core/instruct-ai-writer.ts +0 -15
- package/src/core/load-config.ts +0 -3
- package/src/core/rules/no-any-unknown.ts +0 -35
- package/src/core/rules/no-shell-substitution.ts +0 -71
- package/src/core/runner.ts +0 -205
- package/src/core/strip-ts-noise.ts +0 -103
- package/src/core/types.ts +0 -196
- package/src/index.ts +0 -14
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { run } from '../../runner';
|
|
2
|
-
import { NormalizedToolInput, NormalizedEdit } from '../../types';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import * as os from 'os';
|
|
5
|
-
import * as path from 'path';
|
|
6
|
-
|
|
7
|
-
function ws(): string {
|
|
8
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ret-type-'));
|
|
9
|
-
fs.writeFileSync(path.join(dir, 'webpieces.ai-hooks.json'), JSON.stringify({
|
|
10
|
-
rules: { 'no-any-unknown': { enabled: false }, 'max-file-lines': { enabled: false },
|
|
11
|
-
'file-location': { enabled: false }, 'no-destructure': { enabled: false },
|
|
12
|
-
'no-unmanaged-exceptions': { enabled: false } },
|
|
13
|
-
rulesDir: [],
|
|
14
|
-
}));
|
|
15
|
-
return dir;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('require-return-type rule', () => {
|
|
19
|
-
it('blocks function without return type', () => {
|
|
20
|
-
const w = ws();
|
|
21
|
-
const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [
|
|
22
|
-
new NormalizedEdit('', 'function foo(x: number) {\n return x;\n}'),
|
|
23
|
-
]), w);
|
|
24
|
-
expect(r).not.toBeNull();
|
|
25
|
-
expect(r!.report).toContain('require-return-type');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('allows function with return type', () => {
|
|
29
|
-
const w = ws();
|
|
30
|
-
const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [
|
|
31
|
-
new NormalizedEdit('', 'function foo(x: number): number {\n return x;\n}'),
|
|
32
|
-
]), w);
|
|
33
|
-
expect(r).toBeNull();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('blocks async method without return type', () => {
|
|
37
|
-
const w = ws();
|
|
38
|
-
const content = ' async fetchData(id: string) {\n return null;\n }';
|
|
39
|
-
const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', content)]), w);
|
|
40
|
-
expect(r).not.toBeNull();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('allows async method with return type', () => {
|
|
44
|
-
const w = ws();
|
|
45
|
-
const content = ' async fetchData(id: string): Promise<string> {\n return "";\n }';
|
|
46
|
-
const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', content)]), w);
|
|
47
|
-
expect(r).toBeNull();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('skips constructors', () => {
|
|
51
|
-
const w = ws();
|
|
52
|
-
const content = ' constructor(private x: number) {\n this.x = x;\n }';
|
|
53
|
-
const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', content)]), w);
|
|
54
|
-
expect(r).toBeNull();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('blocks arrow function without return type', () => {
|
|
58
|
-
const w = ws();
|
|
59
|
-
const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [
|
|
60
|
-
new NormalizedEdit('', 'const fn = (x: number) => x + 1;'),
|
|
61
|
-
]), w);
|
|
62
|
-
expect(r).not.toBeNull();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('allows arrow function with return type', () => {
|
|
66
|
-
const w = ws();
|
|
67
|
-
const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [
|
|
68
|
-
new NormalizedEdit('', 'const fn = (x: number): number => x + 1;'),
|
|
69
|
-
]), w);
|
|
70
|
-
expect(r).toBeNull();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('respects ai-hook-disable', () => {
|
|
74
|
-
const w = ws();
|
|
75
|
-
const content = '// ai-hook-disable require-return-type -- generated\nfunction foo(x: number) {\n return x;\n}';
|
|
76
|
-
const r = run('Write', new NormalizedToolInput(path.join(w, 'f.ts'), [new NormalizedEdit('', content)]), w);
|
|
77
|
-
expect(r).toBeNull();
|
|
78
|
-
});
|
|
79
|
-
});
|
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @webpieces/max-method-lines -- test describe blocks are inherently large */
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as os from 'os';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
|
|
6
|
-
import { run } from '../runner';
|
|
7
|
-
import { NormalizedToolInput, NormalizedEdit } from '../types';
|
|
8
|
-
|
|
9
|
-
function makeWorkspace(): string {
|
|
10
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), 'ai-hooks-runner-test-'));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function writeFile(p: string, content: string): void {
|
|
14
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
15
|
-
fs.writeFileSync(p, content);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('core runner', () => {
|
|
19
|
-
it('returns null when no config in tree', () => {
|
|
20
|
-
const ws = makeWorkspace();
|
|
21
|
-
const input = new NormalizedToolInput(
|
|
22
|
-
path.join(ws, 'foo.ts'),
|
|
23
|
-
[new NormalizedEdit('', 'const x: number = 1;')],
|
|
24
|
-
);
|
|
25
|
-
const result = run('Write', input, ws);
|
|
26
|
-
expect(result).toBeNull();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('returns null when all rules disabled', () => {
|
|
30
|
-
const ws = makeWorkspace();
|
|
31
|
-
writeFile(
|
|
32
|
-
path.join(ws, 'webpieces.ai-hooks.json'),
|
|
33
|
-
JSON.stringify({
|
|
34
|
-
rules: {
|
|
35
|
-
'no-any-unknown': { enabled: false },
|
|
36
|
-
'max-file-lines': { enabled: false },
|
|
37
|
-
'file-location': { enabled: false },
|
|
38
|
-
'no-destructure': { enabled: false },
|
|
39
|
-
'require-return-type': { enabled: false },
|
|
40
|
-
'no-unmanaged-exceptions': { enabled: false },
|
|
41
|
-
},
|
|
42
|
-
rulesDir: [],
|
|
43
|
-
}),
|
|
44
|
-
);
|
|
45
|
-
const input = new NormalizedToolInput(
|
|
46
|
-
path.join(ws, 'foo.ts'),
|
|
47
|
-
[new NormalizedEdit('', 'const x: number = 1;')],
|
|
48
|
-
);
|
|
49
|
-
const result = run('Write', input, ws);
|
|
50
|
-
expect(result).toBeNull();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('blocks custom edit-scope rule on proposed content', () => {
|
|
54
|
-
const ws = makeWorkspace();
|
|
55
|
-
const rulesDir = path.join(ws, 'ai-hooks-rules');
|
|
56
|
-
writeFile(
|
|
57
|
-
path.join(rulesDir, 'ban-foo.js'),
|
|
58
|
-
`module.exports = {
|
|
59
|
-
name: 'ban-foo',
|
|
60
|
-
description: 'Ban foo',
|
|
61
|
-
scope: 'edit',
|
|
62
|
-
files: ['**/*.ts'],
|
|
63
|
-
defaultOptions: {},
|
|
64
|
-
fixHint: ['use bar instead'],
|
|
65
|
-
check(ctx) {
|
|
66
|
-
const violations = [];
|
|
67
|
-
ctx.strippedLines.forEach((line, i) => {
|
|
68
|
-
if (/\\bfoo\\b/.test(line)) {
|
|
69
|
-
violations.push({ line: i + 1, snippet: ctx.lines[i].trim(), message: 'foo is banned' });
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
return violations;
|
|
73
|
-
},
|
|
74
|
-
};`,
|
|
75
|
-
);
|
|
76
|
-
writeFile(
|
|
77
|
-
path.join(ws, 'webpieces.ai-hooks.json'),
|
|
78
|
-
JSON.stringify({
|
|
79
|
-
rules: {
|
|
80
|
-
'no-any-unknown': { enabled: false },
|
|
81
|
-
'max-file-lines': { enabled: false },
|
|
82
|
-
'file-location': { enabled: false },
|
|
83
|
-
'no-destructure': { enabled: false },
|
|
84
|
-
'require-return-type': { enabled: false },
|
|
85
|
-
'no-unmanaged-exceptions': { enabled: false },
|
|
86
|
-
'ban-foo': { enabled: true },
|
|
87
|
-
},
|
|
88
|
-
rulesDir: ['ai-hooks-rules'],
|
|
89
|
-
}),
|
|
90
|
-
);
|
|
91
|
-
const input = new NormalizedToolInput(
|
|
92
|
-
path.join(ws, 'evil.ts'),
|
|
93
|
-
[new NormalizedEdit('', 'const x = foo;\nconst y = bar;')],
|
|
94
|
-
);
|
|
95
|
-
const result = run('Write', input, ws);
|
|
96
|
-
expect(result).not.toBeNull();
|
|
97
|
-
expect(result!.report).toContain('ban-foo');
|
|
98
|
-
expect(result!.report).toContain('L1');
|
|
99
|
-
expect(result!.report).toContain('foo is banned');
|
|
100
|
-
expect(result!.report).not.toContain('L2');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('disable directive suppresses violation', () => {
|
|
104
|
-
const ws = makeWorkspace();
|
|
105
|
-
const rulesDir = path.join(ws, 'ai-hooks-rules');
|
|
106
|
-
writeFile(
|
|
107
|
-
path.join(rulesDir, 'ban-foo.js'),
|
|
108
|
-
`module.exports = {
|
|
109
|
-
name: 'ban-foo',
|
|
110
|
-
description: 'Ban foo',
|
|
111
|
-
scope: 'edit',
|
|
112
|
-
files: ['**/*.ts'],
|
|
113
|
-
defaultOptions: {},
|
|
114
|
-
fixHint: [],
|
|
115
|
-
check(ctx) {
|
|
116
|
-
const violations = [];
|
|
117
|
-
ctx.strippedLines.forEach((line, i) => {
|
|
118
|
-
if (/\\bfoo\\b/.test(line) && !ctx.isLineDisabled(i + 1, 'ban-foo')) {
|
|
119
|
-
violations.push({ line: i + 1, snippet: ctx.lines[i].trim(), message: 'no foo' });
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
return violations;
|
|
123
|
-
},
|
|
124
|
-
};`,
|
|
125
|
-
);
|
|
126
|
-
writeFile(
|
|
127
|
-
path.join(ws, 'webpieces.ai-hooks.json'),
|
|
128
|
-
JSON.stringify({
|
|
129
|
-
rules: {
|
|
130
|
-
'no-any-unknown': { enabled: false },
|
|
131
|
-
'max-file-lines': { enabled: false },
|
|
132
|
-
'file-location': { enabled: false },
|
|
133
|
-
'no-destructure': { enabled: false },
|
|
134
|
-
'require-return-type': { enabled: false },
|
|
135
|
-
'no-unmanaged-exceptions': { enabled: false },
|
|
136
|
-
'ban-foo': { enabled: true },
|
|
137
|
-
},
|
|
138
|
-
rulesDir: ['ai-hooks-rules'],
|
|
139
|
-
}),
|
|
140
|
-
);
|
|
141
|
-
const input = new NormalizedToolInput(
|
|
142
|
-
path.join(ws, 'allowed.ts'),
|
|
143
|
-
[new NormalizedEdit('', 'const x = foo; // ai-hook-disable ban-foo -- legacy')],
|
|
144
|
-
);
|
|
145
|
-
const result = run('Write', input, ws);
|
|
146
|
-
expect(result).toBeNull();
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('file-scope rule sees projectedFileLines', () => {
|
|
150
|
-
const ws = makeWorkspace();
|
|
151
|
-
const rulesDir = path.join(ws, 'ai-hooks-rules');
|
|
152
|
-
writeFile(
|
|
153
|
-
path.join(rulesDir, 'max-five.js'),
|
|
154
|
-
`module.exports = {
|
|
155
|
-
name: 'max-five',
|
|
156
|
-
description: 'File must be <= 5 lines',
|
|
157
|
-
scope: 'file',
|
|
158
|
-
files: ['**/*.ts'],
|
|
159
|
-
defaultOptions: { limit: 5 },
|
|
160
|
-
fixHint: [],
|
|
161
|
-
check(ctx) {
|
|
162
|
-
if (ctx.projectedFileLines > ctx.options.limit) {
|
|
163
|
-
return [{ line: 1, snippet: '(file too long)', message: 'Projected ' + ctx.projectedFileLines + ' lines exceeds limit ' + ctx.options.limit }];
|
|
164
|
-
}
|
|
165
|
-
return [];
|
|
166
|
-
},
|
|
167
|
-
};`,
|
|
168
|
-
);
|
|
169
|
-
writeFile(
|
|
170
|
-
path.join(ws, 'webpieces.ai-hooks.json'),
|
|
171
|
-
JSON.stringify({
|
|
172
|
-
rules: {
|
|
173
|
-
'no-any-unknown': { enabled: false },
|
|
174
|
-
'max-file-lines': { enabled: false },
|
|
175
|
-
'file-location': { enabled: false },
|
|
176
|
-
'no-destructure': { enabled: false },
|
|
177
|
-
'require-return-type': { enabled: false },
|
|
178
|
-
'no-unmanaged-exceptions': { enabled: false },
|
|
179
|
-
'max-five': { enabled: true },
|
|
180
|
-
},
|
|
181
|
-
rulesDir: ['ai-hooks-rules'],
|
|
182
|
-
}),
|
|
183
|
-
);
|
|
184
|
-
const longContent = Array(10).fill('const x = 1;').join('\n');
|
|
185
|
-
const input = new NormalizedToolInput(
|
|
186
|
-
path.join(ws, 'big.ts'),
|
|
187
|
-
[new NormalizedEdit('', longContent)],
|
|
188
|
-
);
|
|
189
|
-
const result = run('Write', input, ws);
|
|
190
|
-
expect(result).not.toBeNull();
|
|
191
|
-
expect(result!.report).toContain('max-five');
|
|
192
|
-
expect(result!.report).toContain('Projected 10 lines');
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('Edit tool uses new_string as added content', () => {
|
|
196
|
-
const ws = makeWorkspace();
|
|
197
|
-
const target = path.join(ws, 'existing.ts');
|
|
198
|
-
writeFile(target, 'const a = 1;\nconst b = 2;\n');
|
|
199
|
-
const rulesDir = path.join(ws, 'ai-hooks-rules');
|
|
200
|
-
writeFile(
|
|
201
|
-
path.join(rulesDir, 'ban-foo.js'),
|
|
202
|
-
`module.exports = {
|
|
203
|
-
name: 'ban-foo',
|
|
204
|
-
description: 'Ban foo',
|
|
205
|
-
scope: 'edit',
|
|
206
|
-
files: ['**/*.ts'],
|
|
207
|
-
defaultOptions: {},
|
|
208
|
-
fixHint: [],
|
|
209
|
-
check(ctx) {
|
|
210
|
-
const vs = [];
|
|
211
|
-
ctx.strippedLines.forEach((l, i) => {
|
|
212
|
-
if (/foo/.test(l)) vs.push({ line: i+1, snippet: ctx.lines[i].trim(), message: 'no foo' });
|
|
213
|
-
});
|
|
214
|
-
return vs;
|
|
215
|
-
},
|
|
216
|
-
};`,
|
|
217
|
-
);
|
|
218
|
-
writeFile(
|
|
219
|
-
path.join(ws, 'webpieces.ai-hooks.json'),
|
|
220
|
-
JSON.stringify({
|
|
221
|
-
rules: {
|
|
222
|
-
'no-any-unknown': { enabled: false },
|
|
223
|
-
'max-file-lines': { enabled: false },
|
|
224
|
-
'file-location': { enabled: false },
|
|
225
|
-
'no-destructure': { enabled: false },
|
|
226
|
-
'require-return-type': { enabled: false },
|
|
227
|
-
'no-unmanaged-exceptions': { enabled: false },
|
|
228
|
-
'ban-foo': { enabled: true },
|
|
229
|
-
},
|
|
230
|
-
rulesDir: ['ai-hooks-rules'],
|
|
231
|
-
}),
|
|
232
|
-
);
|
|
233
|
-
const input = new NormalizedToolInput(target, [
|
|
234
|
-
new NormalizedEdit('const a = 1;', 'const a = foo;'),
|
|
235
|
-
]);
|
|
236
|
-
const result = run('Edit', input, ws);
|
|
237
|
-
expect(result).not.toBeNull();
|
|
238
|
-
expect(result!.report).toContain('ban-foo');
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('MultiEdit reports which edit triggered', () => {
|
|
242
|
-
const ws = makeWorkspace();
|
|
243
|
-
const target = path.join(ws, 'multi.ts');
|
|
244
|
-
writeFile(target, 'const a = 1;\nconst b = 2;\nconst c = 3;\n');
|
|
245
|
-
const rulesDir = path.join(ws, 'ai-hooks-rules');
|
|
246
|
-
writeFile(
|
|
247
|
-
path.join(rulesDir, 'ban-foo.js'),
|
|
248
|
-
`module.exports = {
|
|
249
|
-
name: 'ban-foo',
|
|
250
|
-
description: 'Ban foo',
|
|
251
|
-
scope: 'edit',
|
|
252
|
-
files: ['**/*.ts'],
|
|
253
|
-
defaultOptions: {},
|
|
254
|
-
fixHint: [],
|
|
255
|
-
check(ctx) {
|
|
256
|
-
const vs = [];
|
|
257
|
-
ctx.strippedLines.forEach((l, i) => {
|
|
258
|
-
if (/foo/.test(l)) vs.push({ line: i+1, snippet: ctx.lines[i].trim(), message: 'no foo' });
|
|
259
|
-
});
|
|
260
|
-
return vs;
|
|
261
|
-
},
|
|
262
|
-
};`,
|
|
263
|
-
);
|
|
264
|
-
writeFile(
|
|
265
|
-
path.join(ws, 'webpieces.ai-hooks.json'),
|
|
266
|
-
JSON.stringify({
|
|
267
|
-
rules: {
|
|
268
|
-
'no-any-unknown': { enabled: false },
|
|
269
|
-
'max-file-lines': { enabled: false },
|
|
270
|
-
'file-location': { enabled: false },
|
|
271
|
-
'no-destructure': { enabled: false },
|
|
272
|
-
'require-return-type': { enabled: false },
|
|
273
|
-
'no-unmanaged-exceptions': { enabled: false },
|
|
274
|
-
'ban-foo': { enabled: true },
|
|
275
|
-
},
|
|
276
|
-
rulesDir: ['ai-hooks-rules'],
|
|
277
|
-
}),
|
|
278
|
-
);
|
|
279
|
-
const input = new NormalizedToolInput(target, [
|
|
280
|
-
new NormalizedEdit('const a = 1;', 'const a = 11;'),
|
|
281
|
-
new NormalizedEdit('const b = 2;', 'const b = foo;'),
|
|
282
|
-
new NormalizedEdit('const c = 3;', 'const c = 33;'),
|
|
283
|
-
]);
|
|
284
|
-
const result = run('MultiEdit', input, ws);
|
|
285
|
-
expect(result).not.toBeNull();
|
|
286
|
-
expect(result!.report).toContain('edit 2/3');
|
|
287
|
-
});
|
|
288
|
-
});
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @webpieces/max-method-lines -- test describe blocks are inherently large */
|
|
2
|
-
import { stripTsNoise } from '../strip-ts-noise';
|
|
3
|
-
|
|
4
|
-
describe('stripTsNoise', () => {
|
|
5
|
-
it('passes code through unchanged', () => {
|
|
6
|
-
const src = 'const x: number = 1;\nconst y = x + 2;\n';
|
|
7
|
-
expect(stripTsNoise(src)).toBe(src);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('preserves line count for all inputs', () => {
|
|
11
|
-
const cases = [
|
|
12
|
-
'a\nb\nc',
|
|
13
|
-
'// comment\nconst x = 1;',
|
|
14
|
-
'/* multi\n line\n comment */\nconst x = 1;',
|
|
15
|
-
'"string with\\nescaped newline"',
|
|
16
|
-
'`template\nwith\nnewlines`',
|
|
17
|
-
'"multi\nline"\nconst y = 2;',
|
|
18
|
-
];
|
|
19
|
-
for (const src of cases) {
|
|
20
|
-
const out = stripTsNoise(src);
|
|
21
|
-
expect(out.split('\n').length).toBe(src.split('\n').length);
|
|
22
|
-
expect(out.length).toBe(src.length);
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('replaces double-quoted string interior with spaces', () => {
|
|
27
|
-
const src = 'const x = "has any keyword";';
|
|
28
|
-
const out = stripTsNoise(src);
|
|
29
|
-
expect(out).not.toContain('any');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('replaces single-quoted string interior', () => {
|
|
33
|
-
const src = "const x = 'any';";
|
|
34
|
-
const out = stripTsNoise(src);
|
|
35
|
-
expect(out).not.toContain('any');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('handles escaped quotes inside strings', () => {
|
|
39
|
-
const src = 'const x = "has \\"any\\" keyword";';
|
|
40
|
-
const out = stripTsNoise(src);
|
|
41
|
-
expect(out).not.toContain('any');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('replaces line comment body with spaces', () => {
|
|
45
|
-
const src = '// this has any in it\nconst x = 1;';
|
|
46
|
-
const out = stripTsNoise(src);
|
|
47
|
-
expect(out.split('\n')[0]).not.toContain('any');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('replaces block comment body, preserves newlines', () => {
|
|
51
|
-
const src = '/* line1 any\n line2 any */\nconst x = 1;';
|
|
52
|
-
const out = stripTsNoise(src);
|
|
53
|
-
expect(out).not.toContain('any');
|
|
54
|
-
expect(out.split('\n').length).toBe(3);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('strips template literal body but keeps ${} interp as code', () => {
|
|
58
|
-
const src = 'const x = `prefix ${value + 1} suffix any`;';
|
|
59
|
-
const out = stripTsNoise(src);
|
|
60
|
-
expect(out).not.toMatch(/prefix/);
|
|
61
|
-
expect(out).not.toMatch(/ any`/);
|
|
62
|
-
expect(out).toMatch(/\$\{value \+ 1\}/);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('handles nested template inside interpolation', () => {
|
|
66
|
-
const src = 'const x = `outer ${`inner ${y} end`} done`;';
|
|
67
|
-
const out = stripTsNoise(src);
|
|
68
|
-
expect(out).toMatch(/\$\{y\}/);
|
|
69
|
-
expect(out).not.toMatch(/outer/);
|
|
70
|
-
expect(out).not.toMatch(/inner/);
|
|
71
|
-
expect(out).not.toMatch(/end/);
|
|
72
|
-
expect(out).not.toMatch(/done/);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('strips string inside template interpolation', () => {
|
|
76
|
-
const src = 'const x = `prefix ${"some any string"} suffix`;';
|
|
77
|
-
const out = stripTsNoise(src);
|
|
78
|
-
expect(out).not.toContain('some any string');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('keeps any keyword in real code visible', () => {
|
|
82
|
-
const src = 'const x: any = 1;\nconst y: number = 2;';
|
|
83
|
-
const out = stripTsNoise(src);
|
|
84
|
-
expect(out).toContain(': any');
|
|
85
|
-
expect(out).toContain(': number');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('hides any keyword inside comment', () => {
|
|
89
|
-
const src = 'const x: number = 1; // this returns any maybe';
|
|
90
|
-
const out = stripTsNoise(src);
|
|
91
|
-
expect(out).toContain(': number');
|
|
92
|
-
expect(out.indexOf('any')).toBe(-1);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('does not treat division as a comment', () => {
|
|
96
|
-
const src = 'const x = a / b;\nconst y = c / d;';
|
|
97
|
-
expect(stripTsNoise(src)).toBe(src);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('handles empty input', () => {
|
|
101
|
-
expect(stripTsNoise('')).toBe('');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('does not bleed comment into next line', () => {
|
|
105
|
-
const src = '// disable rule\nconst x: any = 1;';
|
|
106
|
-
const out = stripTsNoise(src);
|
|
107
|
-
expect(out).toContain(': any');
|
|
108
|
-
});
|
|
109
|
-
});
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
|
|
4
|
-
import { stripTsNoise } from './strip-ts-noise';
|
|
5
|
-
import { createIsLineDisabled } from './disable-directives';
|
|
6
|
-
import {
|
|
7
|
-
ToolKind, NormalizedToolInput, NormalizedEdit,
|
|
8
|
-
EditContext, FileContext, BashContext,
|
|
9
|
-
} from './types';
|
|
10
|
-
|
|
11
|
-
export class BuiltContexts {
|
|
12
|
-
readonly fileContext: FileContext;
|
|
13
|
-
readonly editContexts: readonly EditContext[];
|
|
14
|
-
|
|
15
|
-
constructor(fileContext: FileContext, editContexts: readonly EditContext[]) {
|
|
16
|
-
this.fileContext = fileContext;
|
|
17
|
-
this.editContexts = editContexts;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function buildContexts(
|
|
22
|
-
toolKind: ToolKind,
|
|
23
|
-
input: NormalizedToolInput,
|
|
24
|
-
workspaceRoot: string,
|
|
25
|
-
): BuiltContexts {
|
|
26
|
-
const filePath = input.filePath;
|
|
27
|
-
const relativePath = path.relative(workspaceRoot, filePath);
|
|
28
|
-
const edits = input.edits;
|
|
29
|
-
|
|
30
|
-
const currentFileLines = readCurrentFileLines(filePath);
|
|
31
|
-
let linesAdded = 0;
|
|
32
|
-
let linesRemoved = 0;
|
|
33
|
-
for (const e of edits) {
|
|
34
|
-
linesAdded += countLines(e.newString);
|
|
35
|
-
linesRemoved += countLines(e.oldString);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const projectedFileLines =
|
|
39
|
-
toolKind === 'Write'
|
|
40
|
-
? countLines(edits.length > 0 ? edits[0].newString : '')
|
|
41
|
-
: currentFileLines + linesAdded - linesRemoved;
|
|
42
|
-
|
|
43
|
-
const fileContext = new FileContext(
|
|
44
|
-
toolKind,
|
|
45
|
-
filePath,
|
|
46
|
-
relativePath,
|
|
47
|
-
workspaceRoot,
|
|
48
|
-
currentFileLines,
|
|
49
|
-
linesAdded,
|
|
50
|
-
linesRemoved,
|
|
51
|
-
projectedFileLines,
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const editContexts = edits.map((e, idx) => {
|
|
55
|
-
const addedContent = e.newString;
|
|
56
|
-
const stripped = stripTsNoise(addedContent);
|
|
57
|
-
const isLineDisabled = createIsLineDisabled(addedContent);
|
|
58
|
-
return new EditContext(
|
|
59
|
-
toolKind,
|
|
60
|
-
idx,
|
|
61
|
-
edits.length,
|
|
62
|
-
filePath,
|
|
63
|
-
relativePath,
|
|
64
|
-
workspaceRoot,
|
|
65
|
-
addedContent,
|
|
66
|
-
stripped,
|
|
67
|
-
addedContent.split('\n'),
|
|
68
|
-
stripped.split('\n'),
|
|
69
|
-
e.oldString,
|
|
70
|
-
isLineDisabled,
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
return new BuiltContexts(fileContext, editContexts);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function buildBashContext(command: string, workspaceRoot: string): BashContext {
|
|
78
|
-
return new BashContext(command, workspaceRoot);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function readCurrentFileLines(filePath: string): number {
|
|
82
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
83
|
-
try {
|
|
84
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
85
|
-
return countLines(content);
|
|
86
|
-
} catch (err: unknown) {
|
|
87
|
-
// eslint-disable-next-line @webpieces/catch-error-pattern -- file-not-found is expected for new Write targets
|
|
88
|
-
void err;
|
|
89
|
-
return 0;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function countLines(s: string): number {
|
|
94
|
-
if (!s) return 0;
|
|
95
|
-
return s.split('\n').length;
|
|
96
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import type { IsLineDisabled } from './types';
|
|
2
|
-
|
|
3
|
-
const DIRECTIVE_RE =
|
|
4
|
-
/\/\/\s*(?:ai-hook-disable|webpieces-disable)(?:-(next|file|all))?(?:\s+([\w\-*,\s]+?))?(?:\s*--\s*(.*))?\s*$/;
|
|
5
|
-
|
|
6
|
-
export class DirectiveIndex {
|
|
7
|
-
private readonly lineDisables: Map<number, Set<string>>;
|
|
8
|
-
private readonly fileDisables: Set<string>;
|
|
9
|
-
|
|
10
|
-
constructor(lineDisables: Map<number, Set<string>>, fileDisables: Set<string>) {
|
|
11
|
-
this.lineDisables = lineDisables;
|
|
12
|
-
this.fileDisables = fileDisables;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
isLineDisabled(lineNum: number, ruleName: string): boolean {
|
|
16
|
-
if (this.fileDisables.has('*') || this.fileDisables.has(ruleName)) return true;
|
|
17
|
-
const set = this.lineDisables.get(lineNum);
|
|
18
|
-
if (!set) return false;
|
|
19
|
-
return set.has('*') || set.has(ruleName);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function parseRuleList(raw: string | undefined): readonly string[] {
|
|
24
|
-
if (!raw || raw.trim() === '' || raw.trim() === '*') return ['*'];
|
|
25
|
-
return raw.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function nextTargetLine(lines: readonly string[], lineIdx: number): number | null {
|
|
29
|
-
for (let j = lineIdx + 1; j < lines.length; j += 1) {
|
|
30
|
-
const trimmed = lines[j].trim();
|
|
31
|
-
if (trimmed === '') continue;
|
|
32
|
-
if (/^\/\/\s*(?:ai-hook-disable|webpieces-disable)/.test(trimmed)) continue;
|
|
33
|
-
return j + 1;
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function addDisable(map: Map<number, Set<string>>, lineNum: number, rules: readonly string[]): void {
|
|
39
|
-
if (!map.has(lineNum)) map.set(lineNum, new Set<string>());
|
|
40
|
-
const set = map.get(lineNum)!;
|
|
41
|
-
for (const r of rules) set.add(r);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function resolveTarget(
|
|
45
|
-
line: string, lines: readonly string[], i: number,
|
|
46
|
-
map: Map<number, Set<string>>, rules: readonly string[],
|
|
47
|
-
): void {
|
|
48
|
-
const beforeComment = line.slice(0, line.indexOf('//')).trim();
|
|
49
|
-
if (beforeComment !== '') {
|
|
50
|
-
addDisable(map, i + 1, rules);
|
|
51
|
-
} else {
|
|
52
|
-
const target = nextTargetLine(lines, i);
|
|
53
|
-
if (target !== null) addDisable(map, target, rules);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function parseDirectives(source: string): DirectiveIndex {
|
|
58
|
-
const lines = source.split('\n');
|
|
59
|
-
const lineDisables = new Map<number, Set<string>>();
|
|
60
|
-
const fileDisables = new Set<string>();
|
|
61
|
-
|
|
62
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
63
|
-
const line = lines[i];
|
|
64
|
-
const match = line.match(DIRECTIVE_RE);
|
|
65
|
-
if (!match) continue;
|
|
66
|
-
const variant = match[1] || null;
|
|
67
|
-
const rules = parseRuleList(match[2] || '');
|
|
68
|
-
|
|
69
|
-
if (variant === 'file') {
|
|
70
|
-
if (i < 20) { for (const r of rules) fileDisables.add(r); }
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (variant === 'next') {
|
|
74
|
-
const target = nextTargetLine(lines, i);
|
|
75
|
-
if (target !== null) addDisable(lineDisables, target, rules);
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
if (variant === 'all') {
|
|
79
|
-
resolveTarget(line, lines, i, lineDisables, ['*']);
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
resolveTarget(line, lines, i, lineDisables, rules);
|
|
83
|
-
}
|
|
84
|
-
return new DirectiveIndex(lineDisables, fileDisables);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function createIsLineDisabled(source: string): IsLineDisabled {
|
|
88
|
-
const index = parseDirectives(source);
|
|
89
|
-
return (lineNum: number, ruleName: string): boolean => index.isLineDisabled(lineNum, ruleName);
|
|
90
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
|
|
4
|
-
const INSTRUCT_DIR = '.webpieces/instruct-ai';
|
|
5
|
-
|
|
6
|
-
export function writeTemplateIfMissing(workspaceRoot: string, templateName: string): void {
|
|
7
|
-
const dir = path.join(workspaceRoot, INSTRUCT_DIR);
|
|
8
|
-
const filePath = path.join(dir, templateName);
|
|
9
|
-
if (fs.existsSync(filePath)) return;
|
|
10
|
-
|
|
11
|
-
const templatePath = path.join(__dirname, '..', '..', 'templates', templateName);
|
|
12
|
-
const content = fs.readFileSync(templatePath, 'utf-8');
|
|
13
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
14
|
-
fs.writeFileSync(filePath, content);
|
|
15
|
-
}
|
package/src/core/load-config.ts
DELETED