Skip to content

Commit 7547c3c

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/csharp-static-receiver-parity
2 parents 6e143c2 + 76907e6 commit 7547c3c

8 files changed

Lines changed: 124 additions & 16 deletions

File tree

src/domain/graph/builder/call-resolver.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,15 @@ export function resolveByMethodOrGlobal(
116116
: (typeEntry as { type?: string }).type
117117
: null;
118118

119-
// Handle inline new-expression receivers: `(new Foo).bar()` or `(new Foo()).bar()`.
120-
// extractReceiverName returns the raw node text for non-identifier nodes, so `(new A).t()`
121-
// produces receiver='(new A)'. Extract the constructor name directly.
122-
// The regex intentionally restricts to uppercase-initial names ([A-Z_$]) as a heuristic
123-
// to distinguish constructors (PascalCase) from regular functions — avoiding false positives
124-
// on `(new xmlParser()).parse()` style calls which are rare in practice.
119+
// Belt-and-suspenders fallback for inline new-expression receivers that
120+
// extractReceiverName did not normalise (e.g. raw text leaked from an
121+
// unhandled AST node type). extractReceiverName already handles the common
122+
// `new_expression` / `parenthesized_expression(new_expression)` shapes by
123+
// returning the constructor name directly, so this branch is exercised only
124+
// by future node types or constructs that fall through to the raw-text path.
125+
// The uppercase-initial restriction ([A-Z_$]) is a heuristic to distinguish
126+
// constructors (PascalCase) from regular functions and avoids false positives
127+
// on `(new xmlParser()).parse()` style calls.
125128
if (!typeName && call.receiver) {
126129
const m = /^\(?\s*new\s+([A-Z_$][A-Za-z0-9_$]*)/.exec(call.receiver);
127130
if (m?.[1]) typeName = m[1];

src/domain/graph/builder/stages/build-edges.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,10 @@ function buildObjectRestParamPostPass(
852852
typeMap.set(orpb.restName, { type: pb.argName, confidence: 0.65 });
853853
}
854854
}
855+
// restNames tracks every rest-parameter name found, regardless of whether the
856+
// scoped key was already in typeMap. This ensures the post-pass (below) processes
857+
// all calls whose receiver matches a known rest binding — not just those whose
858+
// typeMap entry was seeded in this iteration.
855859
restNames.add(orpb.restName);
856860
}
857861
}

src/extractors/javascript.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2676,6 +2676,25 @@ function extractReceiverName(objNode: TreeSitterNode | null): string | undefined
26762676
if (!objNode) return undefined;
26772677
const t = objNode.type;
26782678
if (t === 'identifier' || t === 'this' || t === 'super') return objNode.text;
2679+
// `(new Foo(...)).method()` — extract the constructor name so the resolver can
2680+
// look up `Foo.method` directly without relying on a text-based regex heuristic.
2681+
if (t === 'new_expression') {
2682+
const name = extractNewExprTypeName(objNode);
2683+
if (name) return name;
2684+
}
2685+
if (t === 'parenthesized_expression') {
2686+
// Only one level of parentheses is unwrapped here. Doubly-nested parens
2687+
// (e.g. `((new Dog())).bark()`) and cast expressions inside parens
2688+
// (e.g. `(new Dog() as Animal).bark()`) fall through to raw-text handling
2689+
// below and are caught by the regex fallback in call-resolver.ts.
2690+
for (let i = 0; i < objNode.childCount; i++) {
2691+
const child = objNode.child(i);
2692+
if (child?.type === 'new_expression') {
2693+
const name = extractNewExprTypeName(child);
2694+
if (name) return name;
2695+
}
2696+
}
2697+
}
26792698
return objNode.text;
26802699
}
26812700

