fix(c): a C call, like redis's mstime(), reaches the function its own file can see, past another file's static copy (#2613)

A C call now links to the function of its name the call's own file can see:
its own, or one a header it includes defines, else one with external
linkage, the best placed of them (cppFunctionOverload's C branch). Before,
the name strategies took the function nearest the call; when that was
another source file's `static`, isVisibleAcrossFiles dropped it and the
call was left unlinked, so no call reached redis's `server.c` `mstime()`
or `ustime()`.

- A source file's `static` function is visible to a file that `#include`s
  that source file: hiredis's async.c includes dict.c, redis's ae.c its
  event-loop backend.
- The coordinator's `resolveImport` hook falls back to matchByFilePath for
  a C/C++ `#include` the import resolver can't place, as resolveOneInner
  already does, so cppIncluders sees includes only the end of their path
  names: protobuf's `google/protobuf/descriptor.upb.h`, which two generated
  copies end in, and jemalloc's `jemalloc/internal/*.h`. Standard headers
  are left out: otherwise `#include <math.h>` makes jemalloc's
  test/math.h visible to its own include through isIncludedCppTestSuite.

A/B on the cgstd kit against main 4a591aee: leveldb, fmt and rocksdb
identical; redis +194 calls linked and 115 retargeted (114 to hiredis
alloc.h's POSIX `static inline`s, 1 known miss: lua-cjson's fpconv_init to
an inline only `#ifdef USE_INTERNAL_FPCONV` builds use); protobuf 412 upb
calls retargeted from the unincluded php-upb.h amalgamation to the headers
the calling file includes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-16 04:34:50 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 4a591aeee8
commit dc4ccf6b14
5 changed files with 160 additions and 8 deletions
+4 -2
View File
@@ -3,8 +3,10 @@
* then the files that include those, out to the translation units.
*
* Read from the `import` node extraction mints for every `#include`, each
* resolved by the import resolver exactly as the graph's include edge is, so
* "includes" here is the include edge a reader sees. The name matcher asks it
* resolved by the coordinator's import lookup as the graph's include edge is
* (by the end of its path when nothing else places it, and never to a project
* file for a standard header like `<math.h>`), so "includes" here is the
* include edge a reader sees. The name matcher asks it
* whether a C or C++ file named like a test is part of a reference's
* translation unit: protobuf's conformance framework lives in
* `conformance/conformance_test.h` and `test_runner.h`, which the suites and
+10 -2
View File
@@ -21,7 +21,7 @@ import {
isImportableKind,
CPP_DEFINE_SIGNATURE,
} from './types';
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, GO_TYPE_KINDS, matchGoAssertedCall, isGoAssertedLocal } from './name-matcher';
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, GO_TYPE_KINDS, matchGoAssertedCall, isGoAssertedLocal } from './name-matcher';
import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility';
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
import { isCppSupertypeRef, matchCppSupertype, clearCppSupertypeMemos } from './cpp-supertypes';
@@ -668,7 +668,15 @@ export class ReferenceResolver {
*/
private createContext(): ResolutionContext {
return {
resolveImport: (ref) => resolveViaImport(ref, this.context),
// A C or C++ `#include` the import resolver can't place is linked by the
// end of its path, as resolveOneInner's name matching does: protobuf's
// `google/protobuf/descriptor.upb.h`, which two generated copies end in.
// cppIncluders reads includes through this, so it sees the include edges
// a reader sees. A standard header is never the project's: `<math.h>`
// is not jemalloc's test/math.h, which only its includers may see.
resolveImport: (ref) => resolveViaImport(ref, this.context) ??
((ref.language === 'c' || ref.language === 'cpp') && ref.referenceKind === 'imports' &&
!isExternalImport(ref.referenceName, ref.language, this.context) ? matchByFilePath(ref, this.context) : null),
// A path alias makes an import the project's only when it maps the
// import to a file the index holds. Matching its prefix is not enough:
// cord-field's `"*": ["./typings/*"]` matches every package, and
+16 -4
View File
@@ -3008,7 +3008,10 @@ export function isVisibleAcrossFiles(candidate: Node, ref: UnresolvedRef, contex
(!isCsharpTypeVisible(candidate, ref, context) || !isCsharpNestedTypeInScope(candidate, ref, context))) return false;
const lang = candidate.language as string;
if (lang === 'c' || lang === 'cpp') {
if (candidate.kind === 'function' && C_SOURCE_EXT.test(candidate.filePath) && isStaticCFunction(candidate, context)) return false;
// A source file's `static` function, only for that file and one that
// `#include`s it: hiredis's async.c includes dict.c.
if (candidate.kind === 'function' && C_SOURCE_EXT.test(candidate.filePath) && isStaticCFunction(candidate, context) &&
!cppIncluders(candidate.filePath, context).has(ref.filePath)) return false;
return !isCppTranslationUnitLocal(candidate, ref, context);
}
if (lang === 'go') {
@@ -11211,14 +11214,23 @@ function cppArgumentTypeChoice(tied: Node[], name: string, args: string[], ref:
* where it is; protobuf's `ParseWithLimit(limit, data, result)` on an
* `int32_t result` is the overload taking an `int32_t&`, not the
* `int64_t&` one nearer the call. A function hides a class of its name, so
* a call is to the function when `chosen` is that class. `chosen` itself
* for anything but a C++ call, or when no function is in sight.
* a call is to the function when `chosen` is that class. C has no
* overloads: a C call reaches the function of the name its file sees — its
* own or one a header it includes defines, else one with external linkage —
* the best placed of them (findBestMatch). jemalloc's tests call the
* `static inline do_arena_create` of the arena_util.h they include, not a
* neighbouring test's own `static do_arena_create`. `chosen` itself for
* anything but a C or C++ call, or when no function is in sight.
*/
function cppFunctionOverload(chosen: Node, candidates: Node[], ref: UnresolvedRef, context: ResolutionContext): Node {
if (ref.language !== 'cpp' || ref.referenceKind !== 'calls') return chosen;
if ((ref.language !== 'cpp' && ref.language !== 'c') || ref.referenceKind !== 'calls') return chosen;
const overloads = candidates.filter((n) => n.kind === 'function' && n.qualifiedName === chosen.qualifiedName &&
isVisibleAcrossFiles(n, ref, context));
if (overloads.length === 0) return chosen;
if (ref.language === 'c') {
const own = overloads.filter((n) => n.filePath === ref.filePath || cppIncluders(n.filePath, context).has(ref.filePath));
return findBestMatch(ref, own.length > 0 ? own : overloads, context) ?? chosen;
}
return cppOverloadFor(overloads.includes(chosen) ? [chosen, ...overloads.filter((n) => n !== chosen)] : overloads, chosen.name, ref, context);
}