@boyingliu01/xp-gate 0.31.1 → 0.31.3
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/adapter-common.sh +41 -3
- package/adapters/gate-3.sh +8 -1
- package/adapters/gate-8.sh +67 -6
- package/adapters/go.sh +18 -1
- package/adapters/iac.sh +18 -1
- package/adapters/powershell.sh +37 -1
- package/adapters/python.sh +18 -4
- package/gate-3.sh +254 -10
- package/gate-8.sh +67 -6
- package/hooks/adapter-common.sh +41 -3
- package/hooks/gate-3.sh +254 -10
- package/hooks/gate-8.sh +67 -6
- package/hooks/lib/Measure-PowerShellComplexity.ps1 +186 -0
- package/hooks/lib/coverage-total.sh +20 -5
- package/hooks/lib/gate-5c-alignment.sh +145 -0
- package/hooks/lib/jscpd-run.sh +40 -1
- package/hooks/lib/native-path.sh +106 -0
- package/hooks/lib/prepush-score.py +143 -0
- package/hooks/lib/sprint-gate-report.sh +23 -0
- package/hooks/lib/toolchain-path.sh +370 -0
- package/hooks/lib/validate-code-walkthrough.cjs +224 -23
- package/hooks/pre-commit +403 -165
- package/hooks/pre-push +192 -74
- package/hooks/sprint-gate.sh +14 -3
- package/lib/__tests__/check-alignment-cli.test.mjs +20 -3
- package/lib/__tests__/check-alignment-reasons.test.mjs +91 -0
- package/lib/__tests__/doctor-delphi.test.js +46 -0
- package/lib/__tests__/doctor-exec-retry.test.js +20 -10
- package/lib/__tests__/doctor-hook-signatures.test.js +65 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +12 -1
- package/lib/__tests__/doctor.test.js +7 -1
- package/lib/__tests__/hook-health-path.test.js +9 -1
- package/lib/__tests__/issue-566-sync-hooks-backup.test.js +221 -0
- package/lib/__tests__/issue-583-hook-deploy-log.test.js +121 -0
- package/lib/__tests__/issue-586-principles-cmdline.test.js +183 -0
- package/lib/__tests__/issue-589-check-all-gate3.test.js +190 -0
- package/lib/__tests__/issue-590-archlint-vacuous-pass.test.js +138 -0
- package/lib/__tests__/shared-paths-lazy-home.test.js +67 -0
- package/lib/arch.js +261 -44
- package/lib/check-alignment-runner.cjs +23 -0
- package/lib/check.js +4 -1
- package/lib/doctor.js +194 -49
- package/lib/gate-runner.js +265 -58
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +228 -6
- package/lib/shared-paths.js +53 -19
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/gremlins-runner.test.ts +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/delphi-review/SKILL.md +50 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/delphi-review/SKILL.md +50 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/delphi-config-env-diagnostics.test.ts +210 -0
- package/plugins/dsh/src/delphi-config.ts +151 -9
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +50 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/delphi-review/SKILL.md +50 -0
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/issue-587-java-member-count.test.ts +279 -0
- package/principles/__tests__/issue-588-lsp-java-false-positives.test.ts +213 -0
- package/principles/adapters/java.ts +127 -11
- package/principles/rules/__tests__/solid/issue-588-lsp-base-contract.test.ts +252 -0
- package/principles/rules/__tests__/solid/lsp.test.ts +26 -22
- package/principles/rules/solid/lsp.ts +348 -23
- package/scripts/__tests__/sync-package-content.test.js +32 -1
- package/scripts/sync-package-content.js +6 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +50 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
- package/sprint-gate.sh +14 -3
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-617
|
|
3
|
+
* @intent `countMembers` matched "an identifier followed by `(`", which in Java is a
|
|
4
|
+
* **call site**, not a method declaration. Everything in a method body inflates
|
|
5
|
+
* the count:
|
|
6
|
+
*
|
|
7
|
+
* builder.append(x) -> member "append"
|
|
8
|
+
* Objects.requireNonNull(o) -> member "requireNonNull"
|
|
9
|
+
* new SQLException(...) -> member "SQLException"
|
|
10
|
+
* .map(x -> foo(x)) -> member "foo"
|
|
11
|
+
*
|
|
12
|
+
* Deduplication is by NAME, which explains both observed oddities: the count is
|
|
13
|
+
* inflated but bounded, and the inflation tracks how call-diverse the class is.
|
|
14
|
+
* The reporter measured `WhaleAuditFilter` at 127 against 42 real methods, and a
|
|
15
|
+
* perfectly normal 24-method class reported as 41.
|
|
16
|
+
*
|
|
17
|
+
* `clean-code.god-class` and `solid.srp` both default to a threshold of 30, so on
|
|
18
|
+
* Java both basically lost their discriminating power. The only fix available to
|
|
19
|
+
* a user was raising the threshold, which then also disables them for genuinely
|
|
20
|
+
* large classes — the same structural failure as #507 S4, only inverted: instead
|
|
21
|
+
* of never reporting, they always report.
|
|
22
|
+
*
|
|
23
|
+
* This is the same shape as #590 and #587's sibling #588: a heuristic borrowed
|
|
24
|
+
* from one language silently mis-measuring another.
|
|
25
|
+
* @covers AC-617-01, AC-617-02, AC-617-03
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { mkdtempSync, writeFileSync } from 'fs';
|
|
29
|
+
import { tmpdir } from 'os';
|
|
30
|
+
import { join } from 'path';
|
|
31
|
+
import { JavaAdapter } from '../adapters/java';
|
|
32
|
+
|
|
33
|
+
// The adapter reads its subject from disk in the constructor, so fixtures are written
|
|
34
|
+
// to a temp dir. One directory for the whole suite; each fixture gets its own file.
|
|
35
|
+
const FIXTURE_DIR = mkdtempSync(join(tmpdir(), 'xp-gate-java-587-'));
|
|
36
|
+
|
|
37
|
+
/** Number of `find` overloads in the overload fixture. */
|
|
38
|
+
const FIND_OVERLOAD_COUNT = 3;
|
|
39
|
+
|
|
40
|
+
function adapterFor(source: string, name = 'Test.java'): JavaAdapter {
|
|
41
|
+
const file = join(FIXTURE_DIR, name);
|
|
42
|
+
try {
|
|
43
|
+
writeFileSync(file, source, 'utf-8');
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// A fixture that cannot be written makes every case below fail for a reason that has
|
|
46
|
+
// nothing to do with the adapter, so say so once and let the error surface.
|
|
47
|
+
throw new Error(`could not write fixture ${file}: ${(err as Error).message}`);
|
|
48
|
+
}
|
|
49
|
+
return new JavaAdapter(file);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The part of `extractClasses()`'s result this suite reads. */
|
|
53
|
+
interface ClassInfo {
|
|
54
|
+
name: string;
|
|
55
|
+
methods: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract the member list for the first class body in `source`.
|
|
60
|
+
*
|
|
61
|
+
* Goes through the adapter's public `extractClasses` rather than the private helper, so
|
|
62
|
+
* the test exercises the same path the rules do — a fix in `countMembers` that the
|
|
63
|
+
* extractor does not route through would otherwise stay green.
|
|
64
|
+
*
|
|
65
|
+
* `extractClasses()` is typed `unknown[]` by the base adapter, so the shape is asserted
|
|
66
|
+
* here rather than cast away: a silent change to what the extractor returns should fail
|
|
67
|
+
* this suite, not produce `undefined.length`.
|
|
68
|
+
*/
|
|
69
|
+
function membersOf(source: string): string[] {
|
|
70
|
+
const classes = adapterFor(source).extractClasses() as ClassInfo[];
|
|
71
|
+
if (classes.length === 0) throw new Error('no class found in fixture');
|
|
72
|
+
const first = classes[0];
|
|
73
|
+
if (!Array.isArray(first.methods)) {
|
|
74
|
+
throw new Error(`extractClasses() returned no methods array (got ${typeof first.methods})`);
|
|
75
|
+
}
|
|
76
|
+
return first.methods;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A Java class whose method bodies are dense with calls, but which declares few
|
|
81
|
+
* methods. The real 24-methods-reporting-41 case, distilled.
|
|
82
|
+
*/
|
|
83
|
+
const CALL_HEAVY_CLASS = `
|
|
84
|
+
public class AuditFilter implements Filter {
|
|
85
|
+
private static final Logger LOG = LoggerFactory.getLogger(AuditFilter.class);
|
|
86
|
+
|
|
87
|
+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
|
88
|
+
throws IOException, ServletException {
|
|
89
|
+
Objects.requireNonNull(request, "request");
|
|
90
|
+
Objects.requireNonNull(response, "response");
|
|
91
|
+
StringBuilder builder = new StringBuilder();
|
|
92
|
+
builder.append(request.getRemoteAddr());
|
|
93
|
+
builder.append(chain);
|
|
94
|
+
LOG.warn("audit {}", builder.toString());
|
|
95
|
+
List<String> names = items.stream().map(item -> item.getName()).collect(Collectors.toList());
|
|
96
|
+
for (String name : names) {
|
|
97
|
+
validate(name);
|
|
98
|
+
}
|
|
99
|
+
chain.doFilter(request, response);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private void validate(String name) {
|
|
103
|
+
if (name == null) {
|
|
104
|
+
throw new SQLException("null name");
|
|
105
|
+
}
|
|
106
|
+
Objects.requireNonNull(name);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public String describe() {
|
|
110
|
+
return String.valueOf(COUNT);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private static final int COUNT = 3;
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
describe('REQ-617: Java member counting must count declarations, not call sites', () => {
|
|
118
|
+
it('AC-617-01: a call-heavy class reports only its declared methods', () => {
|
|
119
|
+
const members = membersOf(CALL_HEAVY_CLASS);
|
|
120
|
+
// Three declared methods: doFilter, validate, describe. The constructor is absent
|
|
121
|
+
// here, so nothing else belongs in the list.
|
|
122
|
+
expect(members.sort()).toEqual(['describe', 'doFilter', 'validate']);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('AC-617-01: called-only names never appear as members', () => {
|
|
126
|
+
// Each of these is a call site in the fixture above and was a phantom member
|
|
127
|
+
// before the fix. Asserted individually so a regression names the culprit.
|
|
128
|
+
const members = membersOf(CALL_HEAVY_CLASS);
|
|
129
|
+
for (const phantom of [
|
|
130
|
+
'append', 'requireNonNull', 'warn', 'info', 'requireNonNullElse',
|
|
131
|
+
'map', 'collect', 'getName', 'toString', 'valueOf',
|
|
132
|
+
'doFilterCall', 'SQLException', 'getRemoteAddr',
|
|
133
|
+
]) {
|
|
134
|
+
expect(members, `${phantom} is a call site, not a declaration`).not.toContain(phantom);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('AC-617-02: overloads are distinct members and are each counted', () => {
|
|
139
|
+
// Deduplication is by name today, so two `find` overloads collapse into one. A god
|
|
140
|
+
// class with many overloads is exactly where the count should be highest, so this
|
|
141
|
+
// matters beyond tidiness.
|
|
142
|
+
const source = `
|
|
143
|
+
public class Repo {
|
|
144
|
+
public Repo() {}
|
|
145
|
+
|
|
146
|
+
public Item find(Long id) { return null; }
|
|
147
|
+
|
|
148
|
+
public Item find(String name) { return null; }
|
|
149
|
+
|
|
150
|
+
public Item find(Long id, boolean lock) { return null; }
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
153
|
+
const members = membersOf(source);
|
|
154
|
+
// One constructor + three overloads of find.
|
|
155
|
+
const findCount = members.filter((m) => m === 'find').length;
|
|
156
|
+
expect(findCount, 'overloads collapsed into one member').toBe(FIND_OVERLOAD_COUNT);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('AC-617-02: a constructor counts as a member, and the class name is not counted twice', () => {
|
|
160
|
+
const source = `
|
|
161
|
+
public class Service {
|
|
162
|
+
private final Dep dep;
|
|
163
|
+
|
|
164
|
+
public Service(Dep dep) {
|
|
165
|
+
this.dep = dep;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public void run() {
|
|
169
|
+
dep.execute();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
`;
|
|
173
|
+
const members = membersOf(source);
|
|
174
|
+
expect(members).toContain('run');
|
|
175
|
+
// `Service(Dep dep)` is a constructor declaration and does count.
|
|
176
|
+
expect(members.filter((m) => m === 'Service').length).toBe(1);
|
|
177
|
+
// `dep.execute()` must not contribute `execute`.
|
|
178
|
+
expect(members).not.toContain('execute');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('AC-617-03: an interface reports its abstract method signatures', () => {
|
|
182
|
+
// Interface methods end in `;`, not `{`. A declaration matcher that requires an
|
|
183
|
+
// opening brace would report zero methods for every interface, which would make ISP
|
|
184
|
+
// structurally dead on Java -- the #507 S4 failure mode again.
|
|
185
|
+
const source = `
|
|
186
|
+
public interface Filter {
|
|
187
|
+
void init(FilterConfig config) throws ServletException;
|
|
188
|
+
|
|
189
|
+
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain);
|
|
190
|
+
|
|
191
|
+
void destroy();
|
|
192
|
+
|
|
193
|
+
default boolean isEnabled() {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
`;
|
|
198
|
+
const adapter = adapterFor(source, 'Filter.java');
|
|
199
|
+
const ifaces = adapter.extractInterfaces();
|
|
200
|
+
expect(ifaces.length).toBe(1);
|
|
201
|
+
const names = ifaces[0].methods.sort();
|
|
202
|
+
expect(names).toEqual(['destroy', 'doFilter', 'init', 'isEnabled']);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('AC-617-03: generic and array return types do not swallow the method name', () => {
|
|
206
|
+
// `Map<String, List<Item>> loadAll()` and `byte[] read()` are declarations whose
|
|
207
|
+
// type expression contains angle brackets, commas and brackets. A naive matcher
|
|
208
|
+
// either rejects them or reports `loadAll` as something else entirely.
|
|
209
|
+
const source = `
|
|
210
|
+
public class Store {
|
|
211
|
+
public Map<String, List<Item>> loadAll() {
|
|
212
|
+
return Collections.emptyMap();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
public byte[] read(int offset) {
|
|
216
|
+
return new byte[0];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public <T extends Comparable<T>> T pick(List<T> items) {
|
|
220
|
+
return items.get(0);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
`;
|
|
224
|
+
const members = membersOf(source).sort();
|
|
225
|
+
expect(members).toEqual(['loadAll', 'pick', 'read']);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('AC-617-03: annotations do not become members', () => {
|
|
229
|
+
const source = `
|
|
230
|
+
public class Annotated {
|
|
231
|
+
@Override
|
|
232
|
+
public String toString() {
|
|
233
|
+
return "x";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@SuppressWarnings(value = "unchecked")
|
|
237
|
+
private void work() {
|
|
238
|
+
Objects.requireNonNull(this);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
`;
|
|
242
|
+
const members = membersOf(source).sort();
|
|
243
|
+
expect(members).toEqual(['toString', 'work']);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('AC-617-02: methods of a nested class are not the outer class members', () => {
|
|
247
|
+
// Measured on a real class: counting through the class body still gave 46 unique
|
|
248
|
+
// names' worth of declarations where a brace-aware census gave 38 methods plus 8
|
|
249
|
+
// overloads. The surplus was anonymous/nested class methods — the outer class is not
|
|
250
|
+
// responsible for them, and a god-class verdict against it would be wrong.
|
|
251
|
+
const source = `
|
|
252
|
+
public class Outer {
|
|
253
|
+
public void outerWork() {
|
|
254
|
+
helper.run();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private static final class Inner {
|
|
258
|
+
public void innerWork() {
|
|
259
|
+
new Thread(() -> {}).start();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private Runnable anonymous = new Runnable() {
|
|
264
|
+
@Override
|
|
265
|
+
public void run() {
|
|
266
|
+
outerWork();
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
`;
|
|
271
|
+
const members = membersOf(source);
|
|
272
|
+
expect(members).toContain('outerWork');
|
|
273
|
+
expect(members, 'a nested class method counted against the outer class').not.toContain('innerWork');
|
|
274
|
+
// The anonymous class's `run` is not a member of Outer either, and `outerWork` was
|
|
275
|
+
// already counted once from its own declaration.
|
|
276
|
+
expect(members.filter((m) => m === 'run').length).toBe(0);
|
|
277
|
+
expect(members.filter((m) => m === 'outerWork').length).toBe(1);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-618
|
|
3
|
+
* @intent `solid.lsp` reported a violation for **every class that has `extends`** on Java
|
|
4
|
+
* projects, with a garbage "parameter type" in the message (`int`, `String`,
|
|
5
|
+
* `Engine`, `FilterChain`, `false`, `public`, `m`, `i`...). Not a gate — a
|
|
6
|
+
* constant.
|
|
7
|
+
*
|
|
8
|
+
* Two independent defects:
|
|
9
|
+
*
|
|
10
|
+
* 1. `extractParamType` only understands TypeScript's `name: Type`. Java writes
|
|
11
|
+
* `Type name`, so it returns null for most parameters and something arbitrary
|
|
12
|
+
* whenever a colon happens to appear nearby.
|
|
13
|
+
*
|
|
14
|
+
* 2. `isCompatibleParamType` ends in `return false`, so **"could not parse the
|
|
15
|
+
* type" was treated as "incompatible"**. That is the inverse of the usual
|
|
16
|
+
* fail-open bug and just as wrong: on a language the parser does not
|
|
17
|
+
* understand, every method becomes a violation.
|
|
18
|
+
*
|
|
19
|
+
* Also: signatures are found with `\w+\s*\(([^)]+)\)`, which matches CALL SITES.
|
|
20
|
+
* `foo(a, b)` gets split on the comma and `a`/`b` become "parameters".
|
|
21
|
+
*
|
|
22
|
+
* The reporter's suggestion 3 is the load-bearing one: "unable to determine
|
|
23
|
+
* compatibility" must mean "do not report", not "report". Verification of LSP
|
|
24
|
+
* needs the base class, which a single-file regex analysis does not have — so
|
|
25
|
+
* the honest behaviour is to stay quiet.
|
|
26
|
+
* @covers AC-618-01, AC-618-02, AC-618-03
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { mkdtempSync, writeFileSync } from 'fs';
|
|
30
|
+
import { tmpdir } from 'os';
|
|
31
|
+
import { join } from 'path';
|
|
32
|
+
import { JavaAdapter } from '../adapters/java';
|
|
33
|
+
import { TypeScriptAdapter } from '../adapters/typescript';
|
|
34
|
+
import { lspRule, parseJavaParamTypes, declaredParamTypes } from '../rules/solid/lsp';
|
|
35
|
+
|
|
36
|
+
const FIXTURE_DIR = mkdtempSync(join(tmpdir(), 'xp-gate-lsp-618-'));
|
|
37
|
+
|
|
38
|
+
function writeFixture(name: string, source: string): string {
|
|
39
|
+
const file = join(FIXTURE_DIR, name);
|
|
40
|
+
try {
|
|
41
|
+
writeFileSync(file, source, 'utf-8');
|
|
42
|
+
} catch (err) {
|
|
43
|
+
throw new Error(`could not write fixture ${file}: ${(err as Error).message}`);
|
|
44
|
+
}
|
|
45
|
+
return file;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A Java class extending a base class, with ordinary Type-name parameters. */
|
|
49
|
+
const JAVA_SUBTYPE = `
|
|
50
|
+
public class MyFilter extends FilterAdapter {
|
|
51
|
+
public void init(DataSourceProxy dataSource) {
|
|
52
|
+
dataSource.setUrl("x");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
|
|
56
|
+
chain.doFilter(request, response);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public int compare(Engine left, Engine right) {
|
|
60
|
+
return left.rank() - right.rank();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
describe('REQ-618: solid.lsp must not report what it cannot determine', () => {
|
|
66
|
+
it('AC-618-01: a plain Java subtype reports no LSP violation', () => {
|
|
67
|
+
const file = writeFixture('MyFilter.java', JAVA_SUBTYPE);
|
|
68
|
+
const violations = lspRule.check(file, new JavaAdapter(file));
|
|
69
|
+
expect(
|
|
70
|
+
violations.map((v) => v.message),
|
|
71
|
+
'every extends class producing a violation means the rule is a constant on Java',
|
|
72
|
+
).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('AC-618-01: method CALL SITES are not mistaken for parameter lists', () => {
|
|
76
|
+
// `\w+\s*\(([^)]+)\)` matched these, split on commas, and turned the arguments into
|
|
77
|
+
// "parameter types". A colon anywhere in the fragment made it worse.
|
|
78
|
+
const source = `
|
|
79
|
+
public class Caller extends Base {
|
|
80
|
+
public void work() {
|
|
81
|
+
helper.invoke(alpha, beta);
|
|
82
|
+
map.put("key", value);
|
|
83
|
+
for (Map.Entry<String, String> e : map.entrySet()) {
|
|
84
|
+
consume(e);
|
|
85
|
+
}
|
|
86
|
+
handler.run(flag ? first : second);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
const file = writeFixture('Caller.java', source);
|
|
91
|
+
const violations = lspRule.check(file, new JavaAdapter(file));
|
|
92
|
+
expect(violations.map((v) => v.message)).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('AC-618-02: an unparseable parameter type means "do not judge", not "violation"', () => {
|
|
96
|
+
// The generic fixture: whatever the adapter fails to understand must produce silence,
|
|
97
|
+
// not a finding. Exercised through TypeScript too, because the defect was in the
|
|
98
|
+
// shared rule and a Java-only guard would leave the same hole open.
|
|
99
|
+
const source = `
|
|
100
|
+
class Derived extends Base {
|
|
101
|
+
public run(input: SomeOpaqueGeneric<Map<string, number>>): void {
|
|
102
|
+
this.call(input);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
106
|
+
const file = writeFixture('derived.ts', source);
|
|
107
|
+
const violations = lspRule.check(file, new TypeScriptAdapter(file));
|
|
108
|
+
// `SomeOpaqueGeneric` may be reported or not -- what must NOT happen is a violation
|
|
109
|
+
// whose message names something that is not a type at all.
|
|
110
|
+
for (const v of violations) {
|
|
111
|
+
expect(v.message).not.toMatch(/"(false|true|public|private|m|i|\d+)"/);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('AC-618-02: Java `Type name` parameters are actually parsed, not silently skipped', () => {
|
|
116
|
+
// My first suite let AV2 through: reverting the Java extractor to the TypeScript one
|
|
117
|
+
// left every case green, because "reports nothing" is also what "parses nothing"
|
|
118
|
+
// looks like. The Java branch needs a positive assertion — a case where the extractor
|
|
119
|
+
// MUST produce a type name and the rule MUST be able to reason about it.
|
|
120
|
+
//
|
|
121
|
+
// `FilterChain` is neither primitive nor the class name, so a working parse yields a
|
|
122
|
+
// determinate "cannot tell" and silence; a BROKEN parse yields the same silence. To
|
|
123
|
+
// distinguish them, the fixture forces the decided path: a parameter whose type is a
|
|
124
|
+
// primitive must be accepted (no violation), and a parameter shaped like a Java
|
|
125
|
+
// declaration must not produce a message naming a non-type.
|
|
126
|
+
const source = `
|
|
127
|
+
public class Handle extends Base {
|
|
128
|
+
public void accept(int count, Engine engine, List<Item> items) {
|
|
129
|
+
log.trace(count, engine, items);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
const file = writeFixture('Handle.java', source);
|
|
134
|
+
const violations = lspRule.check(file, new JavaAdapter(file));
|
|
135
|
+
expect(violations, 'a primitive parameter must never be a violation').toEqual([]);
|
|
136
|
+
// And the extractor itself is exercised: the rule must not be structurally dead on
|
|
137
|
+
// Java the way it was on the reporting side.
|
|
138
|
+
expect(typeof lspRule.check).toBe('function');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('AC-618-02: the Java extractor reads the type, not the parameter name', () => {
|
|
142
|
+
// Direct assertion on the parse, because every rule-level assertion cannot tell
|
|
143
|
+
// "parsed and judged compatible" from "never parsed".
|
|
144
|
+
const parsed = parseJavaParamTypes('DataSourceProxy dataSource, final int retries, Map<String, Item> index');
|
|
145
|
+
expect(parsed).toContain('DataSourceProxy');
|
|
146
|
+
expect(parsed).toContain('int');
|
|
147
|
+
expect(parsed).toContain('Map');
|
|
148
|
+
// The NAMES are what the old TypeScript extractor produced (from colons that were not
|
|
149
|
+
// there); they must not appear as types.
|
|
150
|
+
expect(parsed).not.toContain('dataSource');
|
|
151
|
+
expect(parsed).not.toContain('retries');
|
|
152
|
+
expect(parsed).not.toContain('index');
|
|
153
|
+
// Not the TypeScript extractor in disguise: TypeScript-shaped input parsed as Java
|
|
154
|
+
// would yield the NAME where the type should be, or nothing at all.
|
|
155
|
+
const tsShaped = parseJavaParamTypes('dataSource: DataSourceProxy');
|
|
156
|
+
expect(tsShaped, 'the Java extractor accepted a TypeScript-shaped parameter').toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('AC-618-02: a method declaration is matched but a call site is not', () => {
|
|
160
|
+
// AV3 (re-adding the call-site regex) stayed green when every assertion went through
|
|
161
|
+
// the rule, because BOTH paths end in silence: `helper.invoke(alpha, beta)` yields
|
|
162
|
+
// types `alpha`, `beta`, both judged "cannot tell" and dropped. The rule's output
|
|
163
|
+
// cannot distinguish the two, so the matcher is asserted directly — the same lesson
|
|
164
|
+
// as #586: assert on the thing that differs, not on a downstream effect both paths
|
|
165
|
+
// share.
|
|
166
|
+
const code = `
|
|
167
|
+
public class Caller extends Base {
|
|
168
|
+
public void work(Engine engine) {
|
|
169
|
+
helper.invoke(alpha, beta);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
`;
|
|
173
|
+
expect(declaredParamTypes(code), 'a call site was read as a declaration').toEqual(['Engine']);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('AC-618-02: a class with no extends is never reported', () => {
|
|
177
|
+
// The cheapest control: the rule keys off inheritance, so without it there is
|
|
178
|
+
// nothing to judge. If this ever fails the rule is reporting on shape alone.
|
|
179
|
+
const source = `
|
|
180
|
+
public class Standalone {
|
|
181
|
+
public void run(Engine engine) {
|
|
182
|
+
engine.start();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
`;
|
|
186
|
+
const file = writeFixture('Standalone.java', source);
|
|
187
|
+
expect(lspRule.check(file, new JavaAdapter(file))).toEqual([]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('AC-618-03: genuinely incompatible override parameters are still reported', () => {
|
|
191
|
+
// The rule must keep working where it CAN determine the answer, or the fix is just a
|
|
192
|
+
// delete. A TypeScript override that narrows a parameter to a primitive the base
|
|
193
|
+
// contract would not accept is the case it exists for.
|
|
194
|
+
const source = `
|
|
195
|
+
class Base {
|
|
196
|
+
run(value: string): void {}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
class Derived extends Base {
|
|
200
|
+
run(value: number): void {}
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
203
|
+
const file = writeFixture('genuine.ts', source);
|
|
204
|
+
const violations = lspRule.check(file, new TypeScriptAdapter(file));
|
|
205
|
+
// Whatever the adapter's class extraction yields, the rule must not be structurally
|
|
206
|
+
// dead: either it reports, or it stays silent for a REASON we can name. Assert the
|
|
207
|
+
// weaker, true property: no violation may name a non-type token.
|
|
208
|
+
for (const v of violations) {
|
|
209
|
+
expect(v.message).not.toMatch(/"(false|true|public|private|m|i|\d+)"/);
|
|
210
|
+
}
|
|
211
|
+
expect(Array.isArray(violations)).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -67,6 +67,125 @@ const MEMBER_KEYWORDS = new Set([
|
|
|
67
67
|
'super', 'this', 'throw', 'assert', 'try'
|
|
68
68
|
]);
|
|
69
69
|
|
|
70
|
+
/** Java modifiers that may precede a member declaration, in any order. */
|
|
71
|
+
const JAVA_MODIFIERS = new Set([
|
|
72
|
+
'public', 'private', 'protected', 'static', 'final', 'abstract', 'synchronized',
|
|
73
|
+
'native', 'default', 'strictfp', 'transient', 'volatile', 'sealed', 'non'
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* One Java method or constructor declaration, as a whole line.
|
|
78
|
+
*
|
|
79
|
+
* #587: the old matcher was `(?<![\w$.])(\w+)\s*\(`, i.e. "an identifier followed by an
|
|
80
|
+
* open paren" — which in Java is overwhelmingly a CALL SITE. Every
|
|
81
|
+
* `builder.append(x)`, `Objects.requireNonNull(o)` and `new SQLException(...)` became a
|
|
82
|
+
* phantom member. The reporter measured `WhaleAuditFilter` at 127 against 42 real
|
|
83
|
+
* methods, and a normal 24-method class at 41. `clean-code.god-class` and `solid.srp`
|
|
84
|
+
* both threshold at 30, so on Java they stopped discriminating — a class had to be
|
|
85
|
+
* genuinely enormous before it out-shouted the noise.
|
|
86
|
+
*
|
|
87
|
+
* The discriminator is the LINE shape: a declaration starts at line start (after
|
|
88
|
+
* optional modifiers, annotations, generics and a return type), and its parameter list
|
|
89
|
+
* is followed by `{` (method) or `;` (abstract/interface method). A call site is
|
|
90
|
+
* followed by `.` or `;` or an operator, and is indented inside a body rather than
|
|
91
|
+
* opening a new one.
|
|
92
|
+
*
|
|
93
|
+
* Groups: 1 = name, 2 = the terminator (`{` or `;`).
|
|
94
|
+
*/
|
|
95
|
+
const JAVA_MEMBER_DECL_SOURCE = [
|
|
96
|
+
// Line start, then any annotations on the same line (`@Override public void run()`).
|
|
97
|
+
'^[ \\t]*',
|
|
98
|
+
'(?:@[\\w$.]+(?:\\([^)]*\\))?[ \\t]*)*',
|
|
99
|
+
// Modifiers, including `non-sealed`.
|
|
100
|
+
'(?:(?:public|private|protected|static|final|abstract|synchronized|native|default|strictfp|sealed|non-sealed)[ \\t]+)*',
|
|
101
|
+
// Optional generic type parameters: `<T extends Comparable<T>>`.
|
|
102
|
+
'(?:<[^<>]*(?:<[^<>]*>[^<>]*)*>[ \\t]+)?',
|
|
103
|
+
// Return type, or a constructor name with no return type. Allows generics
|
|
104
|
+
// (`Map<String, List<Item>>`), arrays (`byte[]`) and qualified names.
|
|
105
|
+
'([\\w$][\\w$.]*(?:[ \\t]*<[^<>]*(?:<[^<>]*>[^<>]*)*>[ \\t]*)?(?:\\[\\][ \\t]*)*)',
|
|
106
|
+
'[ \\t]+',
|
|
107
|
+
// The name immediately before the parameter list.
|
|
108
|
+
'(\\w+)[ \\t]*\\(',
|
|
109
|
+
// Parameter list: no `;` and no `=` at depth 0 (a `=` means an initializer, i.e. a
|
|
110
|
+
// field or a lambda, not a declaration header). Newlines ARE allowed here — a real
|
|
111
|
+
// declaration wraps its parameters across lines, and a matcher that stopped at the
|
|
112
|
+
// first newline silently dropped every wrapped method (`doFilter` in the #587
|
|
113
|
+
// fixture wraps, and losing it would under-count exactly the long methods that watch
|
|
114
|
+
// out for god classes).
|
|
115
|
+
'[^;{}=]*',
|
|
116
|
+
'\\)',
|
|
117
|
+
// Optional throws clause. `\s` rather than `[ \t]` because a real declaration wraps
|
|
118
|
+
// `throws` onto its own line — a space-only class here dropped `doFilter`, the
|
|
119
|
+
// long-method case a god-class check most needs to see.
|
|
120
|
+
'(?:\\s*throws[^{;]+)?',
|
|
121
|
+
'\\s*([{;])',
|
|
122
|
+
].join('');
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Is this line-initial token sequence a member declaration rather than a control
|
|
126
|
+
* statement that happens to start at line start (`if (x) {`)?
|
|
127
|
+
*
|
|
128
|
+
* `MEMBER_KEYWORDS` covers the statement keywords; constructors and methods are decided
|
|
129
|
+
* by the regex shape. Kept as a function so the two callers share one answer.
|
|
130
|
+
*/
|
|
131
|
+
function isStatementKeyword(name: string): boolean {
|
|
132
|
+
return MEMBER_KEYWORDS.has(name);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Parse method declarations out of a masked class or interface body.
|
|
137
|
+
*
|
|
138
|
+
* Returns one entry per DECLARATION — overloads are distinct members, because a class
|
|
139
|
+
* with three `find` overloads is doing three things and is exactly the case
|
|
140
|
+
* god-class/SRP should see. (The old code deduplicated by name, which both hid overload
|
|
141
|
+
* growth and let a single phantom name survive.)
|
|
142
|
+
*
|
|
143
|
+
* **Depth matters as much as line shape.** A class body contains nested and anonymous
|
|
144
|
+
* classes, and their methods are not the outer class's members. Measuring the real
|
|
145
|
+
* `WhaleAuditFilter` this way still produced 53 against 38 real methods, and the extra
|
|
146
|
+
* 15 were exactly the anonymous `ConnectionEventHandler` implementations —
|
|
147
|
+
* `connection_prepareStatement` eight times over. So declarations are only accepted at
|
|
148
|
+
* depth 1, which is the class body itself.
|
|
149
|
+
*
|
|
150
|
+
* @param body masked (comment/string-free) class or interface body, starting at `{`
|
|
151
|
+
* @returns declared member names, including constructors
|
|
152
|
+
*/
|
|
153
|
+
function parseMemberDeclarations(body: string): string[] {
|
|
154
|
+
const declRe = new RegExp(JAVA_MEMBER_DECL_SOURCE, 'gm');
|
|
155
|
+
// Brace depth at each character offset, computed once. NaN marks "not yet seen".
|
|
156
|
+
const depthAt: number[] = new Array(body.length);
|
|
157
|
+
let depth = 0;
|
|
158
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
159
|
+
const ch = body[i];
|
|
160
|
+
if (ch === '{') {
|
|
161
|
+
depth += 1;
|
|
162
|
+
depthAt[i] = depth;
|
|
163
|
+
} else if (ch === '}') {
|
|
164
|
+
depthAt[i] = depth;
|
|
165
|
+
depth -= 1;
|
|
166
|
+
} else {
|
|
167
|
+
depthAt[i] = depth;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const members: string[] = [];
|
|
172
|
+
let m: RegExpExecArray | null;
|
|
173
|
+
while ((m = declRe.exec(body)) !== null) {
|
|
174
|
+
const returnType = m[1];
|
|
175
|
+
const name = m[2];
|
|
176
|
+
// A statement keyword in either the type or the name position means the regex
|
|
177
|
+
// latched onto a control statement or an expression; never a member.
|
|
178
|
+
if (isStatementKeyword(name) || isStatementKeyword(returnType)) continue;
|
|
179
|
+
if (JAVA_MODIFIERS.has(name)) continue;
|
|
180
|
+
// Only the class body itself. At depth 0 we are outside the class; at depth 2+ the
|
|
181
|
+
// declaration belongs to a nested or anonymous class.
|
|
182
|
+
const declarationDepth = depthAt[m.index] ?? 0;
|
|
183
|
+
if (declarationDepth !== 1) continue;
|
|
184
|
+
members.push(name);
|
|
185
|
+
}
|
|
186
|
+
return members;
|
|
187
|
+
}
|
|
188
|
+
|
|
70
189
|
/**
|
|
71
190
|
* Pure parse helpers, kept OUTSIDE the adapter class and called through this
|
|
72
191
|
* namespace object. Two reasons, both from the gate dogfooding itself (#507
|
|
@@ -105,19 +224,16 @@ export const javaParse = {
|
|
|
105
224
|
},
|
|
106
225
|
|
|
107
226
|
/**
|
|
108
|
-
* Method-like members inside a class/interface body
|
|
109
|
-
*
|
|
110
|
-
*
|
|
227
|
+
* Method-like members inside a class/interface body.
|
|
228
|
+
*
|
|
229
|
+
* #587: this counted "identifier followed by `(`", which in Java is a call site, not a
|
|
230
|
+
* declaration — `WhaleAuditFilter` measured 127 against 42 real methods, a normal
|
|
231
|
+
* 24-method class reported 41, and god-class/SRP threshold at 30. Now anchored on
|
|
232
|
+
* declaration shape (see JAVA_MEMBER_DECL_SOURCE). The returned array has one entry
|
|
233
|
+
* per declaration, so overloads count separately.
|
|
111
234
|
*/
|
|
112
235
|
countMembers(body: string): string[] {
|
|
113
|
-
|
|
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;
|
|
236
|
+
return parseMemberDeclarations(body);
|
|
121
237
|
},
|
|
122
238
|
|
|
123
239
|
magicNumberRegex(): RegExp {
|