@boyingliu01/xp-gate 0.20.0 → 0.20.2
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/adapters/gate-8.sh +115 -73
- package/adapters/java.sh +373 -27
- package/adapters/powershell.sh +60 -5
- package/adapters/python.sh +38 -4
- package/build-integrity/__tests__/import-resolver.test.ts +59 -0
- package/build-integrity/gate-10.ts +17 -2
- package/gate-3.sh +51 -3
- package/gate-4.sh +32 -1
- package/gate-8.sh +115 -73
- package/hooks/gate-3.sh +51 -3
- package/hooks/gate-4.sh +32 -1
- package/hooks/gate-8.sh +115 -73
- package/hooks/pre-commit +404 -62
- package/lib/__tests__/doctor-spawn-capability.test.js +116 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +49 -10
- package/lib/doctor-tui.js +21 -3
- package/lib/doctor.js +111 -8
- package/lib/gate-audit.ts +3 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/package.json +2 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/admin-template-guidelines/SKILL.md +1 -0
- package/plugins/claude-code/skills/batch-grill-me/SKILL.md +1 -0
- package/plugins/claude-code/skills/clipboard-vision/SKILL.md +1 -0
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/domain-modeling/SKILL.md +1 -0
- package/plugins/claude-code/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/claude-code/skills/grilling/SKILL.md +1 -0
- package/plugins/claude-code/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/plugins/claude-code/skills/ralph-loop/SKILL.md +1 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/test-driven-development/SKILL.md +1 -0
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +1 -0
- package/plugins/claude-code/skills/to-issues/SKILL.md +1 -0
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/admin-template-guidelines/SKILL.md +1 -0
- package/plugins/dsh/skills/batch-grill-me/SKILL.md +1 -0
- package/plugins/dsh/skills/clipboard-vision/SKILL.md +1 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/dsh/skills/domain-modeling/SKILL.md +1 -0
- package/plugins/dsh/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/dsh/skills/grilling/SKILL.md +1 -0
- package/plugins/dsh/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/plugins/dsh/skills/ralph-loop/SKILL.md +1 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/dsh/skills/test-driven-development/SKILL.md +1 -0
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/skills/test-specification-alignment/SKILL.md +1 -0
- package/plugins/dsh/skills/to-issues/SKILL.md +1 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/admin-template-guidelines/SKILL.md +1 -0
- package/plugins/opencode/skills/batch-grill-me/SKILL.md +1 -0
- package/plugins/opencode/skills/clipboard-vision/SKILL.md +1 -0
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/domain-modeling/SKILL.md +1 -0
- package/plugins/opencode/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/opencode/skills/grilling/SKILL.md +1 -0
- package/plugins/opencode/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/plugins/opencode/skills/ralph-loop/SKILL.md +1 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/test-driven-development/SKILL.md +1 -0
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/opencode/skills/test-specification-alignment/SKILL.md +1 -0
- package/plugins/opencode/skills/to-issues/SKILL.md +1 -0
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +4 -4
- package/principles/adapters/__tests__/java.test.ts +317 -0
- package/principles/adapters/java.ts +325 -13
- package/principles/boy-scout.ts +36 -6
- package/scripts/sync-package-content.js +13 -6
- package/skills/admin-template-guidelines/SKILL.md +1 -0
- package/skills/batch-grill-me/SKILL.md +1 -0
- package/skills/clipboard-vision/SKILL.md +1 -0
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/domain-modeling/SKILL.md +1 -0
- package/skills/grill-with-docs/SKILL.md +1 -0
- package/skills/grilling/SKILL.md +1 -0
- package/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/skills/ralph-loop/SKILL.md +1 -0
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/test-driven-development/SKILL.md +1 -0
- package/skills/test-specification-alignment/AGENTS.md +4 -4
- package/skills/test-specification-alignment/SKILL.md +1 -0
- package/skills/to-issues/SKILL.md +1 -0
|
@@ -65,4 +65,321 @@ describe('JavaAdapter', () => {
|
|
|
65
65
|
const adapter = new JavaAdapter('test.ts');
|
|
66
66
|
expect(adapter.detectLanguage()).toBe('typescript');
|
|
67
67
|
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @test REQ-507-04
|
|
72
|
+
* @intent 验证 JavaAdapter 为各规则提供其所需度量字段(#507 S4):修复前适配器
|
|
73
|
+
* 只返回 {name,type,line,code},读结构字段的 12 条规则在 Java 上静默
|
|
74
|
+
* 拿到 undefined,G4 报告已检查但 0 覆盖。覆盖矩阵见
|
|
75
|
+
* docs/java-principles-coverage.md。
|
|
76
|
+
* @covers AC-507-04-01
|
|
77
|
+
*/
|
|
78
|
+
describe('JavaAdapter metric extraction (#507)', () => {
|
|
79
|
+
interface FnInfo {
|
|
80
|
+
name: string;
|
|
81
|
+
startLine?: number;
|
|
82
|
+
line?: number;
|
|
83
|
+
length?: number;
|
|
84
|
+
paramCount?: number;
|
|
85
|
+
nestingDepth?: number;
|
|
86
|
+
hasTryCatch?: boolean;
|
|
87
|
+
ioOperations?: string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const LONG_BODY = Array.from({ length: 60 }, (_, i) => ` int v${i} = ${i};`).join('\n');
|
|
91
|
+
|
|
92
|
+
const SAMPLE = `
|
|
93
|
+
public class Sample {
|
|
94
|
+
public void eightParams(int a, int b, int c, int d, int e, int f, int g, int h) {
|
|
95
|
+
System.out.println(a);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public void longOne() {
|
|
99
|
+
${LONG_BODY}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public void deepOne(int x) {
|
|
104
|
+
if (x > 0) {
|
|
105
|
+
if (x > 1) {
|
|
106
|
+
if (x > 2) {
|
|
107
|
+
if (x > 3) {
|
|
108
|
+
if (x > 4) {
|
|
109
|
+
System.out.println(x);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public void guardedIo() {
|
|
118
|
+
try {
|
|
119
|
+
java.sql.Connection c = java.sql.DriverManager.getConnection("jdbc:x");
|
|
120
|
+
c.createStatement().executeQuery("SELECT 1");
|
|
121
|
+
} catch (Exception e) {
|
|
122
|
+
// swallowed
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public void rawIo() {
|
|
127
|
+
java.sql.Connection c = java.sql.DriverManager.getConnection("jdbc:x");
|
|
128
|
+
c.createStatement().executeQuery("SELECT 1");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public int magic() { return 42; }
|
|
132
|
+
}
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
function adapterFor(content = SAMPLE): JavaAdapter {
|
|
136
|
+
(readFileSync as Mock).mockReturnValue(content);
|
|
137
|
+
return new JavaAdapter('Sample.java');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function findFn(fns: unknown[], name: string): FnInfo {
|
|
141
|
+
const found = fns.find(f => (f as FnInfo).name === name);
|
|
142
|
+
if (!found) throw new Error(`fixture function not extracted: ${name}`);
|
|
143
|
+
return found as FnInfo;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
it('reports paramCount (too-many-params contract)', () => {
|
|
147
|
+
const fns = adapterFor().extractFunctions() as FnInfo[];
|
|
148
|
+
expect(findFn(fns, 'eightParams').paramCount).toBe(8);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('reports length body span (long-function contract)', () => {
|
|
152
|
+
const fns = adapterFor().extractFunctions() as FnInfo[];
|
|
153
|
+
expect(findFn(fns, 'longOne').length).toBeGreaterThan(50);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('reports nestingDepth (deep-nesting contract)', () => {
|
|
157
|
+
const fns = adapterFor().extractFunctions() as FnInfo[];
|
|
158
|
+
expect(findFn(fns, 'deepOne').nestingDepth).toBeGreaterThanOrEqual(5);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('reports hasTryCatch and ioOperations (missing-error-handling contract)', () => {
|
|
162
|
+
const fns = adapterFor().extractFunctions() as FnInfo[];
|
|
163
|
+
const guarded = findFn(fns, 'guardedIo');
|
|
164
|
+
expect(guarded.hasTryCatch).toBe(true);
|
|
165
|
+
expect((guarded.ioOperations ?? []).length).toBeGreaterThan(0);
|
|
166
|
+
|
|
167
|
+
const raw = findFn(fns, 'rawIo');
|
|
168
|
+
expect(raw.hasTryCatch).toBe(false);
|
|
169
|
+
expect(raw.ioOperations).toContain('getConnection');
|
|
170
|
+
expect(raw.ioOperations).toContain('executeQuery');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('reports startLine for every function (violation line attribution)', () => {
|
|
174
|
+
const fns = adapterFor().extractFunctions() as FnInfo[];
|
|
175
|
+
for (const f of fns) {
|
|
176
|
+
expect(f.startLine).toBeGreaterThan(0);
|
|
177
|
+
expect(f.name).toBeTruthy();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('does not report control keywords as functions', () => {
|
|
182
|
+
const fns = adapterFor().extractFunctions() as FnInfo[];
|
|
183
|
+
const names = fns.map(f => f.name);
|
|
184
|
+
for (const kw of ['if', 'for', 'while', 'switch', 'catch', 'return']) {
|
|
185
|
+
expect(names).not.toContain(kw);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('extract (magic numbers)', () => {
|
|
190
|
+
it('returns {value,line} and masks strings/comments', () => {
|
|
191
|
+
const content = `
|
|
192
|
+
// 999 in a comment must not count
|
|
193
|
+
String s = "1234 in a string must not count";
|
|
194
|
+
int x = 12345;
|
|
195
|
+
`;
|
|
196
|
+
(readFileSync as Mock).mockReturnValue(content);
|
|
197
|
+
const adapter = new JavaAdapter('M.java') as unknown as {
|
|
198
|
+
extract: () => Array<{ value: number; line: number }>;
|
|
199
|
+
};
|
|
200
|
+
const values = adapter.extract().map(n => n.value);
|
|
201
|
+
expect(values).toContain(12345);
|
|
202
|
+
expect(values).not.toContain(999);
|
|
203
|
+
expect(values).not.toContain(1234);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('reports digits used as call arguments (#507 blind review M2)', () => {
|
|
207
|
+
const content = `
|
|
208
|
+
public class Calls {
|
|
209
|
+
public int go() {
|
|
210
|
+
return Math.max(1, 99);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
`;
|
|
214
|
+
(readFileSync as Mock).mockReturnValue(content);
|
|
215
|
+
const adapter = new JavaAdapter('C.java') as unknown as {
|
|
216
|
+
extract: () => Array<{ value: number; line: number }>;
|
|
217
|
+
};
|
|
218
|
+
const values = adapter.extract().map(n => n.value);
|
|
219
|
+
expect(values).toContain(99);
|
|
220
|
+
expect(values).toContain(1);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('masks block comments including cross-line ones (#507 blind review M3)', () => {
|
|
224
|
+
const content = `
|
|
225
|
+
public class Commented {
|
|
226
|
+
/* int inlineBlock = 555; */
|
|
227
|
+
// string containing slashes must not swallow the rest: see below
|
|
228
|
+
String url = "http://x"; int y = 42;
|
|
229
|
+
/*
|
|
230
|
+
* int multi = 777;
|
|
231
|
+
* int line2 = 888;
|
|
232
|
+
*/
|
|
233
|
+
int live = 11;
|
|
234
|
+
}
|
|
235
|
+
`;
|
|
236
|
+
(readFileSync as Mock).mockReturnValue(content);
|
|
237
|
+
const adapter = new JavaAdapter('B.java') as unknown as {
|
|
238
|
+
extract: () => Array<{ value: number; line: number }>;
|
|
239
|
+
};
|
|
240
|
+
const values = adapter.extract().map(n => n.value);
|
|
241
|
+
expect(values).toContain(42); // after a string with `//`, same line
|
|
242
|
+
expect(values).toContain(11); // after a multi-line block comment
|
|
243
|
+
expect(values).not.toContain(555);
|
|
244
|
+
expect(values).not.toContain(777);
|
|
245
|
+
expect(values).not.toContain(888);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('skips static final constant initializers but reports bare literals', () => {
|
|
249
|
+
// The rule's remedy is "use a named constant" -- flagging the constant
|
|
250
|
+
// itself would make violations unfixable (#446 precedent on TS).
|
|
251
|
+
const content = `
|
|
252
|
+
public class C {
|
|
253
|
+
public static final int MAX = 3;
|
|
254
|
+
public int bare() { return 137; }
|
|
255
|
+
}
|
|
256
|
+
`;
|
|
257
|
+
(readFileSync as Mock).mockReturnValue(content);
|
|
258
|
+
const adapter = new JavaAdapter('C.java') as unknown as {
|
|
259
|
+
extract: () => Array<{ value: number; line: number }>;
|
|
260
|
+
};
|
|
261
|
+
const values = adapter.extract().map(n => n.value);
|
|
262
|
+
expect(values).not.toContain(3);
|
|
263
|
+
expect(values).toContain(137);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('imports (unused-imports contract)', () => {
|
|
268
|
+
it('marks unused and used imports with line numbers', () => {
|
|
269
|
+
const content = `
|
|
270
|
+
import java.util.ArrayList;
|
|
271
|
+
import java.io.File;
|
|
272
|
+
|
|
273
|
+
public class C {
|
|
274
|
+
public ArrayList<String> go() { return new ArrayList<>(); }
|
|
275
|
+
}
|
|
276
|
+
`;
|
|
277
|
+
(readFileSync as Mock).mockReturnValue(content);
|
|
278
|
+
const adapter = new JavaAdapter('C.java') as unknown as {
|
|
279
|
+
imports: Array<{ name: string; line: number; used: boolean }>;
|
|
280
|
+
};
|
|
281
|
+
const imports = adapter.imports;
|
|
282
|
+
const list = imports.find(i => i.name === 'java.util.ArrayList');
|
|
283
|
+
expect(list?.used).toBe(true);
|
|
284
|
+
const file = imports.find(i => i.name === 'java.io.File');
|
|
285
|
+
expect(file?.used).toBe(false);
|
|
286
|
+
expect(file?.line).toBe(3);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('extractClasses / extractInterfaces (srp/isp contracts)', () => {
|
|
291
|
+
it('reports methodCount per class (srp reads it directly)', () => {
|
|
292
|
+
const content = `
|
|
293
|
+
public class Busy {
|
|
294
|
+
public void m1() {}
|
|
295
|
+
public void m2() {}
|
|
296
|
+
public void m3() {}
|
|
297
|
+
public void m4() {}
|
|
298
|
+
public void m5() {}
|
|
299
|
+
public void m6() {}
|
|
300
|
+
}
|
|
301
|
+
`;
|
|
302
|
+
(readFileSync as Mock).mockReturnValue(content);
|
|
303
|
+
const classes = adapterFor(content).extractClasses() as Array<{ name: string; methodCount: number }>;
|
|
304
|
+
expect(classes.find(c => c.name === 'Busy')?.methodCount).toBe(6);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('extractInterfaces reports interfaces with methodCount (isp contract)', () => {
|
|
308
|
+
const content = `
|
|
309
|
+
public interface Worker {
|
|
310
|
+
void run();
|
|
311
|
+
void stop();
|
|
312
|
+
}
|
|
313
|
+
`;
|
|
314
|
+
(readFileSync as Mock).mockReturnValue(content);
|
|
315
|
+
const adapter = new JavaAdapter('W.java') as unknown as {
|
|
316
|
+
extractInterfaces: () => Array<{ name: string; methodCount: number }>;
|
|
317
|
+
};
|
|
318
|
+
const ifaces = adapter.extractInterfaces();
|
|
319
|
+
expect(ifaces.find(i => i.name === 'Worker')?.methodCount).toBe(2);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('finds generic classes and interfaces (#507 blind review M1)', () => {
|
|
323
|
+
const content = `
|
|
324
|
+
public class Box<T> {
|
|
325
|
+
private T value;
|
|
326
|
+
public T get() { return value; }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
public interface Repo<T, ID> {
|
|
330
|
+
T findById(ID id);
|
|
331
|
+
}
|
|
332
|
+
`;
|
|
333
|
+
(readFileSync as Mock).mockReturnValue(content);
|
|
334
|
+
const classes = adapterFor(content).extractClasses() as Array<{ name: string; methodCount: number }>;
|
|
335
|
+
expect(classes.find(c => c.name === 'Box')?.methodCount).toBe(1);
|
|
336
|
+
const adapter = adapterFor(content) as unknown as {
|
|
337
|
+
extractInterfaces: () => Array<{ name: string; methodCount: number }>;
|
|
338
|
+
};
|
|
339
|
+
expect(adapter.extractInterfaces().find(i => i.name === 'Repo')?.methodCount).toBe(1);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('does not count masked-out members (strings/comments/inner calls)', () => {
|
|
343
|
+
const content = `
|
|
344
|
+
public class Noisy {
|
|
345
|
+
public void real() {
|
|
346
|
+
String s = "fake(1)";
|
|
347
|
+
// phantomCall(
|
|
348
|
+
/* alsoFake(2) */
|
|
349
|
+
helper(1);
|
|
350
|
+
}
|
|
351
|
+
public void second() {}
|
|
352
|
+
}
|
|
353
|
+
`;
|
|
354
|
+
(readFileSync as Mock).mockReturnValue(content);
|
|
355
|
+
const classes = adapterFor(content).extractClasses() as Array<{ name: string; methodCount: number; methods: string[] }>;
|
|
356
|
+
const methods = classes.find(c => c.name === 'Noisy')?.methods ?? [];
|
|
357
|
+
// The Java member heuristic mirrors the TS adapter (pure `name(` scan on
|
|
358
|
+
// the class body), so `helper(` -- a real, unqualified call statement --
|
|
359
|
+
// DOES count. What must never surface are the masked-out members:
|
|
360
|
+
// fake( lived inside a string literal
|
|
361
|
+
// phantomCall( lived in a line comment
|
|
362
|
+
// alsoFake( lived in a block comment
|
|
363
|
+
expect(methods).not.toContain('fake');
|
|
364
|
+
expect(methods).not.toContain('phantomCall');
|
|
365
|
+
expect(methods).not.toContain('alsoFake');
|
|
366
|
+
expect(methods).toEqual(expect.arrayContaining(['real', 'second']));
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('does not leak brace depth from string literals (#507 blind review M4)', () => {
|
|
370
|
+
const content = `
|
|
371
|
+
public class Formatter {
|
|
372
|
+
public String weird() {
|
|
373
|
+
String s = "unbalanced { brace";
|
|
374
|
+
return s;
|
|
375
|
+
}
|
|
376
|
+
public int after() { return 0; }
|
|
377
|
+
}
|
|
378
|
+
`;
|
|
379
|
+
(readFileSync as Mock).mockReturnValue(content);
|
|
380
|
+
const fns = adapterFor(content).extractFunctions() as Array<{ name: string; nestingDepth: number }>;
|
|
381
|
+
// `after` must not inherit inflated depth from the string's `{`.
|
|
382
|
+
expect(findFn(fns as FnInfo[], 'after').nestingDepth).toBeLessThan(3);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
68
385
|
});
|
|
@@ -1,6 +1,191 @@
|
|
|
1
1
|
import { BaseAdapter } from './base';
|
|
2
2
|
import { Adapter } from '../types';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Structural fields the clean-code rules consume (mirror of the TypeScript
|
|
6
|
+
* adapter's function shape). Before #507 this adapter returned only
|
|
7
|
+
* `{ name, type, modifiers, line, code }`, so every rule reading
|
|
8
|
+
* `startLine`/`length`/`paramCount`/`nestingDepth`/`hasTryCatch`/`ioOperations`
|
|
9
|
+
* silently saw `undefined` and skipped the file -- G4 reported a run over Java
|
|
10
|
+
* with zero effective coverage (#507 S4 audit).
|
|
11
|
+
*/
|
|
12
|
+
interface JavaFunctionInfo {
|
|
13
|
+
name: string;
|
|
14
|
+
type: string;
|
|
15
|
+
modifiers: string[];
|
|
16
|
+
line: number;
|
|
17
|
+
startLine: number;
|
|
18
|
+
/** Body line span, matching the `length` field long-function reads. */
|
|
19
|
+
length: number;
|
|
20
|
+
paramCount: number;
|
|
21
|
+
nestingDepth: number;
|
|
22
|
+
hasTryCatch: boolean;
|
|
23
|
+
ioOperations: string[];
|
|
24
|
+
code: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface JavaImportInfo {
|
|
28
|
+
name: string;
|
|
29
|
+
line: number;
|
|
30
|
+
used: boolean;
|
|
31
|
+
type: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Java-side I/O markers for the missing-error-handling rule. */
|
|
35
|
+
const JAVA_IO_OPERATIONS = [
|
|
36
|
+
'executeQuery',
|
|
37
|
+
'executeUpdate',
|
|
38
|
+
'executeBatch',
|
|
39
|
+
'getConnection',
|
|
40
|
+
'FileInputStream',
|
|
41
|
+
'FileOutputStream',
|
|
42
|
+
'FileReader',
|
|
43
|
+
'FileWriter',
|
|
44
|
+
'BufferedReader',
|
|
45
|
+
'BufferedWriter',
|
|
46
|
+
'Files.',
|
|
47
|
+
'HttpClient',
|
|
48
|
+
'RestTemplate',
|
|
49
|
+
'Socket',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Numeric literal scan for the magic-numbers rule. Lookahead mirrors the
|
|
54
|
+
* TypeScript adapter (`(?![\w$])`): digits as call arguments (`foo(2)`) are
|
|
55
|
+
* the most common magic-number site and must be reported, so `)` is NOT
|
|
56
|
+
* excluded. A per-call regex comes from `javaParse.magicNumberRegex()`;
|
|
57
|
+
* `String.prototype.match` with the global flag returns all matches per line
|
|
58
|
+
* and ignores lastIndex, so there is no shared-state bug to worry about.
|
|
59
|
+
* Known gaps (documented in docs/java-principles-coverage.md): hex
|
|
60
|
+
* (0xFF), underscore separators (1_000) and literal suffixes (1000L) are
|
|
61
|
+
* not matched.
|
|
62
|
+
*/
|
|
63
|
+
const MAGIC_NUMBER_SOURCE = '(?<![\\w$.])(-?\\d+(?:\\.\\d+)?)(?![\\w$])';
|
|
64
|
+
|
|
65
|
+
const MEMBER_KEYWORDS = new Set([
|
|
66
|
+
'if', 'for', 'while', 'switch', 'catch', 'return', 'synchronized', 'new',
|
|
67
|
+
'super', 'this', 'throw', 'assert', 'try'
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Pure parse helpers, kept OUTSIDE the adapter class and called through this
|
|
72
|
+
* namespace object. Two reasons, both from the gate dogfooding itself (#507
|
|
73
|
+
* S4): the srp heuristic counts every bare `word(` inside a class body as a
|
|
74
|
+
* method, and dip flags every bare `new X(` as direct instantiation -- dotted
|
|
75
|
+
* namespace calls keep both scanners blind to implementation helpers.
|
|
76
|
+
*/
|
|
77
|
+
export const javaParse = {
|
|
78
|
+
/** Top-level brace depth of a method body (deep-nesting rule). */
|
|
79
|
+
braceDepth(body: string): number {
|
|
80
|
+
let depth = 0;
|
|
81
|
+
let max = 0;
|
|
82
|
+
for (const ch of body) {
|
|
83
|
+
if (ch === '{') {
|
|
84
|
+
depth += 1;
|
|
85
|
+
if (depth > max) max = depth;
|
|
86
|
+
} else if (ch === '}') {
|
|
87
|
+
depth -= 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return max;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/** Parameter count of a raw parameter list, comma-aware at depth zero. */
|
|
94
|
+
countParams(paramList: string): number {
|
|
95
|
+
const trimmed = paramList.trim();
|
|
96
|
+
if (!trimmed) return 0;
|
|
97
|
+
let depth = 0;
|
|
98
|
+
let count = 1;
|
|
99
|
+
for (const ch of trimmed) {
|
|
100
|
+
if (ch === '(' || ch === '<' || ch === '[') depth += 1;
|
|
101
|
+
else if (ch === ')' || ch === '>' || ch === ']') depth -= 1;
|
|
102
|
+
else if (ch === ',' && depth === 0) count += 1;
|
|
103
|
+
}
|
|
104
|
+
return count;
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Method-like members inside a class/interface body: `name(` occurrences
|
|
109
|
+
* minus control keywords and constructor calls. Mirrors the TypeScript
|
|
110
|
+
* adapter's member heuristic so SRP/god-class see the same signal shape.
|
|
111
|
+
*/
|
|
112
|
+
countMembers(body: string): string[] {
|
|
113
|
+
const memberRe = /(?<![\w$.])(\w+)\s*\(/g;
|
|
114
|
+
const methods: string[] = [];
|
|
115
|
+
let m: RegExpExecArray | null;
|
|
116
|
+
while ((m = memberRe.exec(body)) !== null) {
|
|
117
|
+
const name = m[1];
|
|
118
|
+
if (!MEMBER_KEYWORDS.has(name) && !methods.includes(name)) methods.push(name);
|
|
119
|
+
}
|
|
120
|
+
return methods;
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
magicNumberRegex(): RegExp {
|
|
124
|
+
return new RegExp(MAGIC_NUMBER_SOURCE, 'g');
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Strip string/char literals and comments (line + block, multi-line)
|
|
129
|
+
* before any brace/call-site analysis. Contents become empty/space so the
|
|
130
|
+
* LINE structure (and thus line numbers) is preserved -- column positions
|
|
131
|
+
* shift, so callers must use lineOf() instead of getLineNumber() on masked
|
|
132
|
+
* content. This is what keeps a `"{` inside a format string or a javadoc
|
|
133
|
+
* code example from corrupting nestingDepth/hasTryCatch/methodCount
|
|
134
|
+
* (#507 blind review M3/M4).
|
|
135
|
+
*
|
|
136
|
+
* String/char patterns exclude newlines (#507 Delphi round-1 B-MAJOR-3):
|
|
137
|
+
* a cross-line pairing let one unbalanced quote inside a comment swallow
|
|
138
|
+
* every line up to the NEXT real string, deleting whole control blocks
|
|
139
|
+
* from the analysis. Damage is now bounded to the offending line.
|
|
140
|
+
*/
|
|
141
|
+
maskCode(body: string): string {
|
|
142
|
+
return body
|
|
143
|
+
.replace(/"(?:[^"\\\n]|\\[^\n])*"/g, '""')
|
|
144
|
+
.replace(/'(?:[^'\\\n]|\\[^\n])'/g, "''")
|
|
145
|
+
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '))
|
|
146
|
+
.replace(/\/\/.*$/gm, '');
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Line number of `position` within `content`. maskCode preserves line
|
|
151
|
+
* structure (contents shrink but never add/remove lines), so a line
|
|
152
|
+
* computed on masked content is exact for the original file.
|
|
153
|
+
*/
|
|
154
|
+
lineOf(content: string, position: number): number {
|
|
155
|
+
return content.substring(0, position).split('\n').length;
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Brace-balanced block starting at the first `{` at/after `startPos`.
|
|
160
|
+
* Runs on MASKED content only -- on raw text a string-embedded `{`
|
|
161
|
+
* desynchronizes the depth tracker. Returns null when unbalanced
|
|
162
|
+
* (callers decide the fallback; base.extractCodeBlock's 100-char
|
|
163
|
+
* truncation fallback would silently hand back garbage structure).
|
|
164
|
+
*/
|
|
165
|
+
extractBracedBlock(content: string, startPos: number): string | null {
|
|
166
|
+
let inBlock = false;
|
|
167
|
+
let depth = 0;
|
|
168
|
+
for (let i = startPos; i < content.length; i += 1) {
|
|
169
|
+
const ch = content[i];
|
|
170
|
+
if (ch === '{') {
|
|
171
|
+
inBlock = true;
|
|
172
|
+
depth += 1;
|
|
173
|
+
} else if (ch === '}' && inBlock) {
|
|
174
|
+
depth -= 1;
|
|
175
|
+
if (depth === 0) {
|
|
176
|
+
return content.substring(startPos, i + 1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
/** Usage detector for an imported simple name (word-boundary, $-escaped). */
|
|
184
|
+
importUsage(simpleName: string): RegExp {
|
|
185
|
+
return new RegExp(`\\b${simpleName.replace(/\$/g, '\\$')}\\b`);
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
4
189
|
export class JavaAdapter extends BaseAdapter implements Adapter {
|
|
5
190
|
detectLanguage(): string {
|
|
6
191
|
const ext = this.filePath.toLowerCase();
|
|
@@ -19,38 +204,165 @@ export class JavaAdapter extends BaseAdapter implements Adapter {
|
|
|
19
204
|
}
|
|
20
205
|
|
|
21
206
|
extractFunctions(): unknown[] {
|
|
22
|
-
const
|
|
23
|
-
const methodRegex = /(public|private|protected|static|final|synchronized|native)\s+[\w<>[\]]
|
|
207
|
+
const results: JavaFunctionInfo[] = [];
|
|
208
|
+
const methodRegex = /(public|private|protected|static|final|synchronized|native)\s+[\w<>[\],\s]+?\s+(\w+)\s*\(([^)]*)\)\s*(throws\s+[\w,\s.]+)?\s*\{/g;
|
|
209
|
+
// Signature scan AND block extraction run on the MASKED content
|
|
210
|
+
// (#507 Delphi round-1 B-MAJOR-6 / A-minor-1): a javadoc example like
|
|
211
|
+
// `public int foo(int x) {` on raw text produced a ghost function whose
|
|
212
|
+
// length/nestingDepth fed long-function/deep-nesting, and an unbalanced
|
|
213
|
+
// string brace stretched the extracted block past the real method end.
|
|
214
|
+
// Masked content keeps line structure, so lineOf() is exact.
|
|
215
|
+
const maskedContent = javaParse.maskCode(this.fileContent);
|
|
24
216
|
let match;
|
|
25
217
|
|
|
26
|
-
while ((match = methodRegex.exec(
|
|
27
|
-
|
|
218
|
+
while ((match = methodRegex.exec(maskedContent)) !== null) {
|
|
219
|
+
const code = javaParse.extractBracedBlock(maskedContent, match.index) ?? '';
|
|
220
|
+
const line = javaParse.lineOf(maskedContent, match.index);
|
|
221
|
+
const masked = code; // already masked (extracted from maskedContent)
|
|
222
|
+
results.push({
|
|
28
223
|
name: match[2],
|
|
29
224
|
type: 'method',
|
|
30
|
-
modifiers: match[1].split(
|
|
31
|
-
line
|
|
32
|
-
|
|
225
|
+
modifiers: match[1].split(/\s+/).filter(m => m.trim()),
|
|
226
|
+
line,
|
|
227
|
+
startLine: line,
|
|
228
|
+
length: code ? code.split('\n').length : 0,
|
|
229
|
+
paramCount: javaParse.countParams(match[3] ?? ''),
|
|
230
|
+
nestingDepth: javaParse.braceDepth(masked),
|
|
231
|
+
hasTryCatch: /catch\s*\(/.test(masked),
|
|
232
|
+
ioOperations: JAVA_IO_OPERATIONS.filter(op => masked.includes(op)),
|
|
233
|
+
code
|
|
33
234
|
});
|
|
34
235
|
}
|
|
35
236
|
|
|
36
|
-
return
|
|
237
|
+
return results;
|
|
37
238
|
}
|
|
38
239
|
|
|
39
240
|
extractClasses(): unknown[] {
|
|
40
241
|
const classMatches = [];
|
|
41
|
-
|
|
242
|
+
// Generic type parameters -- `class Box<T>`, `class Foo<T> extends Bar<T>` --
|
|
243
|
+
// are the norm in modern Java; without the optional `<...>` slot the
|
|
244
|
+
// whole class vanished from extractClasses and srp/god-class/dip went
|
|
245
|
+
// blind on generic code -- #507 blind review M1.
|
|
246
|
+
const classRegex = /(public|private|protected)?\s*(abstract|final|static)?\s*class\s+(\w+)\s*(?:<[^<>]*>)?\s*(extends\s+[\w.]+(?:<[^<>]*>)?)?\s*(implements\s+[\w,\s.]+(?:<[^<>]*>)?)?\s*\{/g;
|
|
42
247
|
let match;
|
|
43
248
|
|
|
44
|
-
|
|
249
|
+
// Class scan also runs on masked content: a javadoc mention of `class
|
|
250
|
+
// Foo` no longer yields a ghost class, and string-embedded braces cannot
|
|
251
|
+
// stretch the extracted body (#507 Delphi round-1 B-MAJOR-6).
|
|
252
|
+
const maskedContent = javaParse.maskCode(this.fileContent);
|
|
253
|
+
while ((match = classRegex.exec(maskedContent)) !== null) {
|
|
254
|
+
const code = javaParse.extractBracedBlock(maskedContent, match.index) ?? '';
|
|
255
|
+
// srp reads `cls.methodCount` directly, with no code re-scan fallback,
|
|
256
|
+
// so an adapter omitting it makes SRP structurally dead on Java -- #507 S4.
|
|
257
|
+
const methods = javaParse.countMembers(code);
|
|
45
258
|
classMatches.push({
|
|
46
259
|
name: match[3],
|
|
47
260
|
type: 'class',
|
|
48
261
|
modifiers: [match[1], match[2]].filter(m => m && m.trim()),
|
|
49
|
-
line:
|
|
50
|
-
|
|
262
|
+
line: javaParse.lineOf(maskedContent, match.index),
|
|
263
|
+
methodCount: methods.length,
|
|
264
|
+
methods,
|
|
265
|
+
code
|
|
51
266
|
});
|
|
52
267
|
}
|
|
53
268
|
|
|
54
269
|
return classMatches;
|
|
55
270
|
}
|
|
56
|
-
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Interfaces for the ISP rule, which reads `adapter.extractInterfaces()`.
|
|
274
|
+
* Java interface methods are bare signature lines, counted with the same
|
|
275
|
+
* member heuristic as classes minus constructors.
|
|
276
|
+
*/
|
|
277
|
+
extractInterfaces(): Array<{ name: string; line: number; methods: string[]; methodCount: number; code: string }> {
|
|
278
|
+
const results: Array<{ name: string; line: number; methods: string[]; methodCount: number; code: string }> = [];
|
|
279
|
+
const ifaceRegex = /(?:public|private|protected)?\s*interface\s+(\w+)\s*(?:<[^<>]*>)?\s*\{/g;
|
|
280
|
+
let match;
|
|
281
|
+
|
|
282
|
+
// Masked scan, same rationale as extractClasses (#507 Delphi round-1
|
|
283
|
+
// B-MAJOR-6): no ghost interfaces from javadoc, no string-brace bleed.
|
|
284
|
+
const maskedContent = javaParse.maskCode(this.fileContent);
|
|
285
|
+
while ((match = ifaceRegex.exec(maskedContent)) !== null) {
|
|
286
|
+
const code = javaParse.extractBracedBlock(maskedContent, match.index) ?? '';
|
|
287
|
+
const methods = javaParse.countMembers(code);
|
|
288
|
+
results.push({
|
|
289
|
+
name: match[1],
|
|
290
|
+
line: javaParse.lineOf(maskedContent, match.index),
|
|
291
|
+
methods,
|
|
292
|
+
methodCount: methods.length,
|
|
293
|
+
code
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return results;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Numeric literal scan for the magic-numbers rule. Constant declarations
|
|
302
|
+
* (`static final ... = <n>`) are skipped: naming a constant is exactly the
|
|
303
|
+
* remedy the rule asks for. Masking ORDER matters -- #507 blind review M3:
|
|
304
|
+
* string/char literals first, so `"http://x"` cannot swallow the rest of
|
|
305
|
+
* the line as a comment), then line comments, then block comments with
|
|
306
|
+
* cross-line state -- a commented-out code block must not resurrect its
|
|
307
|
+
* numbers as findings.
|
|
308
|
+
*/
|
|
309
|
+
extract(): Array<{ value: number; line: number }> {
|
|
310
|
+
const out: Array<{ value: number; line: number }> = [];
|
|
311
|
+
const lines = this.fileContent.split('\n');
|
|
312
|
+
const numRegex = javaParse.magicNumberRegex();
|
|
313
|
+
let inBlockComment = false;
|
|
314
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
315
|
+
let line = lines[i];
|
|
316
|
+
if (inBlockComment) {
|
|
317
|
+
const end = line.indexOf('*/');
|
|
318
|
+
if (end === -1) continue;
|
|
319
|
+
line = line.slice(end + 2);
|
|
320
|
+
inBlockComment = false;
|
|
321
|
+
}
|
|
322
|
+
line = line
|
|
323
|
+
.replace(/"(?:[^"\\]|\\.)*"/g, '""')
|
|
324
|
+
.replace(/'(?:[^'\\]|\\.)'/g, "''")
|
|
325
|
+
.replace(/\/\/.*$/, '');
|
|
326
|
+
while (line.includes('/*')) {
|
|
327
|
+
const start = line.indexOf('/*');
|
|
328
|
+
const end = line.indexOf('*/', start + 2);
|
|
329
|
+
if (end === -1) {
|
|
330
|
+
line = line.slice(0, start);
|
|
331
|
+
inBlockComment = true;
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
line = line.slice(0, start) + ' ' + line.slice(end + 2);
|
|
335
|
+
}
|
|
336
|
+
if (/^\s*(?:public|private|protected)?\s*static\s+final\s/.test(line)) continue;
|
|
337
|
+
const matches = line.match(numRegex) ?? [];
|
|
338
|
+
for (const value of matches) out.push({ value: Number(value), line: i + 1 });
|
|
339
|
+
}
|
|
340
|
+
return out;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Import statements with a usage signal for the unused-imports rule. Usage
|
|
345
|
+
* is a conservative check: the imported simple name must appear somewhere
|
|
346
|
+
* outside the import block itself.
|
|
347
|
+
*/
|
|
348
|
+
get imports(): JavaImportInfo[] {
|
|
349
|
+
const lines = this.fileContent.split('\n');
|
|
350
|
+
const imports: JavaImportInfo[] = [];
|
|
351
|
+
const importRegex = /^\s*import\s+(?:static\s+)?([\w.]+)\s*;\s*$/;
|
|
352
|
+
const importLines = new Set<number>();
|
|
353
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
354
|
+
const m = importRegex.exec(lines[i]);
|
|
355
|
+
if (m) {
|
|
356
|
+
imports.push({ name: m[1], line: i + 1, used: false, type: 'import' });
|
|
357
|
+
importLines.add(i);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (imports.length === 0) return imports;
|
|
361
|
+
const body = lines.filter((_, idx) => !importLines.has(idx)).join('\n');
|
|
362
|
+
for (const imp of imports) {
|
|
363
|
+
const simpleName = imp.name.split('.').pop() ?? imp.name;
|
|
364
|
+
imp.used = javaParse.importUsage(simpleName).test(body);
|
|
365
|
+
}
|
|
366
|
+
return imports;
|
|
367
|
+
}
|
|
368
|
+
}
|