-
Notifications
You must be signed in to change notification settings - Fork 15
fix: add missing same-class bare-call fallback to incremental rebuild #1947
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
carlos-alm
wants to merge
1
commit into
fix/issue-1764-object-literal-computed-method-keys-extract
from
fix/issue-1765-incremental-rebuild-missing-same-class-bare
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| namespace App | ||
| { | ||
| public static class Validators | ||
| { | ||
| public static bool ValidateUser(string email) | ||
| { | ||
| return IsValidEmail(email); | ||
| } | ||
|
|
||
| public static bool IsValidEmail(string email) | ||
| { | ||
| return email.Contains("@"); | ||
| } | ||
| } | ||
|
|
||
| public static class Formatters | ||
| { | ||
| // Same method name as Validators.IsValidEmail — must NOT cross-resolve. | ||
| public static bool IsValidEmail(string value) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| public static string FormatName(string name) | ||
| { | ||
| return name.Trim(); | ||
| } | ||
| } | ||
| } |
115 changes: 115 additions & 0 deletions
115
tests/integration/issue-1765-incremental-bare-call-fallback.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| /** | ||
| * Regression test for #1765: incremental rebuild was missing the same-class | ||
| * bare-call fallback that the full-build pipeline has | ||
| * (`resolveSameClassBareCallFallback` in `stages/build-edges.ts`). | ||
| * | ||
| * For class-scoped languages (e.g. C#), a bare call with no receiver that | ||
| * fails global resolution should retry qualified as `<CallerClass>.<callName>` | ||
| * — this is how C# static sibling calls like `IsValidEmail()` inside | ||
| * `Validators.ValidateUser` resolve to `Validators.IsValidEmail`. | ||
| * | ||
| * `incremental.ts`'s call-resolution path (`applyCallResolutionFallbacks`) | ||
| * now shares the same fallback helpers as the full build | ||
| * (`resolveSameClassThisFallback` / `resolveSameClassBareCallFallback` in | ||
| * `call-resolver.ts`), so a single-file incremental rebuild (watch mode) | ||
| * must produce the exact same call edges as a full `codegraph build`. | ||
| */ | ||
|
|
||
| import fs from 'node:fs'; | ||
| import os from 'node:os'; | ||
| import path from 'node:path'; | ||
| import Database from 'better-sqlite3'; | ||
| import { afterAll, beforeAll, describe, expect, it } from 'vitest'; | ||
| import { buildGraph } from '../../src/domain/graph/builder.js'; | ||
|
|
||
| const FIXTURE_DIR = path.join(import.meta.dirname, '..', 'fixtures', 'bare-call-scope'); | ||
|
|
||
| interface CallEdgeRow { | ||
| caller_name: string; | ||
| callee_name: string; | ||
| } | ||
|
|
||
| function readCallEdges(dbPath: string): CallEdgeRow[] { | ||
| const db = new Database(dbPath, { readonly: true }); | ||
| try { | ||
| return db | ||
| .prepare( | ||
| `SELECT n1.name AS caller_name, n2.name AS callee_name | ||
| FROM edges e | ||
| JOIN nodes n1 ON e.source_id = n1.id | ||
| JOIN nodes n2 ON e.target_id = n2.id | ||
| WHERE e.kind = 'calls' | ||
| ORDER BY n1.name, n2.name`, | ||
| ) | ||
| .all() as CallEdgeRow[]; | ||
| } finally { | ||
| db.close(); | ||
| } | ||
| } | ||
|
|
||
| describe('incremental same-class bare-call fallback parity (#1765)', () => { | ||
| let tmpDir: string; | ||
| let fullEdges: CallEdgeRow[]; | ||
| let incrEdges: CallEdgeRow[]; | ||
|
|
||
| beforeAll(async () => { | ||
| tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-bare-call-parity-')); | ||
| const fullDir = path.join(tmpDir, 'full'); | ||
| const incrDir = path.join(tmpDir, 'incr'); | ||
|
|
||
| fs.cpSync(FIXTURE_DIR, fullDir, { recursive: true }); | ||
| fs.cpSync(FIXTURE_DIR, incrDir, { recursive: true }); | ||
|
|
||
| // Initial full build on the incr copy (establishes baseline hashes) | ||
| await buildGraph(incrDir, { incremental: false, skipRegistry: true, engine: 'wasm' }); | ||
|
|
||
| // Comment-only touch — triggers incremental rebuild of Validators.cs | ||
| const touch = (dir: string) => | ||
| fs.appendFileSync(path.join(dir, 'Validators.cs'), '\n// touch\n'); | ||
| touch(fullDir); | ||
| touch(incrDir); | ||
|
|
||
| // Full build from scratch | ||
| await buildGraph(fullDir, { incremental: false, skipRegistry: true, engine: 'wasm' }); | ||
| // Incremental rebuild — exercises applyCallResolutionFallbacks's bare-call strategy | ||
| await buildGraph(incrDir, { incremental: true, skipRegistry: true, engine: 'wasm' }); | ||
|
|
||
| fullEdges = readCallEdges(path.join(fullDir, '.codegraph', 'graph.db')); | ||
| incrEdges = readCallEdges(path.join(incrDir, '.codegraph', 'graph.db')); | ||
| }, 60_000); | ||
|
|
||
| afterAll(() => { | ||
| if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| it('incremental emits Validators.ValidateUser → Validators.IsValidEmail (bare-call same-class fallback)', () => { | ||
| const edge = incrEdges.find( | ||
| (e) => | ||
| e.caller_name === 'Validators.ValidateUser' && e.callee_name === 'Validators.IsValidEmail', | ||
| ); | ||
| expect( | ||
| edge, | ||
| `Expected Validators.ValidateUser -> Validators.IsValidEmail in incremental build.\nActual edges:\n${JSON.stringify(incrEdges, null, 2)}`, | ||
| ).toBeDefined(); | ||
| }); | ||
|
|
||
| it('incremental does NOT emit Validators.ValidateUser → Formatters.IsValidEmail (cross-class false-positive)', () => { | ||
| const edge = incrEdges.find( | ||
| (e) => | ||
| e.caller_name === 'Validators.ValidateUser' && e.callee_name === 'Formatters.IsValidEmail', | ||
| ); | ||
| expect( | ||
| edge, | ||
| `Expected NO Validators.ValidateUser -> Formatters.IsValidEmail edge.\nActual edges:\n${JSON.stringify(incrEdges, null, 2)}`, | ||
| ).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('incremental edges match full build edges exactly', () => { | ||
| const fullSet = new Set(fullEdges.map((e) => `${e.caller_name}→${e.callee_name}`)); | ||
| const incrSet = new Set(incrEdges.map((e) => `${e.caller_name}→${e.callee_name}`)); | ||
| const missing = [...fullSet].filter((k) => !incrSet.has(k)); | ||
| const extra = [...incrSet].filter((k) => !fullSet.has(k)); | ||
| expect(missing, `Missing in incremental: ${missing.join(', ')}`).toEqual([]); | ||
| expect(extra, `Extra in incremental: ${extra.join(', ')}`).toEqual([]); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) andresolveReflectionKeyExprFallback(run between the bare-call anddefinePropertystrategies). 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.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!