mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
fix(c): a header's definition for a flag nothing sets, like lua-cjson's #ifdef USE_INTERNAL_FPCONV fpconv_init, gives way to the source file the build compiles (#2615)
lua-cjson's fpconv.h defines `static inline void fpconv_init()` only under
`#ifdef USE_INTERNAL_FPCONV` and otherwise declares it `extern` for fpconv.c
to define. Nothing in redis's tree sets the flag, so its build calls
fpconv.c's, but #2613's rule (the file's own or an included header's
definition first) took the header's.
c-feature-variants.ts decides whether a line is compiled only by a build
that sets a flag nothing sets. It reads the conditionals around the line
(cpp-macro-visibility's directiveLines, now exported): `#ifdef`/`#ifndef`,
and `#if` with defined, !, &&, ||, parentheses, integers and bare flags,
then `#elif` and `#else`. A flag is set nowhere when the line's own file
doesn't define it, no build file from the project root down to the
definition or the call names it, and no C/C++ file of their components
(the tree under the nearest directory with a build file) defines it.
Reserved and platform names stay unknown.
cppFunctionOverload's C branch then takes a non-static, non-variant
definition outside the file's tier when one exists, when every definition
the file sees is such a variant, and when the call itself is compiled by
the default build. A branch around both the definition and the call decides
nothing: redis's quicklist.c tests call their own `static mstime` inside
`#ifdef REDIS_TEST`.
A/B against main dc4ccf6b: leveldb, fmt, rocksdb and protobuf identical;
redis 1 call retargeted, lua_cjson.c's fpconv_init() from fpconv.h to
fpconv.c. Indexing time within noise (redis 10.5-11.9 s on main, 10.2-12.3 s
here).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -102,7 +102,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In C++, a method defined by a macro of the same file is now indexed where the file invokes the macro, so calls to it link. protocolbuffers/protobuf's descriptor classes only declare most of their accessors and define them further down with macros like `PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, number, int)`, and its reflection defines `GetInt32`, `SetInt32` and the like by pasting names together in a macro. Those methods had no node at all, so calls like `field->number()`, `descriptor->full_name()` or `reflection->GetInt32(…)` linked to nothing. Calls through what such a method returns, like `field->containing_type()->full_name()`, now link too. A macro defined in another file, and a method declared but defined nowhere in the repository, are still not indexed. Re-index C++ projects after upgrading.
|
||||
- In C++, an explicit specialization of a function or member function template, like protocolbuffers/protobuf's `template <> bool WireFormatLite::ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>(…)` or facebook/rocksdb's `template <> int64_t Deserialize<int64_t>(…)`, is now indexed under its template's name, so search finds it and calls link to it. Before, it kept its template arguments in its name, and one with a `::` among them was named after the last part, like `TYPE_INT32>`. A call that writes template arguments, like `Deserialize<int64_t>(src, offset)`, links to the specialization written with the same ones, and otherwise to the template itself, as fmt's `num_bits<unsigned short>()` does; a call that writes none links to a specialization only when nothing else has that name. Re-index C++ projects after upgrading.
|
||||
- In C++, a call to an overloaded function outside a class now links to the overload its arguments fit, by how many it passes and the types they have where the call writes them, as a call to a method already did: protocolbuffers/protobuf's `ParseWithLimit(limit, data, result)` on an `int32_t result` reaches the overload taking an `int32_t&`, and facebook/rocksdb's `NewLRUCache(cache_opts)` the one taking an `LRUCacheOptions`. Before, the overload nearest the call was taken. Such a call is never linked to an overload only another source file can see, like another `.cc` file's `static` function: it now links to the one its own file sees, where before it often linked to nothing. A call to a name that both a function and a class have reaches the function, which hides the class, and a parameter named like the function it belongs to, like protobuf's `std::string u64(uint64_t u64)`, now has its own type. Re-index C++ projects after upgrading.
|
||||
- In C, a call now links to the function of that name its own file can see: its own, else one a header it includes defines, like a `static inline` helper, else one another source file defines without `static`. Before, the function nearest the call was taken, and when that was another source file's `static` function, which no other file can call, the call linked to nothing: in redis, the calls to `server.c`'s `mstime()` and `ustime()` from the rest of the server were lost to the `static` copies other files keep for themselves, and jemalloc's tests' calls to the `do_arena_create` their shared header defines to a neighbouring test's own. A call to a `static inline` function that several headers define reaches the copy in a header the file includes, so protocolbuffers/protobuf's upb code no longer links to the copies in its PHP extension's combined `php-upb.h`. A source file that `#include`s another, like redis's `ae.c` with its event-loop backend or hiredis's `async.c` with `dict.c`, now calls that file's `static` functions, and a header a file includes through an include directory, like jemalloc's `#include "jemalloc/internal/bit_util.h"`, counts as included. Re-index C projects after upgrading.
|
||||
- In C, a call now links to the function of that name its own file can see: its own, else one a header it includes defines, like a `static inline` helper, else one another source file defines without `static`. Before, the function nearest the call was taken, and when that was another source file's `static` function, which no other file can call, the call linked to nothing: in redis, the calls to `server.c`'s `mstime()` and `ustime()` from the rest of the server were lost to the `static` copies other files keep for themselves, and jemalloc's tests' calls to the `do_arena_create` their shared header defines to a neighbouring test's own. A call to a `static inline` function that several headers define reaches the copy in a header the file includes, so protocolbuffers/protobuf's upb code no longer links to the copies in its PHP extension's combined `php-upb.h`. A source file that `#include`s another, like redis's `ae.c` with its event-loop backend or hiredis's `async.c` with `dict.c`, now calls that file's `static` functions, and a header a file includes through an include directory, like jemalloc's `#include "jemalloc/internal/bit_util.h"`, counts as included. A function a header defines only for a build that turns on a flag, like lua-cjson's `fpconv_init` under `#ifdef USE_INTERNAL_FPCONV`, gives way to another source file's definition when nothing in the project's code or build files sets that flag, so redis's calls to `fpconv_init()` reach the `fpconv.c` its build compiles. Re-index C projects after upgrading.
|
||||
- In C++, text inside a string literal is no longer taken for a variable's declaration. That covers the C code protocolbuffers/protobuf's upb generator writes in a raw string (`R"cc( … upb_MiniTableField field = $field_init$; … )cc"`) and log messages like facebook/rocksdb's `"… in WriteBatch handler; …"`. Before, a later call like `field.ctype()` on the function's own `upb::FieldDefPtr field` parameter, or `handler->PutCF(…)` on a `WriteBatch::Handler* handler`, was matched against the type that text seemed to name, so it linked to nothing or to another class's method. Re-index C++ projects after upgrading.
|
||||
- In C++, a call on an `auto` variable now links to the method of the class C++ finds for the type its initializer gives it, as a call on a variable declared with that type does. The type is read from a cast or smart pointer written out, like facebook/rocksdb's `auto iter = std::unique_ptr<Iterator>(…)` or `auto* mutex = static_cast<InstrumentedMutex*>(arg)`, from `new` or `std::make_unique` of a class, and from what a function or method is declared to return, like the `Iterator*` of `txn->GetIterator(…)`, read to the end of an initializer that goes on to the next line. Before, such a variable took the type of an earlier variable of the same name, or none: in RocksDB, `iter->SeekToFirst()` reached `MemTableRep::Iterator` and `mutex->Lock()` reached `port::Mutex`. A chained call like `cfd->current()->storage_info()` now looks up the class the inner call is declared to return the same way, through a smart pointer too, so protocolbuffers/protobuf's Java generator no longer reaches the C# generator's `EnumGenerator`. A type from outside the project no longer links to a class of the project that shares its name: protobuf's `status.ok()` on an `absl::Status` reached `upb::Status::ok`, and fmtlib/fmt's `os.flush()` on a `std::ostream&` reached fmt's own `ostream::flush`. The same holds for a variable declared in a macro body, or typed by what a function in another file returns, and for a class the index doesn't hold whose name only another class shares: protobuf's `_status.ok()` in its `RETURN_IF_ERROR` macro reached `upb::Status::ok`, and `range.options()` on its generated `DescriptorProto::ExtensionRange` reached `Descriptor::ExtensionRange::options`. A namespace a macro stands for is the project's under either name: facebook/rocksdb declares its classes in `namespace ROCKSDB_NAMESPACE` with `#define ROCKSDB_NAMESPACE rocksdb`, so `db->Put(…)` on a `rocksdb::DB*` links to RocksDB's own `DB::Put`. Re-index C++ projects after upgrading.
|
||||
- In C++, a call on a data member of the calling method's class, like `versions_->GetColumnFamilySet()` in facebook/rocksdb's `db_impl_write.cc`, now links to the method of the member's type wherever the class is declared. Before, only the call's own file and the header of the same name were read for the member, and RocksDB declares `DBImpl` in `db_impl.h` while its methods are spread over `db_impl_write.cc`, `db_impl_compaction_flush.cc`, `db_impl_open.cc` and more, so such calls were guessed by the member's name. A member declared below an inline method that uses it, one a base class declares, and, in a gtest `TEST_F` or `TEST_P` body, one of the test's fixture, like RocksDB's `db_`, are found the same way, and a member of a class nested in the caller's class is no longer taken for the caller's own. A call on a member of a standard-library type, like `std::atomic<bool> shutting_down_`, no longer links to a project method that only shares the method's name. Re-index C++ projects after upgrading.
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* A C call the file's headers answer only when the build sets a flag nothing
|
||||
* in the project sets reaches the definition the build without it calls.
|
||||
* lua-cjson's fpconv.h defines `fpconv_init` under `#ifdef
|
||||
* USE_INTERNAL_FPCONV` and otherwise declares it for fpconv.c to define; the
|
||||
* included header's definition was taken, though no build of redis compiles
|
||||
* 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';
|
||||
import type { Node } from '../src/types';
|
||||
|
||||
const lines = (...l: string[]): string => [...l, ''].join('\n');
|
||||
|
||||
describe('a C call past a definition only a flag nothing sets compiles', () => {
|
||||
let root: string;
|
||||
let cg: CodeGraph;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-c-feature-variants-'));
|
||||
const write = (file: string, text: string): void => {
|
||||
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, file), text);
|
||||
};
|
||||
// lua-cjson in redis: the header's definition is for a build that sets USE_INTERNAL_FPCONV.
|
||||
write('deps/lua/src/fpconv.h', lines(
|
||||
'#ifndef FPCONV_H',
|
||||
'#define FPCONV_H',
|
||||
'#ifdef USE_INTERNAL_FPCONV',
|
||||
'static inline void fpconv_init()',
|
||||
'{',
|
||||
'}',
|
||||
'#else',
|
||||
'extern void fpconv_init();',
|
||||
'#endif',
|
||||
'#endif',
|
||||
));
|
||||
write('deps/lua/src/fpconv.c', lines('#include "fpconv.h"', 'void fpconv_init()', '{', '}'));
|
||||
// ...nor the internal one's own definition.
|
||||
write('deps/lua/src/dtoa_fpconv.c', lines('#ifdef USE_INTERNAL_FPCONV', 'void fpconv_init()', '{', '}', '#endif'));
|
||||
write('deps/lua/src/lua_cjson.c', lines('#include "fpconv.h"', 'int luaopen_cjson(void) { fpconv_init(); return 0; }'));
|
||||
// hiredis: the header's definition is for the build that leaves the flag off.
|
||||
write('deps/hiredis/alloc.h', lines(
|
||||
'#ifndef HIREDIS_ALLOC_H',
|
||||
'#define HIREDIS_ALLOC_H',
|
||||
'#ifndef HIREDIS_EXTERN_ALLOC',
|
||||
'static inline void hi_free(void *ptr) {',
|
||||
'}',
|
||||
'#else',
|
||||
'void hi_free(void *ptr);',
|
||||
'#endif',
|
||||
'#endif',
|
||||
));
|
||||
write('deps/hiredis/alloc_ext.c', lines('void hi_free(void *ptr) {', '}'));
|
||||
write('deps/hiredis/sds.c', lines('#include "alloc.h"', 'void sdsfree(void *s) { hi_free(s); }'));
|
||||
// A flag the project defines may be set.
|
||||
write('include/config.h', lines('#define USE_FAST_CRC 1'));
|
||||
write('include/crc.h', lines(
|
||||
'#ifdef USE_FAST_CRC',
|
||||
'static inline unsigned crc32c(unsigned crc) { return crc; }',
|
||||
'#else',
|
||||
'unsigned crc32c(unsigned crc);',
|
||||
'#endif',
|
||||
));
|
||||
write('src/crc.c', lines('unsigned crc32c(unsigned crc) { return ~crc; }'));
|
||||
write('src/store.c', lines('#include "include/config.h"', '#include "include/crc.h"', 'unsigned checksum(void) { return crc32c(0); }'));
|
||||
// So may one a build file names.
|
||||
write('lib/Makefile', lines('CFLAGS += -DUSE_SIMD_HASH'));
|
||||
write('lib/hash.h', lines('#ifdef USE_SIMD_HASH', 'static inline unsigned hash_bytes(const char *p) { return 0; }', '#endif'));
|
||||
write('lib/hash_scalar.c', lines('unsigned hash_bytes(const char *p) { return 1; }'));
|
||||
write('lib/table.c', lines('#include "hash.h"', 'unsigned slot(const char *k) { return hash_bytes(k); }'));
|
||||
// And one of the platform's.
|
||||
write('include/clock.h', lines('#ifdef __linux__', 'static inline long clock_ns(void) { return 0; }', '#else', 'long clock_ns(void);', '#endif'));
|
||||
write('src/clock_posix.c', lines('long clock_ns(void) { return 1; }'));
|
||||
write('src/timer.c', lines('#include "include/clock.h"', 'long tick(void) { return clock_ns(); }'));
|
||||
// `#if` expressions, `#elif` and `#else`.
|
||||
write('include/codec.h', lines(
|
||||
'#if defined(CODEC_ZSTD) && !defined(CODEC_DISABLE)',
|
||||
'static inline int codec_level(void) { return 3; }',
|
||||
'#elif CODEC_LZ4',
|
||||
'static inline int codec_bound(int n) { return n; }',
|
||||
'#else',
|
||||
'static inline int codec_name(void) { return 0; }',
|
||||
'#endif',
|
||||
));
|
||||
write('src/codec_impl.c', lines(
|
||||
'int codec_level(void) { return 1; }',
|
||||
'int codec_bound(int n) { return n + 1; }',
|
||||
'int codec_name(void) { return 1; }',
|
||||
));
|
||||
write('src/pack.c', lines('#include "include/codec.h"', 'int pack(void) { return codec_level() + codec_bound(1) + codec_name(); }'));
|
||||
// An `#else` after a test that holds.
|
||||
write('include/log.h', lines(
|
||||
'#ifndef LOG_EXTERNAL',
|
||||
'static inline void log_write(const char *m) {}',
|
||||
'#else',
|
||||
'static inline void log_flush(void) {}',
|
||||
'#endif',
|
||||
));
|
||||
write('src/log_ext.c', lines('void log_flush(void) {}'));
|
||||
write('src/server.c', lines('#include "include/log.h"', 'void shutdown_server(void) { log_flush(); }'));
|
||||
// A header that defines the function for either build.
|
||||
write('include/prof.h', lines(
|
||||
'#ifdef PROF_ENABLED',
|
||||
'static inline int prof_tick(void) { return 1; }',
|
||||
'#else',
|
||||
'static inline int prof_tick(void) { return 0; }',
|
||||
'#endif',
|
||||
));
|
||||
write('src/prof_impl.c', lines('int prof_tick(void) { return 2; }'));
|
||||
write('src/loop.c', lines('#include "include/prof.h"', 'int loop_tick(void) { return prof_tick(); }'));
|
||||
// redis's quicklist.c: a call compiled only with the flag reaches what the flag compiles with it.
|
||||
write('src/quicklist.c', lines(
|
||||
'#ifdef REDIS_TEST',
|
||||
'static long long ustime(void) { return 0; }',
|
||||
'int quicklistTest(void) { return (int)ustime(); }',
|
||||
'#endif',
|
||||
));
|
||||
write('src/server_time.c', lines('long long ustime(void) { return 1; }'));
|
||||
// ...in another file too.
|
||||
write('include/simd.h', lines('#ifdef USE_SIMD_CRC', 'static inline unsigned crc_simd(unsigned c) { return c; }', '#endif'));
|
||||
write('src/crc_soft.c', lines('unsigned crc_simd(unsigned c) { return ~c; }'));
|
||||
write('src/checksum_simd.c', lines('#include "include/simd.h"', '#ifdef USE_SIMD_CRC', 'unsigned fast_sum(void) { return crc_simd(0); }', '#endif'));
|
||||
// A flag another component defines is not this one's.
|
||||
write('fpx/Makefile', lines('all: fpx.o use.o'));
|
||||
write('fpx/fpx.h', lines('#ifdef FPX_FAST', 'static inline int fpx_round(int v) { return v; }', '#endif'));
|
||||
write('fpx/fpx.c', lines('int fpx_round(int v) { return v + 1; }'));
|
||||
write('fpx/use.c', lines('#include "fpx.h"', 'int use_round(void) { return fpx_round(1); }'));
|
||||
write('other/config.h', lines('#define FPX_FAST 1'));
|
||||
// A test in a comment is no test.
|
||||
write('include/api.h', lines('/*', '#ifdef OLD_API', '*/', 'static inline int api_version(void) { return 2; }'));
|
||||
write('src/api_v1.c', lines('int api_version(void) { return 1; }'));
|
||||
write('src/client.c', lines('#include "include/api.h"', 'int client_version(void) { return api_version(); }'));
|
||||
cg = await CodeGraph.init(root, { index: true });
|
||||
}, 120_000);
|
||||
|
||||
afterAll(() => {
|
||||
cg?.close();
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
/** `<file>:<line>` of what the function `name` calls, sorted. */
|
||||
const calls = (name: string): string[] => {
|
||||
const caller = cg.getNodesByKind('function').find((n: Node) => n.name === name)!;
|
||||
expect(caller, name).toBeDefined();
|
||||
return cg.getCallees(caller.id).filter((r) => r.edge.kind === 'calls').map((r) => `${r.node.filePath}:${r.node.startLine}`).sort();
|
||||
};
|
||||
|
||||
it('the definition the build without the flag compiles', () => {
|
||||
expect(calls('luaopen_cjson')).toEqual(['deps/lua/src/fpconv.c:2']);
|
||||
expect(calls('use_round')).toEqual(['fpx/fpx.c:1']);
|
||||
});
|
||||
|
||||
it("the header's own, when the build without the flag compiles it", () => {
|
||||
expect(calls('sdsfree')).toEqual(['deps/hiredis/alloc.h:4']);
|
||||
});
|
||||
|
||||
it("the header's own, when the project or its build may set the flag", () => {
|
||||
expect(calls('checksum')).toEqual(['include/crc.h:2']);
|
||||
expect(calls('slot')).toEqual(['lib/hash.h:2']);
|
||||
expect(calls('tick')).toEqual(['include/clock.h:2']);
|
||||
});
|
||||
|
||||
it('reads `#if` expressions, `#elif` and `#else`', () => {
|
||||
expect(calls('pack')).toEqual(['include/codec.h:6', 'src/codec_impl.c:1', 'src/codec_impl.c:2']);
|
||||
expect(calls('shutdown_server')).toEqual(['src/log_ext.c:1']);
|
||||
});
|
||||
|
||||
it('the definition the flag compiles with a call it compiles too', () => {
|
||||
expect(calls('quicklistTest')).toEqual(['src/quicklist.c:2']);
|
||||
expect(calls('fast_sum')).toEqual(['include/simd.h:2']);
|
||||
});
|
||||
|
||||
it("the header's own, when it defines the function for either build", () => {
|
||||
const callees = calls('loop_tick');
|
||||
expect(callees).toHaveLength(1);
|
||||
expect(callees[0]).toMatch(/^include\/prof\.h:/);
|
||||
});
|
||||
|
||||
it('reads no test in a comment', () => {
|
||||
expect(calls('client_version')).toEqual(['include/api.h:4']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* Is a C function defined only for a build that turns on a flag nothing in
|
||||
* the project turns on? lua-cjson's fpconv.h defines `static inline void
|
||||
* fpconv_init()` under `#ifdef USE_INTERNAL_FPCONV` and otherwise declares it
|
||||
* `extern`, for fpconv.c to define. Nothing in redis's tree sets
|
||||
* USE_INTERNAL_FPCONV, so its build compiles fpconv.c's.
|
||||
*
|
||||
* The name matcher asks this only when choosing among definitions of one
|
||||
* name, where a call has to reach one of them: of a flag nothing sets, the
|
||||
* build to follow is the one that leaves it off. Whether a macro is visible
|
||||
* at a call (./cpp-macro-visibility) keeps such a flag unknown instead, since
|
||||
* there an unknown flag must not suppress anything.
|
||||
*
|
||||
* A branch is off only when flags decide its test: `#ifdef X`, `#ifndef X`,
|
||||
* `#if defined(X)`, `#if X`, `#if 0`, with `!`, `&&`, `||` and parentheses,
|
||||
* and the `#elif` and `#else` after them. A flag is set nowhere when the
|
||||
* definition's own file doesn't `#define` it, no build file in the
|
||||
* directories from the project root down to the definition or the call names
|
||||
* it (a Makefile's `-DX`, CMake's `option(X …)`, configure's `AC_DEFINE(X)`)
|
||||
* and no C or C++ file of their components does — a component being the
|
||||
* tree under the nearest directory with a build file, redis's `deps/lua/src`
|
||||
* for fpconv.h. A macro of the compiler or the platform — a reserved name
|
||||
* like `_WIN32` or `__linux__`, or `WIN32`, `linux`, `unix` — is unknown, and
|
||||
* so is any other test (`#if LEVEL > 1`).
|
||||
*/
|
||||
import type { Node } from '../types';
|
||||
import type { ResolutionContext } from './types';
|
||||
import { directiveLines } from './cpp-macro-visibility';
|
||||
|
||||
/** Three-valued: true / false / undefined = the build may go either way. */
|
||||
type Truth = boolean | undefined;
|
||||
type Op = 'if' | 'ifdef' | 'ifndef' | 'elifdef' | 'elifndef' | 'elif' | 'else' | 'endif';
|
||||
interface Directive { op: Op; expression: string; line: number }
|
||||
interface FileDirectives { conditionals: Directive[]; defines: Set<string> }
|
||||
|
||||
interface Memo {
|
||||
files: Map<string, FileDirectives>;
|
||||
/** Directory → the text of the build files directly in it. */
|
||||
buildText: Map<string, string>;
|
||||
/** Directory → the nearest directory at or above it with a build file ('' for the project root). */
|
||||
components: Map<string, string>;
|
||||
/** The project's C and C++ files, once. */
|
||||
cFiles: string[] | null;
|
||||
/** `${flag}\0${component}` → whether a C or C++ file of the component `#define`s it. */
|
||||
defines: Map<string, boolean>;
|
||||
/** Flag → the build-file test for its name. */
|
||||
named: Map<string, RegExp>;
|
||||
answers: Map<string, boolean>;
|
||||
}
|
||||
|
||||
const MEMO = new WeakMap<ResolutionContext, Memo>();
|
||||
|
||||
export function clearCFeatureVariantMemos(context: ResolutionContext): void {
|
||||
MEMO.delete(context);
|
||||
}
|
||||
|
||||
function memoFor(context: ResolutionContext): Memo {
|
||||
let memo = MEMO.get(context);
|
||||
if (!memo) {
|
||||
memo = { files: new Map(), buildText: new Map(), components: new Map(), cFiles: null, defines: new Map(), named: new Map(), answers: new Map() };
|
||||
MEMO.set(context, memo);
|
||||
}
|
||||
return memo;
|
||||
}
|
||||
|
||||
const and = (a: Truth, b: Truth): Truth => (a === false || b === false ? false : a === true && b === true ? true : undefined);
|
||||
const or = (a: Truth, b: Truth): Truth => (a === true || b === true ? true : a === false && b === false ? false : undefined);
|
||||
const not = (a: Truth): Truth => (a === undefined ? undefined : !a);
|
||||
|
||||
const C_FAMILY_FILE = /\.(c|h|cc|cpp|cxx|c\+\+|hpp|hh|hxx|h\+\+|inl|ipp|tcc|inc|m|mm)$/i;
|
||||
const PLATFORM_FLAGS = new Set(['WIN32', 'WIN64', 'linux', 'unix']);
|
||||
const BUILD_FILES = [
|
||||
'Makefile', 'makefile', 'GNUmakefile', 'Makefile.am', 'Makefile.in', 'CMakeLists.txt', 'meson.build',
|
||||
'meson_options.txt', 'configure.ac', 'configure.in', 'config.h.in', 'BUILD', 'BUILD.bazel', 'SConstruct', 'SConscript',
|
||||
];
|
||||
|
||||
const dirOf = (file: string): string => file.slice(0, Math.max(0, file.lastIndexOf('/')));
|
||||
|
||||
/** `a/b/c.h` → `a/b`, `a`, `` (the project root). */
|
||||
function ancestors(file: string): string[] {
|
||||
const dirs: string[] = [];
|
||||
for (let dir = dirOf(file); ; dir = dirOf(dir)) {
|
||||
dirs.push(dir);
|
||||
if (dir === '') return dirs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is `fn`'s definition compiled only by a build that sets a flag nothing in
|
||||
* the project sets, for the call at `ref`? A branch around both the
|
||||
* definition and the call, in the call's own file, decides nothing.
|
||||
*/
|
||||
export function isCFeatureVariant(fn: Node, ref: { filePath: string; line: number }, context: ResolutionContext): boolean {
|
||||
const memo = memoFor(context);
|
||||
const sameFile = fn.filePath === ref.filePath;
|
||||
const key = `v\0${fn.id}\0${sameFile ? ref.line : dirOf(ref.filePath)}`;
|
||||
const hit = memo.answers.get(key);
|
||||
if (hit !== undefined) return hit;
|
||||
const file = directivesOf(fn.filePath, context, memo);
|
||||
let groups = enclosingTests(file.conditionals, fn.startLine);
|
||||
if (sameFile) {
|
||||
const around = enclosingTests(file.conditionals, ref.line);
|
||||
let shared = 0;
|
||||
while (shared < groups.length && shared < around.length &&
|
||||
groups[shared]![0] === around[shared]![0] && groups[shared]!.length === around[shared]!.length) shared++;
|
||||
groups = groups.slice(shared);
|
||||
}
|
||||
const off = groups.length > 0 && isOff(groups, file, [fn.filePath, ref.filePath], context, memo);
|
||||
memo.answers.set(key, off);
|
||||
return off;
|
||||
}
|
||||
|
||||
/** Is line `line` of `path` compiled only by a build that sets a flag nothing in the project sets? */
|
||||
export function isOffByDefault(path: string, line: number, context: ResolutionContext): boolean {
|
||||
const memo = memoFor(context);
|
||||
const key = `o\0${path}\0${line}`;
|
||||
const hit = memo.answers.get(key);
|
||||
if (hit !== undefined) return hit;
|
||||
const file = directivesOf(path, context, memo);
|
||||
const groups = enclosingTests(file.conditionals, line);
|
||||
const off = groups.length > 0 && isOff(groups, file, [path], context, memo);
|
||||
memo.answers.set(key, off);
|
||||
return off;
|
||||
}
|
||||
|
||||
/** Is a branch of `groups` off when no flag `scope`'s files see is set? */
|
||||
function isOff(groups: Directive[][], file: FileDirectives, scope: string[], context: ResolutionContext, memo: Memo): boolean {
|
||||
const dirs = [...new Set(scope.flatMap(ancestors))];
|
||||
const flag = (name: string): Truth => {
|
||||
if (name.startsWith('_') || PLATFORM_FLAGS.has(name) || file.defines.has(name)) return undefined;
|
||||
let named = memo.named.get(name);
|
||||
// A word of its own, or a compiler flag's `-DX` / `/DX`.
|
||||
if (!named) memo.named.set(name, (named = new RegExp(`(?:^|[^\\w]|[-/]D)${name}\\b`, 'm')));
|
||||
if (dirs.some((dir) => named!.test(buildTextOf(dir, context, memo)))) return undefined;
|
||||
const components = new Set(scope.map((f) => componentOf(dirOf(f), context, memo)));
|
||||
return [...components].some((component) => isDefinedIn(name, component, context, memo)) ? undefined : false;
|
||||
};
|
||||
return groups.some((tests) => branchTruth(tests, flag) === false);
|
||||
}
|
||||
|
||||
/**
|
||||
* The branches around `line`, innermost last: for each, the tests of its
|
||||
* group's branches up to and including the one `line` is in (`#else` has no
|
||||
* test).
|
||||
*/
|
||||
function enclosingTests(conditionals: Directive[], line: number): Directive[][] {
|
||||
const groups: Directive[][] = [];
|
||||
for (const d of conditionals) {
|
||||
if (d.line >= line) break;
|
||||
if (d.op === 'if' || d.op === 'ifdef' || d.op === 'ifndef') groups.push([d]);
|
||||
else if (d.op === 'endif') groups.pop();
|
||||
else groups[groups.length - 1]?.push(d);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
/** Whether the last branch of `tests` is taken: no earlier test held, and its own does. */
|
||||
function branchTruth(tests: Directive[], flag: (name: string) => Truth): Truth {
|
||||
let earlier: Truth = false;
|
||||
for (let i = 0; i < tests.length - 1; i++) {
|
||||
earlier = or(earlier, testTruth(tests[i]!, flag));
|
||||
if (earlier === true) return false;
|
||||
}
|
||||
return and(not(earlier), testTruth(tests[tests.length - 1]!, flag));
|
||||
}
|
||||
|
||||
function testTruth(d: Directive, flag: (name: string) => Truth): Truth {
|
||||
const name = d.expression.trim().match(/^\w+$/)?.[0];
|
||||
switch (d.op) {
|
||||
case 'ifdef':
|
||||
case 'elifdef':
|
||||
return name ? flag(name) : undefined;
|
||||
case 'ifndef':
|
||||
case 'elifndef':
|
||||
return name ? not(flag(name)) : undefined;
|
||||
case 'else':
|
||||
return true;
|
||||
default:
|
||||
return evaluate(d.expression, flag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A `#if` expression of `defined`, flags, integers, `!`, `&&`, `||` and
|
||||
* parentheses; anything else is unknown. A flag that is set nowhere reads as
|
||||
* `0`, as the preprocessor reads a name no `#define` gave a value.
|
||||
*/
|
||||
function evaluate(expression: string, flag: (name: string) => Truth): Truth {
|
||||
const tokens = expression.match(/0x[\da-f]+\w*|\d\w*|[A-Za-z_]\w*|&&|\|\||\S/gi) ?? [];
|
||||
// Check the shape first, so an unsupported test reads no flag.
|
||||
let i = 0;
|
||||
const shape = (): boolean => {
|
||||
const term = (): boolean => {
|
||||
const t = tokens[i++];
|
||||
if (t === '!') return term();
|
||||
if (t === '(') return shape() && tokens[i++] === ')';
|
||||
if (t === 'defined') {
|
||||
const paren = tokens[i] === '(';
|
||||
if (paren) i++;
|
||||
return /^[A-Za-z_]\w*$/.test(tokens[i++] ?? '') && (!paren || tokens[i++] === ')');
|
||||
}
|
||||
return t !== undefined && (/^(0x[\da-f]+|\d+)[ul]*$/i.test(t) || /^[A-Za-z_]\w*$/.test(t));
|
||||
};
|
||||
if (!term()) return false;
|
||||
while (tokens[i] === '&&' || tokens[i] === '||') {
|
||||
i++;
|
||||
if (!term()) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (tokens.length === 0 || !shape() || i !== tokens.length) return undefined;
|
||||
|
||||
i = 0;
|
||||
const disjunction = (): Truth => {
|
||||
let value = conjunction();
|
||||
while (tokens[i] === '||') {
|
||||
i++;
|
||||
value = or(value, conjunction());
|
||||
}
|
||||
return value;
|
||||
};
|
||||
const conjunction = (): Truth => {
|
||||
let value = term();
|
||||
while (tokens[i] === '&&') {
|
||||
i++;
|
||||
value = and(value, term());
|
||||
}
|
||||
return value;
|
||||
};
|
||||
const term = (): Truth => {
|
||||
const t = tokens[i++]!;
|
||||
if (t === '!') return not(term());
|
||||
if (t === '(') {
|
||||
const value = disjunction();
|
||||
i++;
|
||||
return value;
|
||||
}
|
||||
if (t === 'defined') {
|
||||
if (tokens[i] === '(') {
|
||||
const value = flag(tokens[i + 1]!);
|
||||
i += 3;
|
||||
return value;
|
||||
}
|
||||
return flag(tokens[i++]!);
|
||||
}
|
||||
if (/^\d|^0x/i.test(t)) return Number.parseInt(t.replace(/[ul]+$/i, ''), /^0x/i.test(t) ? 16 : 10) !== 0;
|
||||
return flag(t) === false ? false : undefined;
|
||||
};
|
||||
return disjunction();
|
||||
}
|
||||
|
||||
function directivesOf(file: string, context: ResolutionContext, memo: Memo): FileDirectives {
|
||||
const hit = memo.files.get(file);
|
||||
if (hit) return hit;
|
||||
const lines = directiveLines(context.readFile(file) ?? '');
|
||||
const conditionals: Directive[] = [];
|
||||
const defines = new Set<string>();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const define = lines[i]!.match(/^\s*#\s*define\s+(\w+)/);
|
||||
if (define) {
|
||||
defines.add(define[1]!);
|
||||
continue;
|
||||
}
|
||||
const m = lines[i]!.match(/^\s*#\s*(ifdef|ifndef|if|elifdef|elifndef|elif|else|endif)\b(.*)$/);
|
||||
if (!m) continue;
|
||||
// A test continued with a backslash goes on over the next lines.
|
||||
let expression = m[2]!;
|
||||
for (let j = i; /\\\s*$/.test(lines[j]!) && j + 1 < lines.length; j++) {
|
||||
expression = expression.replace(/\\\s*$/, ' ') + lines[j + 1]!;
|
||||
}
|
||||
conditionals.push({ op: m[1] as Op, expression, line: i + 1 });
|
||||
}
|
||||
const directives = { conditionals, defines };
|
||||
memo.files.set(file, directives);
|
||||
return directives;
|
||||
}
|
||||
|
||||
function buildTextOf(dir: string, context: ResolutionContext, memo: Memo): string {
|
||||
const hit = memo.buildText.get(dir);
|
||||
if (hit !== undefined) return hit;
|
||||
const text = BUILD_FILES.map((name) => context.readFile(dir ? `${dir}/${name}` : name) ?? '').join('\n');
|
||||
memo.buildText.set(dir, text);
|
||||
return text;
|
||||
}
|
||||
|
||||
/** The nearest directory at or above `dir` with a build file, else the project root. */
|
||||
function componentOf(dir: string, context: ResolutionContext, memo: Memo): string {
|
||||
const hit = memo.components.get(dir);
|
||||
if (hit !== undefined) return hit;
|
||||
const component = dir === '' || buildTextOf(dir, context, memo).trim() ? dir : componentOf(dirOf(dir), context, memo);
|
||||
memo.components.set(dir, component);
|
||||
return component;
|
||||
}
|
||||
|
||||
/** Does a C or C++ file under `component` `#define` `name`? Read only from files whose bytes hold the name. */
|
||||
function isDefinedIn(name: string, component: string, context: ResolutionContext, memo: Memo): boolean {
|
||||
const key = `${name}\0${component}`;
|
||||
const hit = memo.defines.get(key);
|
||||
if (hit !== undefined) return hit;
|
||||
memo.cFiles ??= context.getAllFiles().filter((file) => C_FAMILY_FILE.test(file));
|
||||
const prefix = component ? `${component}/` : '';
|
||||
const define = new RegExp(`^[ \\t]*#[ \\t]*define[ \\t]+${name}\\b`, 'm');
|
||||
const defined = memo.cFiles.some((file) =>
|
||||
file.startsWith(prefix) &&
|
||||
(context.fileContains ? context.fileContains(file, name) : true) &&
|
||||
define.test(context.readFile(file) ?? ''));
|
||||
memo.defines.set(key, defined);
|
||||
return defined;
|
||||
}
|
||||
@@ -141,7 +141,7 @@ function summarize(file: string, context: ResolutionContext, cache: Cache): File
|
||||
* generic comment stripper is a regex pass over the whole file and this walk
|
||||
* touches every header of a translation unit, vendor trees included.
|
||||
*/
|
||||
function directiveLines(source: string): string[] {
|
||||
export function directiveLines(source: string): string[] {
|
||||
const out: string[] = [];
|
||||
let inBlock = false;
|
||||
for (const raw of maskCppRawStrings(source).source.split(/\r?\n/)) {
|
||||
|
||||
@@ -32,6 +32,7 @@ import { gateTypeParameter, clearTypeParameterMemos } from './type-parameters';
|
||||
import { gateDartLocal, clearDartLocalScopeMemos } from './dart-local-scope';
|
||||
import { clearCppTypeAliasMemos } from './cpp-type-aliases';
|
||||
import { clearCppIncluderMemos } from './cpp-includers';
|
||||
import { clearCFeatureVariantMemos } from './c-feature-variants';
|
||||
import { matchShopifyThemeFile } from './shopify-themes';
|
||||
import { resolveViaImport, resolvePhpImportedStaticCall, resolvePhpQualifiedClassRef, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, isDartImportRef, isLuaRequireRef, isJsPathImportRef, isBoundToOutOfRepoImport, importBindingTail, clearImportResolverMemos, resolveImportPath, isExternalImport } from './import-resolver';
|
||||
import { ResolverPool, minRefsForPool, shouldEngageAdaptively } from './resolver-pool';
|
||||
@@ -575,6 +576,7 @@ export class ReferenceResolver {
|
||||
clearCppTypeAliasMemos(this.context);
|
||||
clearCppSupertypeMemos(this.context);
|
||||
clearCppIncluderMemos(this.context);
|
||||
clearCFeatureVariantMemos(this.context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { clearCppNamespaceMemos, cppMacroNamespaceFrames, cppMacroNamespaceHeads
|
||||
import { clearCppRawStringMemos, cppLiteralsBlanked, cppNonCodeLines } from './cpp-raw-strings';
|
||||
import { cppClassWritten, type CppNameSite } from './cpp-supertypes';
|
||||
import { cppIncludedFile, cppIncluders } from './cpp-includers';
|
||||
import { isCFeatureVariant, isOffByDefault } from './c-feature-variants';
|
||||
import { isTestPath } from '../search/query-utils';
|
||||
import { isMinifiedContent } from '../extraction/generated-detection';
|
||||
import { getCargoWorkspaceCrateMap } from './frameworks/cargo-workspace';
|
||||
@@ -11219,8 +11220,15 @@ function cppArgumentTypeChoice(tied: Node[], name: string, args: string[], ref:
|
||||
* 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.
|
||||
* neighbouring test's own `static do_arena_create`. What the file sees only
|
||||
* when the build sets a flag nothing in the project sets
|
||||
* (isCFeatureVariant) yields to a definition with external linkage that the
|
||||
* build without it calls: lua-cjson's fpconv.h defines `fpconv_init` under
|
||||
* `#ifdef USE_INTERNAL_FPCONV`, and redis's build calls fpconv.c's. A call
|
||||
* compiled only with such a flag itself is left to the file's own: redis's
|
||||
* quicklist.c tests call its `static mstime` inside `#ifdef REDIS_TEST`.
|
||||
* `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.language !== 'c') || ref.referenceKind !== 'calls') return chosen;
|
||||
@@ -11229,7 +11237,12 @@ function cppFunctionOverload(chosen: Node, candidates: Node[], ref: UnresolvedRe
|
||||
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;
|
||||
const external = own.length > 0 ? overloads.filter((n) => !own.includes(n) && !isStaticCFunction(n, context)) : [];
|
||||
const outside = external.length > 0 && own.every((n) => isCFeatureVariant(n, ref, context)) &&
|
||||
!isOffByDefault(ref.filePath, ref.line, context)
|
||||
? external.filter((n) => !isCFeatureVariant(n, ref, context))
|
||||
: [];
|
||||
return findBestMatch(ref, outside.length > 0 ? outside : own.length > 0 ? own : overloads, context) ?? chosen;
|
||||
}
|
||||
return cppOverloadFor(overloads.includes(chosen) ? [chosen, ...overloads.filter((n) => n !== chosen)] : overloads, chosen.name, ref, context);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user