From 5d843d0c31965d8aa1205bbdd88e5b39933f01c3 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Fri, 30 Oct 2026 00:39:08 -0500 Subject: [PATCH] fix(resolution): calls between C, C++ and Objective-C files link by name Name matching compared a candidate's raw language tag with the reference's: the single-candidate confidence in matchByExactName and matchFuzzy, and findBestMatch's same-language skip and +50/-80 score. So a call whose name had several definitions in another of C, C++ and Objective-C scored -80 for each and linked to nothing (an Objective-C file's _glfwPlatformSetTls(), a C++ call into Dear ImGui's imgui.h, which reads as C), and the one definition of a name across TSX and TS, or C and C++, was a 0.5 cross-language guess. The single-candidate confidence now compares language families (sameLanguageFamily). Ranking treats C, C++ and Objective-C as one language (sameRankingLanguage), the very language worth a point that only breaks a tie; other families keep the language itself, because ranking them by family moved excalidraw's Store::scheduleCapture calls to the App.tsx method beside them and gave okhttp 83 wrong Java-to-Kotlin Builder picks. Validation: - __tests__/name-match-language-family.test.ts, 8 tests (default and wasm): 6 fail on the base main; the pin that a .ts call keeps its .ts target over a .tsx namesake beside it passes on both; all pass with the fix. 24 related test files (cross-language, React Native, Expo, Fabric, frameworks, mixed TS/JSX, Objective-C, resolution): 746 pass. - A/B, main vs fix: glfw +4 calls from Objective-C into C, imgui +93 from C++ into imgui.h (read as C) and imstb_rectpack.h, all to the definitions meant; excalidraw, okhttp, leveldb and jq gain or lose no edge (43, 9, 97 and 30 unique-name links go from 0.5 to 0.9 with the same targets); flask and gin byte-identical. - Index time, median of 3 interleaved: imgui 7.9 s main, 7.7 s fix; excalidraw 9.2 s main, 9.4 s fix. Reimplements PR #1662 by ferrine. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/name-match-language-family.test.ts | 132 +++++++++++++++++++ src/resolution/name-matcher.ts | 47 +++++-- 3 files changed, 167 insertions(+), 13 deletions(-) create mode 100644 __tests__/name-match-language-family.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e81208385..f31cfd7f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -236,6 +236,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, a name written in a googletest `TEST_F` or `TEST_P` body now finds the test fixture's nested types, those of its base classes included, before anything in the surrounding namespace, as C++ does for the member function such a body is: facebook/rocksdb's `new TestUserDefinedIndexFactory()` links to the class nested in the fixture's base. A class declared inside a test body, or inside any function, is now only found from that function, so the local classes many RocksDB tests each declare under one name, like `FakeWF` or `MyManager`, no longer stand in for one another, and a call like `fwf->SetIOError(true)` reaches the `FakeWF` its own test declares. Re-index C++ projects after upgrading. - In C and C++ tests, a name that several files declare now links to the one the test's own includes declare, when there is one: protocolbuffers/protobuf's C# generator tests call the `TestSourceDir()` that `googletest.h` declares, not another test helper's they never include; its upb tests call `upb_Arena_New()` and the rest of upb's C API in upb's own files, not the copies bundled for PHP and Ruby, which had left those calls unlinked; and facebook/rocksdb's C API test reaches `rocksdb_open` and its siblings in `db/c.cc`. Re-index C and C++ projects after upgrading. - In C and C++ code outside tests too, a name that several files declare now links to the one the file's own includes declare, when there is one: protocolbuffers/protobuf's Python extension calls functions like `upb_Encode()` in upb's own files rather than the copies bundled for PHP, and facebook/rocksdb's stress tool calls the `StringToHex()` its own header declares rather than `LDBCommand::StringToHex`. Re-index C and C++ projects after upgrading. +- In C, C++ and Objective-C, a call into a function that another of the three languages defines now links to it even when the name has more than one definition, like a macOS `.m` file's `_glfwPlatformSetTls(…)`, which glfw defines in both `posix_thread.c` and `win32_thread.c`, or a C++ call into a header that reads as C, like Dear ImGui's `ImVector` methods in `imgui.h`. Before, such a call linked to nothing. Re-index C, C++ and Objective-C projects after upgrading. Thanks @ferrine. (#1662) - In C and C++, a call of a function a header declares now links to its definition even when the file that defines it reaches that header only through other headers: protocolbuffers/protobuf's Python extension calls upb's reflection functions, like `upb_FieldDef_Number()`, in upb's own files rather than the copies bundled for PHP. Re-index C and C++ projects after upgrading. - In C++, a declaration continued on a line that starts with `*`, like facebook/rocksdb's `const auto& el =` over `*reinterpret_cast*>(jhandle);`, is now read, so calls such as `el->OnFlushCompleted(…)` link to `EventListener`'s methods; such a line used to be skipped as part of a comment. Code commented out in a `/* … */` block, and the text of a string literal, like protocolbuffers/protobuf's `[[deprecated("Please use SpaceUsedLong() instead")]]`, are no longer read as declarations, which had sent a call to the wrong method or to none. Re-index C++ projects after upgrading. - In C++, a call with a comparison or a shift among its arguments, like protocolbuffers/protobuf's `p->Emit({{"declare", has_bit_word_index < 0 ? … : ""}, …}, "…")` or `WriteVarint(num << 3, s)`, is now read with the right number of arguments, so it links to the overload that fits rather than one taking fewer. A parameter or variable whose type has template arguments is now read even after another such parameter on the same line, like facebook/rocksdb's `const autovector& cf_ids`, or with empty ones, like fmt's `parse_context<>& ctx`: calls on it link to that class's methods, and calls on a `std::vector` declared that way no longer link to a project class's method of the same name. Re-index C++ projects after upgrading. diff --git a/__tests__/name-match-language-family.test.ts b/__tests__/name-match-language-family.test.ts new file mode 100644 index 000000000..a7d1158c2 --- /dev/null +++ b/__tests__/name-match-language-family.test.ts @@ -0,0 +1,132 @@ +/** + * Name matching weighed candidates by their raw language tag, so a `.tsx` + * file's call to the one `.ts` helper of its name was a cross-language guess + * (0.5) and an Objective-C or C++ file's call into C found nothing at all: + * + * app/window.m setTls(slot); // defined in two .c files + * app/main.cpp getTime(); // app/clock.c, or win/clock_win.c + * ui/Row.tsx formatDate(d); // the one in lib/fmt.ts + * + * The one candidate of a name is now judged by language family (TypeScript and + * TSX, Java and Kotlin, C and C++ and Objective-C are one each), and C, C++ + * and Objective-C — whose files share one table of names, and whose `.h` files + * are told apart only by sniffing — are ranked as one language. Other + * families keep the language itself for ranking: a `.ts` file's + * `this.app.store.scheduleCapture()` still means the `.ts` Store's method, not + * the `.tsx` App's beside it. + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; + +const FILES: Record = { + // A TSX component calling the one `.ts` helper of the name (a classic + // script: no import or export), and a `.ts` file calling a method of a + // `.ts` class while a `.tsx` class beside it has one of the same name. + 'web/ui/Row.tsx': 'export function Row() { return
{formatDate(new Date())}
; }\n', + 'web/lib/fmt.ts': 'function formatDate(d: Date) { return d.toISOString(); }\n', + 'web/components/App.tsx': [ + 'export class App {', + ' store = new Store();', + ' scheduleCapture() { return 1; }', + ' render() { return
; }', + '}', + '', + ].join('\n'), + 'web/components/App.clipboard.ts': [ + 'export class AppClipboard {', + ' constructor(private app: any) {}', + ' paste() {', + ' this.app.store.scheduleCapture();', + ' }', + '}', + '', + ].join('\n'), + 'web/store/store.ts': [ + 'export class Store {', + ' scheduleCapture() { return 2; }', + '}', + '', + ].join('\n'), + // C, C++ and Objective-C: one table of names. + 'native/app/init.c': 'void inputError(int code) { (void)code; }\n', + 'native/app/posix_thread.c': 'void setTls(int *slot) { (void)slot; }\n', + 'native/win/win32_thread.c': 'void setTls(int *slot) { (void)slot; }\n', + 'native/app/window.m': [ + '#import ', + '', + 'void makeCurrent(int *slot) {', + ' setTls(slot);', + ' inputError(1);', + '}', + '', + ].join('\n'), + 'native/app/clock.c': 'long getTime(void) { return 1; }\n', + 'native/win/clock_win.c': 'long getTime(void) { return 2; }\n', + 'native/app/main.cpp': [ + 'long run() {', + ' return getTime();', + '}', + '', + ].join('\n'), +}; + +describe.each(['default', 'wasm'])('name matching reads language families (%s)', (backend) => { + let root = ''; + let cg: CodeGraph | undefined; + let kernel: string | undefined; + + beforeAll(async () => { + kernel = process.env.CODEGRAPH_KERNEL; + if (backend === 'wasm') process.env.CODEGRAPH_KERNEL = '0'; + else delete process.env.CODEGRAPH_KERNEL; + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-lang-family-')); + for (const [rel, content] of Object.entries(FILES)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + cg = await CodeGraph.init(root, { index: true }); + }, 60_000); + + afterAll(() => { + cg?.destroy(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + if (kernel === undefined) delete process.env.CODEGRAPH_KERNEL; + else process.env.CODEGRAPH_KERNEL = kernel; + }); + + /** `target file:name@confidence` of each call out of `name` in `file`, sorted. */ + const callsFrom = (file: string, name: string): string[] => { + const from = cg!.getNodesInFile(file).filter((n) => n.name === name && (n.kind === 'function' || n.kind === 'method')); + expect(from, `${name} in ${file}`).toHaveLength(1); + return cg!.getOutgoingEdgesFrom([from[0]!.id]) + .filter((e) => e.kind === 'calls') + .map((e) => { + const t = cg!.getNode(e.target)!; + return `${t.filePath}:${t.name}@${(e.metadata as { confidence?: number } | undefined)?.confidence}`; + }) + .sort(); + }; + + const withoutConfidence = (call: string): string => call.replace(/@.*$/, ''); + + it('a TSX file calling the one TypeScript function of the name is a same-family match', () => { + expect(callsFrom('web/ui/Row.tsx', 'Row')).toEqual(['web/lib/fmt.ts:formatDate@0.9']); + }); + + it('TypeScript and TSX are still ranked by their own language', () => { + expect(callsFrom('web/components/App.clipboard.ts', 'paste').map(withoutConfidence)).toEqual(['web/store/store.ts:scheduleCapture']); + }); + + it('Objective-C calls into C: the one function, and the nearer of two', () => { + const calls = callsFrom('native/app/window.m', 'makeCurrent'); + expect(calls.map(withoutConfidence)).toEqual(['native/app/init.c:inputError', 'native/app/posix_thread.c:setTls']); + expect(calls).toContain('native/app/init.c:inputError@0.9'); + }); + + it('C++ calls into C: the nearer of two definitions', () => { + expect(callsFrom('native/app/main.cpp', 'run').map(withoutConfidence)).toEqual(['native/app/clock.c:getTime']); + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 464dbc0bf..eba5b22c9 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -175,6 +175,21 @@ export function sameLanguageFamily(a: string, b: string): boolean { const fa = LANGUAGE_FAMILY[a]; return fa !== undefined && fa === LANGUAGE_FAMILY[b]; } + +/** + * The languages whose files share one table of names: a C function is called + * by its bare name from C++ and Objective-C alike, and a `.h` file is C, C++ + * or Objective-C only as far as sniffing its text can tell (Dear ImGui's + * C++ `imgui.h` reads as C), so name matching ranks them as one language. + * Other families keep the language itself: their files reach each other's + * names through imports, and a name-only pick across TypeScript and TSX, or + * Java and Kotlin, went to a same-named declaration in the next file more + * often than to the one meant (#1662). + */ +const C_LINKED_LANGUAGES: ReadonlySet = new Set(['c', 'cpp', 'objc']); +function sameRankingLanguage(a: string, b: string): boolean { + return a === b || (C_LINKED_LANGUAGES.has(a) && C_LINKED_LANGUAGES.has(b)); +} /** Config/markup transitions stay open; every other code language has a family. */ const CODE_FAMILY: Record = { ...LANGUAGE_FAMILY, @@ -13283,10 +13298,11 @@ export function matchByExactName( return null; } - // If only one match, use it — but penalize cross-language matches + // If only one match, use it — but penalize a match in another language + // family (a `.tsx` component calling a `.ts` helper is not one, #1662) if (candidates.length === 1) { if (!isCrossFileReachable(candidates[0]!, ref, context)) return null; - const isCrossLanguage = candidates[0]!.language !== ref.language; + const isCrossLanguage = !sameLanguageFamily(candidates[0]!.language, ref.language); return { original: ref, targetNodeId: candidates[0]!.id, @@ -13334,10 +13350,11 @@ export function matchByExactName( // above a proximity guess, but below a unique name or the file's own // definition (0.9). The candidates are those findBestMatch weighed: a // namesake in another language never wins while one in the reference's - // own language exists. The caller's own copy being the caller itself is - // no evidence, as for a same-file match (isDecisiveSameFileMatch). - const weighed = candidates.some((c) => c.language === ref.language) - ? candidates.filter((c) => c.language === ref.language) + // own language (sameRankingLanguage) exists. The caller's own copy being + // the caller itself is no evidence, as for a same-file match + // (isDecisiveSameFileMatch). + const weighed = candidates.some((c) => sameRankingLanguage(c.language, ref.language)) + ? candidates.filter((c) => sameRankingLanguage(c.language, ref.language)) : candidates; const confidence = isDecisiveSameFileMatch(bestMatch, ref, candidates, context) ? 0.9 @@ -29385,7 +29402,8 @@ function findBestMatch( // Prioritization rules: // 1. Same file > different file // 2. Directory proximity (same module/package > different module) - // 3. Same language > different language + // 3. Same language > different language (C, C++ and Objective-C count as + // one: sameRankingLanguage), and the very language as a tie-break // 4. Functions/methods > classes/types (for call references) // 5. Exported > non-exported @@ -29406,10 +29424,11 @@ function findBestMatch( // scoring. Cuts the candidate set to same-language size on mixed front-end + // back-end repos (#915). When ALL candidates are cross-language (a legitimate // cross-language `calls` bridge), none are skipped and behavior is unchanged. - const hasSameLanguage = candidates.some((c) => c.language === ref.language); + const hasSameLanguage = candidates.some((c) => sameRankingLanguage(c.language, ref.language)); for (const candidate of candidates) { - if (hasSameLanguage && candidate.language !== ref.language) continue; + const sameLanguage = sameRankingLanguage(candidate.language, ref.language); + if (hasSameLanguage && !sameLanguage) continue; let score = 0; @@ -29428,9 +29447,11 @@ function findBestMatch( score += 80; } - // Language matching: strongly prefer same language, penalize cross-language - if (candidate.language === ref.language) { - score += 50; + // Language matching: strongly prefer same language, penalize cross-language. + // Between C, C++ and Objective-C the very language adds one point, which + // only breaks a tie: a directory nearer counts 15. + if (sameLanguage) { + score += candidate.language === ref.language ? 51 : 50; } else { score -= 80; } @@ -29662,7 +29683,7 @@ export function matchFuzzy( !(finalCandidates[0]!.kind !== 'function' && isBarePhpCall(ref, context)) && isLexicallyReachable(finalCandidates[0]!, ref, context) ) { - const isCrossLanguage = finalCandidates[0]!.language !== ref.language; + const isCrossLanguage = !sameLanguageFamily(finalCandidates[0]!.language, ref.language); return { original: ref, targetNodeId: finalCandidates[0]!.id,