tests/benchmarks/regression-guard.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,21 +72,22 @@ const NOISY_METRICS = new Set<string>(['No-op rebuild', '1-file rebuild', 'fnDep
7272
* than native and dominated by interpreter + GC overhead. The same ±10–20ms
7373
* of shared-runner jitter therefore lands as a much larger *percentage* swing
7474
* than on native. Empirically, WASM timing metrics on the publish runner swing
75-
* run-to-run by +27–67% on byte-identical code (No-op rebuild 15→25 = +67%,
75+
* run-to-run by +27–71% on byte-identical code (No-op rebuild 15→25 = +67%,
7676
* Query time 32.5→44.2 = +36%, fnDeps depth 3/5 ~+31%, Full build 7664→9833
77-
* = +28%), which previously required a per-version KNOWN_REGRESSIONS entry for
78-
* each metric on every release — an endless whack-a-mole.
77+
* = +28%, Build ms/file 18.7→32 = +71%), which previously required a
78+
* per-version KNOWN_REGRESSIONS entry for each metric on every release — an
79+
* endless whack-a-mole.
7980
*
8081
* Why this is safe: the native engine shares all extraction, resolution, and
8182
* query logic with WASM (the WASM path only swaps the parser/runtime), so any
8283
* *real* algorithmic regression shows up on the native numbers too — and native
8384
* keeps the strict 25% / 50% thresholds. Native is the canary. WASM timing only
8485
* needs to catch gross WASM-specific catastrophes (the 100–220% blowups seen in
85-
* v3.0.1–3.4.0), which 70% still flags, while absorbing the ≤67% shared-runner
86+
* v3.0.1–3.4.0), which 75% still flags, while absorbing the ≤71% shared-runner
8687
* jitter. Size metrics (DB bytes/file) are engine-independent and excluded from
8788
* this widening via SIZE_METRICS below — they keep the strict threshold.
8889
*/
89-
const WASM_TIMING_THRESHOLD = 0.7;
90+
const WASM_TIMING_THRESHOLD = 0.75;
9091

9192
/**
9293
* Metric labels that measure size/count rather than wall-clock time. These are

tests/benchmarks/resolution/jelly-micro.test.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,53 @@ function discoverTests(): string[] {
5454
.sort();
5555
}
5656

57+
/**
58+
* Per-fixture minimum recall floors based on the baseline measured on origin/main
59+
* (commit 784951d, June 2026). Fixtures not listed here default to 0 — they
60+
* produce 0% recall and can only improve, never regress below zero.
61+
*
62+
* Format: fixture-name → minimum recall fraction in [0, 1].
63+
* Exact fractions shown in comments; stored as the corresponding percentage
64+
* value so a single lost TP triggers a failure.
65+
*
66+
* Note: more1 was moved to the pts-javascript fixture set in #1383 and is
67+
* no longer part of jelly-micro.
68+
*/
69+
const RECALL_FLOORS: Record<string, number> = {
70+
accessors3: 1.0, // 1/1
71+
arguments: 1.0, // 1/1
72+
classes: 0.19, // 6/31
73+
defineProperty: 0.5, // 3/6
74+
fun: 1.0, // 4/4
75+
generators: 1.0, // 9/9
76+
'receiver-callee-mixup': 1.0, // 1/1
77+
rest: 1.0, // 1/1
78+
spread: 1.0, // 4/4
79+
super: 0.38, // 5/13
80+
super2: 0.4, // 2/5
81+
super3: 1.0, // 3/3
82+
this: 1.0, // 1/1
83+
};
84+
5785
const tests = discoverTests();
5886

87+
// Sanity-check: every RECALL_FLOORS key must match a discovered fixture name.
88+
// A mismatch means either a fixture was renamed or the key was mistyped, and
89+
// the regression floor would silently degrade to 0 without this guard.
90+
// Only enforce when the fixture directory is present (CI skips the whole suite
91+
// when fixtures are absent).
92+
if (tests.length > 0) {
93+
const testSet = new Set(tests);
94+
for (const key of Object.keys(RECALL_FLOORS)) {
95+
if (!testSet.has(key)) {
96+
throw new Error(
97+
`RECALL_FLOORS key "${key}" does not match any discovered fixture in ${FIXTURES_DIR}. ` +
98+
'Update the key or remove the stale entry.',
99+
);
100+
}
101+
}
102+
}
103+
59104
// Per-test results collected for summary
60105
const allResults: Record<
61106
string,
@@ -194,8 +239,8 @@ describe.skipIf(tests.length === 0)('Jelly Micro-Test Benchmark', () => {
194239
for (const e of fn) console.log(` FN: ${e}`);
195240
}
196241

197-
// Soft gate: recall must be ≥ 0% (we don't gate yet — this benchmark is diagnostic)
198-
expect(recall).toBeGreaterThanOrEqual(0);
242+
const floor = RECALL_FLOORS[testName] ?? 0;
243+
expect(recall).toBeGreaterThanOrEqual(floor);
199244
});
200245
});
201246
}

