fix: add missing same-class bare-call fallback to incremental rebuild#1947
Conversation
The full-build call-resolution pipeline (stages/build-edges.ts) retries a no-receiver call as <CallerClass>.<callName> when global resolution fails -- this is how C#/Java-style static sibling calls (e.g. IsValidEmail() inside Validators.ValidateUser) resolve to Validators.IsValidEmail. The incremental single-file rebuild path (incremental.ts, used by watch mode) only implemented the same-class this.method() and Object.defineProperty fallbacks, leaving class-scoped bare calls without a matching fallback. Consolidates resolveSameClassThisFallback and resolveSameClassBareCallFallback into the shared call-resolver.ts module (previously private to build-edges.ts) so the full-build and incremental pipelines call the exact same functions and cannot drift out of sync again. incremental.ts's applyCallResolutionFallbacks (renamed from applyThisReceiverFallbacks, since it now covers more than this-receiver calls) wires in the bare-call strategy between the existing this-fallback and the defineProperty accessor fallback, mirroring resolveFallbackTargets's strategy order in build-edges.ts.
Greptile SummaryThis PR fixes a parity gap between the full-build and incremental (watch-mode) call-resolution pipelines: the incremental path was missing the same-class bare-call fallback (
Confidence Score: 4/5Safe to merge — the change is a well-scoped refactor that promotes two private helpers to shared exports without altering their logic, and the integration test directly validates that an incremental rebuild of the C# fixture produces byte-identical call edges to a full build. The core fix is correct and the test coverage is strong. The only open point is the inline JSDoc in applyCallResolutionFallbacks claiming it mirrors the full-build pipeline when two fallbacks (Kotlin pre-qualified pass, reflection key expr) are still missing from the incremental path — a gap the PR description explicitly defers to #1946. This does not affect runtime behavior of the fix itself, but leaves the comment slightly misleading for future readers of that function. incremental.ts — the JSDoc for applyCallResolutionFallbacks overstates parity with resolveFallbackTargets; the remaining divergences should be noted inline so future engineers do not assume the two pipelines are fully aligned. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant BE as build-edges.ts (full build)
participant INC as incremental.ts (watch mode)
participant CR as call-resolver.ts (shared)
note over BE,INC: Before this PR — bare-call fallback only in full build
BE->>CR: resolveSameClassThisFallback() [private copy]
BE->>CR: resolveSameClassBareCallFallback() [private copy]
INC->>CR: resolveSameClassQualifiedMethod() [no language guard]
note over INC: bare-call fallback missing
note over BE,INC: After this PR — both pipelines share exported helpers
BE->>CR: resolveSameClassThisFallback() [exported]
BE->>CR: resolveSameClassBareCallFallback() [exported, incl. isModuleScopedLanguage guard]
INC->>CR: resolveSameClassThisFallback() [exported]
INC->>CR: resolveSameClassBareCallFallback() [exported, incl. isModuleScopedLanguage guard]
note over INC: bare-call fallback now present
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant BE as build-edges.ts (full build)
participant INC as incremental.ts (watch mode)
participant CR as call-resolver.ts (shared)
note over BE,INC: Before this PR — bare-call fallback only in full build
BE->>CR: resolveSameClassThisFallback() [private copy]
BE->>CR: resolveSameClassBareCallFallback() [private copy]
INC->>CR: resolveSameClassQualifiedMethod() [no language guard]
note over INC: bare-call fallback missing
note over BE,INC: After this PR — both pipelines share exported helpers
BE->>CR: resolveSameClassThisFallback() [exported]
BE->>CR: resolveSameClassBareCallFallback() [exported, incl. isModuleScopedLanguage guard]
INC->>CR: resolveSameClassThisFallback() [exported]
INC->>CR: resolveSameClassBareCallFallback() [exported, incl. isModuleScopedLanguage guard]
note over INC: bare-call fallback now present
Reviews (1): Last reviewed commit: "fix: add missing same-class bare-call fa..." | Re-trigger Greptile |
| * Runs in order (mirrors `resolveFallbackTargets` in the full-build | ||
| * `stages/build-edges.ts` pipeline — see #1765): | ||
| * 1. Same-class `this.method()` fallback. | ||
| * 2. Same-class bare-call fallback for non-JS/TS class-scoped languages. | ||
| * 3. Object.defineProperty accessor fallback (this-calls inside getter/setter). |
There was a problem hiding this comment.
The JSDoc says this function "mirrors
resolveFallbackTargets" but it still omits two fallbacks that are present in the full-build pipeline: the Kotlin reflection pre-qualified pass (run before primary resolution) and resolveReflectionKeyExprFallback (run between the bare-call and defineProperty strategies). The PR description documents these gaps and files them as #1946, but a reader of only this function would incorrectly assume full parity. A short note in the comment would prevent future confusion.
| * Runs in order (mirrors `resolveFallbackTargets` in the full-build | |
| * `stages/build-edges.ts` pipeline — see #1765): | |
| * 1. Same-class `this.method()` fallback. | |
| * 2. Same-class bare-call fallback for non-JS/TS class-scoped languages. | |
| * 3. Object.defineProperty accessor fallback (this-calls inside getter/setter). | |
| * Runs in order (partially mirrors `resolveFallbackTargets` in the full-build | |
| * `stages/build-edges.ts` pipeline — see #1765 for this fix, #1946 for the | |
| * remaining divergences: Kotlin reflection pre-qualified pass and | |
| * resolveReflectionKeyExprFallback are not yet wired into the incremental path): | |
| * 1. Same-class `this.method()` fallback. | |
| * 2. Same-class bare-call fallback for non-JS/TS class-scoped languages. | |
| * 3. Object.defineProperty accessor fallback (this-calls inside getter/setter). |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Codegraph Impact Analysis10 functions changed → 11 callers affected across 4 files
|
Fixes #1765
The full-build call-resolution pipeline (
resolveFallbackTargetsinsrc/domain/graph/builder/stages/build-edges.ts) retries a no-receiver call as<CallerClass>.<callName>when global resolution fails — this is how C#/Java-style static sibling calls (e.g.IsValidEmail()insideValidators.ValidateUser) resolve toValidators.IsValidEmail. The incremental single-file rebuild path (src/domain/graph/builder/incremental.ts, used by watch mode) only implemented the same-classthis.method()andObject.definePropertyfallbacks — the class-scoped bare-call fallback, and theisModuleScopedLanguageimport it needs, were entirely missing.Fix
Rather than duplicating the fallback logic into
incremental.ts(which is exactly how it drifted out of sync withbuild-edges.tsin the first place), this consolidates the two same-class fallback strategies into the sharedsrc/domain/graph/builder/call-resolver.tsmodule — already the home of the underlyingresolveSameClassQualifiedMethodhelper both pipelines call:resolveSameClassThisFallbackandresolveSameClassBareCallFallbackmove frombuild-edges.ts(private) tocall-resolver.ts(exported), unchanged in behavior.build-edges.ts'sresolveFallbackTargetsnow imports and calls the shared versions instead of its own local copies.incremental.ts's fallback dispatcher (renamedapplyThisReceiverFallbacks→applyCallResolutionFallbacks, since it now covers more thanthis-receiver calls) wires in the bare-call strategy between the existing this-fallback and thedefinePropertyaccessor fallback, mirroringresolveFallbackTargets's strategy order exactly.Both pipelines now call the identical exported functions, so they cannot drift apart again.
Scope note
While investigating, I found two more full-build fallback strategies (
resolveKotlinReflectionPreQualified,resolveReflectionKeyExprFallback) that are also missing from the incremental path. Filed as #1946 — out of scope for this PR to keep it to a single concern.Validation
npx tsc --noEmit— clean.npm run lint— clean.tests/unit/call-resolver.test.ts) directly exerciseresolveSameClassThisFallback/resolveSameClassBareCallFallbackin isolation with a lookup mock scoped tobyNameAndFileonly, proving each fallback resolves via the file-scoped lookup independent of the primary resolution path's own embedded same-class handling.tests/integration/issue-1765-incremental-bare-call-fallback.test.ts) with a C# fixture (two classes, colliding method names) verifies an incremental rebuild produces byte-identicalcallsedges to a full build, including a same-class bare-call resolution and a check that it does not false-positive across the sibling class.tests/benchmarks/resolution/resolution-benchmark.test.ts(precision/recall across all 34 languages): 206/206 passed.codegraph diff-impact --staged -Tshows a contained blast radius: the two relocated/renamed functions and their transitive callers inincremental.ts, plus the new fixture.