tests/benchmarks/resolution/resolution-benchmark.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ const TECHNIQUE_MAP: Record<string, string> = {
9797
'pts-set': 'points-to',
9898
'pts-array-from': 'points-to',
9999
'pts-spread': 'points-to',
100+
'pts-param': 'points-to',
100101
'define-property': 'ts-native',
101102
};
102103

@@ -147,7 +148,9 @@ const THRESHOLDS: Record<string, { precision: number; recall: number }> = {
147148
python: { precision: 0.7, recall: 0.3 },
148149
go: { precision: 0.7, recall: 0.3 },
149150
java: { precision: 0.7, recall: 0.3 },
150-
csharp: { precision: 1.0, recall: 0.8 },
151+
// csharp 1.0/0.9: static receiver fix (#1395) ensures precision; var-declared instance typeMap
152+
// (implicit_type) lifts receiver-typed recall from 0/4 → 4/4 (#1396).
153+
csharp: { precision: 1.0, recall: 0.9 },
151154
kotlin: { precision: 0.6, recall: 0.2 },
152155
// Lower bars — resolution still maturing
153156
rust: { precision: 0.6, recall: 0.2 },

tests/parsers/csharp.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,26 @@ public class Service : BaseService, IDisposable {
151151
expect.objectContaining({ name: 'User.Name', kind: 'property' }),
152152
);
153153
});
154+
155+
it('populates typeMap for var-declared instances (implicit type)', () => {
156+
const symbols = parseCSharp(`public class Program {
157+
void Run() {
158+
var service = new UserService();
159+
var repo = new UserRepository();
160+
service.AddUser(null);
161+
}
162+
}`);
163+
expect(symbols.typeMap.get('service')).toEqual({ type: 'UserService', confidence: 0.9 });
164+
expect(symbols.typeMap.get('repo')).toEqual({ type: 'UserRepository', confidence: 0.9 });
165+
});
166+
167+
it('populates typeMap for explicitly-typed local variables', () => {
168+
const symbols = parseCSharp(`public class Foo {
169+
void Bar() {
170+
UserService svc = new UserService();
171+
svc.DoWork();
172+
}
173+
}`);
174+
expect(symbols.typeMap.get('svc')).toEqual({ type: 'UserService', confidence: 0.9 });
175+
});
154176
});

tests/search/embedding-regression.test.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,23 @@ describe.skipIf(!hasTransformers)('embedding regression (real model)', () => {
6868
dbPath = path.join(tmpDir, '.codegraph', 'graph.db');
6969

7070
// Build embeddings with the smallest/fastest model.
71-
// Skip gracefully when HuggingFace rate-limits the model download (HTTP 429).
71+
// Skip gracefully when HuggingFace rate-limits the model download (HTTP 429)
72+
// or when the network is unavailable (ECONNRESET, ETIMEDOUT, ENOTFOUND,
73+
// ECONNREFUSED, ERR_HTTP2_STREAM_CANCEL, ERR_HTTP2_SESSION_ERROR).
7274
try {
7375
await buildEmbeddings(tmpDir, 'minilm', dbPath);
7476
} catch (err: unknown) {
7577
const msg = err instanceof Error ? err.message : String(err);
76-
if (msg.includes('429')) {
78+
const code = (err as NodeJS.ErrnoException).code ?? '';
79+
const isNetworkError =
80+
msg.includes('429') ||
81+
code === 'ECONNRESET' ||
82+
code === 'ETIMEDOUT' ||
83+
code === 'ENOTFOUND' ||
84+
code === 'ECONNREFUSED' ||
85+
code === 'ERR_HTTP2_STREAM_CANCEL' ||
86+
code === 'ERR_HTTP2_SESSION_ERROR';
87+
if (isNetworkError) {
7788
rateLimited = true;
7889
return;
7990
}

0 commit comments

Comments
 (0)