Files
codegraph/__tests__/cpp-translation-unit.test.ts
T
Colby Mchenry 6f8e6caad9 fix(c,cpp): a source implements the header beside it that it reaches through other headers (#2693)
upb's reflection sources include only their internal header:
upb/reflection/field_def.c includes upb/reflection/internal/field_def.h,
which reaches the upb/reflection/field_def.h it implements through def.h.
So the translation-unit rule of PR 2684 never read field_def.c as that
header's implementation, and 343 calls from protobuf's Python extension,
plus 88 from its Lua binding, still went to the copy of upb that the PHP
extension bundles in php-upb.c.

When a source includes no header named like it in its own folder, one
there that it includes through other headers counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 01:00:25 +00:00

403 lines
11 KiB
TypeScript

/**
* A C or C++ name is one its translation unit declares, in production code
* as in tests (cpp-test-translation-unit.test.ts). Of several same-named
* candidates, what the file's includes declare goes first: protobuf's Python
* extension calls `upb_Encode()` from upb/wire/encode.h, not the copy the PHP
* extension bundles in php-upb.c; its Java generator calls the `HasHasbit()`
* helpers.h declares, not `ImmutableFieldGenerator::HasHasbit`, a method
* beside the caller. A source implements the header beside it (the
* Python extension's pyext/message.cc is no implementation of
* google/protobuf/message.h), even one it reaches only through other
* headers (upb's reflection/field_def.c includes
* reflection/internal/field_def.h, which reaches reflection/field_def.h
* through def.h), a header's inline overload reaches the
* definition its own source holds, and a call keeps a function outside the
* unit that takes its arguments over one inside that can't (glog's
* three-argument `GetStackTrace(…)` is not utilities.h's `GetStackTrace()`).
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cpp-tu-'));
const files: Record<string, string> = {
// upb's encoder, and the copy the PHP extension's amalgamation carries.
'upb/wire/encode.h': `#ifndef UPB_WIRE_ENCODE_H_
#define UPB_WIRE_ENCODE_H_
#include <stddef.h>
int upb_Encode(const void* msg, int options, char** buf, size_t* size);
#endif
`,
'upb/wire/encode.c': `#include "upb/wire/encode.h"
int upb_Encode(const void* msg, int options, char** buf, size_t* size) {
*size = 0;
return 0;
}
`,
'php/ext/google/protobuf/php-upb.h': `#include <stddef.h>
int upb_Encode(const void* msg, int options, char** buf, size_t* size);
int upb_FieldDef_Number(const void* f);
`,
'php/ext/google/protobuf/php-upb.c': `#include "php-upb.h"
int upb_Encode(const void* msg, int options, char** buf, size_t* size) {
*size = 0;
return 0;
}
int upb_FieldDef_Number(const void* f) { return 0; }
`,
// upb's reflection/field_def.c implements reflection/field_def.h, which
// it reaches only through its internal header and def.h.
'upb/reflection/field_def.h': `typedef struct upb_FieldDef upb_FieldDef;
int upb_FieldDef_Number(const upb_FieldDef* f);
`,
'upb/reflection/def.h': `#include "upb/reflection/field_def.h"
`,
'upb/reflection/internal/field_def.h': `#include "upb/reflection/def.h"
int _upb_FieldDef_Index(const upb_FieldDef* f);
`,
'upb/reflection/field_def.c': `#include "upb/reflection/internal/field_def.h"
int upb_FieldDef_Number(const upb_FieldDef* f) { return 1; }
`,
'python/convert.c': `#include "upb/reflection/def.h"
int PyUpb_FieldNumber(const upb_FieldDef* f) {
return upb_FieldDef_Number(f);
}
`,
'python/message.c': `#include "upb/wire/encode.h"
int PyUpb_Message_Serialize(const void* msg) {
char* buf;
size_t size;
return upb_Encode(msg, 0, &buf, &size);
}
`,
// A free function the Java generator's helpers.h declares, and a field
// generator's method of the same name beside the caller.
'src/google/protobuf/compiler/java/helpers.h': `namespace google {
namespace protobuf {
class FieldDescriptor;
namespace compiler {
namespace java {
inline bool HasHasbit(const FieldDescriptor* descriptor) {
return descriptor != nullptr;
}
} // namespace java
} // namespace compiler
} // namespace protobuf
} // namespace google
`,
'src/google/protobuf/compiler/java/full/field_generator.h': `namespace google {
namespace protobuf {
class FieldDescriptor;
namespace compiler {
namespace java {
class ImmutableFieldGenerator {
public:
bool HasHasbit() const;
private:
const FieldDescriptor* descriptor_;
};
} // namespace java
} // namespace compiler
} // namespace protobuf
} // namespace google
`,
'src/google/protobuf/compiler/java/full/field_generator.cc': `#include "google/protobuf/compiler/java/full/field_generator.h"
#include "google/protobuf/compiler/java/helpers.h"
namespace google {
namespace protobuf {
namespace compiler {
namespace java {
bool ImmutableFieldGenerator::HasHasbit() const {
return ::google::protobuf::compiler::java::HasHasbit(descriptor_);
}
} // namespace java
} // namespace compiler
} // namespace protobuf
} // namespace google
`,
'src/google/protobuf/compiler/java/full/enum_field.cc': `#include "google/protobuf/compiler/java/helpers.h"
#include "google/protobuf/compiler/java/full/field_generator.h"
namespace google {
namespace protobuf {
namespace compiler {
namespace java {
namespace {
int SetEnumVariables(const FieldDescriptor* descriptor) {
if (HasHasbit(descriptor)) {
return 1;
}
return 0;
}
} // namespace
} // namespace java
} // namespace compiler
} // namespace protobuf
} // namespace google
`,
// A header's template overload calls the one its own source defines.
'conformance/binary_wireformat.h': `#include <cstdint>
#include <string>
namespace google {
namespace protobuf {
namespace conformance {
using Wire = std::string;
Wire Varint(uint64_t x);
// Overloads to avoid forcing sign/size conversion warnings on call-sites.
template <typename T>
Wire Varint(T x) {
return Varint(static_cast<uint64_t>(x));
}
} // namespace conformance
} // namespace protobuf
} // namespace google
`,
'conformance/binary_wireformat.cc': `#include "conformance/binary_wireformat.h"
namespace google {
namespace protobuf {
namespace conformance {
Wire Varint(uint64_t x) {
return std::string(1, static_cast<char>(x));
}
} // namespace conformance
} // namespace protobuf
} // namespace google
`,
// glog's three-parameter GetStackTrace is declared in stacktrace.h and
// defined in a header stacktrace.cc includes; utilities.h declares a
// GetStackTrace() of no parameters.
'src/stacktrace.h': `namespace google {
namespace glog_internal_namespace_ {
int GetStackTrace(void** result, int max_depth, int skip_count);
} // namespace glog_internal_namespace_
} // namespace google
`,
'src/stacktrace_generic-inl.h': `#include "stacktrace.h"
namespace google {
namespace glog_internal_namespace_ {
int GetStackTrace(void** result, int max_depth, int skip_count) {
return 0;
}
} // namespace glog_internal_namespace_
} // namespace google
`,
'src/stacktrace.cc': `#include "stacktrace.h"
#include "stacktrace_generic-inl.h"
`,
'src/utilities.h': `#include <string>
namespace google {
std::string GetStackTrace();
} // namespace google
`,
'src/utilities.cc': `#include "utilities.h"
namespace google {
std::string GetStackTrace() {
return "";
}
} // namespace google
`,
'src/logging.cc': `#include <string>
#include "stacktrace.h"
#include "utilities.h"
namespace google {
using namespace glog_internal_namespace_;
int RecordCrashReason(void** stack) {
std::string trace = GetStackTrace();
int depth = GetStackTrace(stack, 32, 4);
return depth + static_cast<int>(trace.size());
}
} // namespace google
`,
// The Python extension's message.cc implements pyext/message.h beside
// it, though it includes google/protobuf/message.h too.
'src/google/protobuf/message.h': `namespace google {
namespace protobuf {
class Message {
public:
virtual ~Message() {}
};
} // namespace protobuf
} // namespace google
`,
'src/google/protobuf/any.pb.h': `#include "google/protobuf/message.h"
namespace google {
namespace protobuf {
class Any : public Message {
public:
void MergeFrom(const Any& from) {}
};
} // namespace protobuf
} // namespace google
`,
'src/google/protobuf/descriptor.pb.h': `#include "google/protobuf/message.h"
namespace google {
namespace protobuf {
class FileDescriptorSet : public Message {
public:
void CopyFrom(const FileDescriptorSet& from);
};
} // namespace protobuf
} // namespace google
`,
'src/google/protobuf/descriptor.pb.cc': `#include "google/protobuf/descriptor.pb.h"
namespace google {
namespace protobuf {
void FileDescriptorSet::CopyFrom(const FileDescriptorSet& from) {
if (&from == this) return;
MergeFrom(from);
}
} // namespace protobuf
} // namespace google
`,
'python/google/protobuf/pyext/message.h': `namespace google {
namespace protobuf {
namespace python {
namespace cmessage {
int MergeFrom(void* self, void* arg);
} // namespace cmessage
} // namespace python
} // namespace protobuf
} // namespace google
`,
'python/google/protobuf/pyext/message.cc': `#include "google/protobuf/message.h"
#include "google/protobuf/pyext/message.h"
namespace google {
namespace protobuf {
namespace python {
namespace cmessage {
int MergeFrom(void* self, void* arg) { return 0; }
} // namespace cmessage
} // namespace python
} // namespace protobuf
} // namespace google
`,
};
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 });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
/** `qualifiedName (file)` of every call the file's symbols make. */
const callsFrom = (file: string): string[] => {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'calls').map((e) => {
const target = cg.getNode(e.target)!;
return `${target.qualifiedName} (${target.filePath})`;
});
};
describe('a C/C++ reference links what its translation unit declares', () => {
it('calls the C function its includes declare, not a bundled copy', () => {
const calls = callsFrom('python/message.c');
expect(calls).toContain('upb_Encode (upb/wire/encode.c)');
expect(calls.some((c) => c.includes('php-upb'))).toBe(false);
});
it('reads a source as implementing the header beside it that it reaches through others', () => {
const calls = callsFrom('python/convert.c');
expect(calls).toContain('upb_FieldDef_Number (upb/reflection/field_def.c)');
expect(calls.some((c) => c.includes('php-upb'))).toBe(false);
});
it('calls the free function its includes declare, not a class\'s method beside it', () => {
const calls = callsFrom('src/google/protobuf/compiler/java/full/enum_field.cc');
expect(calls).toContain('google::protobuf::compiler::java::HasHasbit (src/google/protobuf/compiler/java/helpers.h)');
expect(calls.some((c) => c.includes('ImmutableFieldGenerator'))).toBe(false);
});
it('lets a header\'s overload call the definition its own source holds', () => {
const calls = callsFrom('conformance/binary_wireformat.h');
expect(calls).toContain('google::protobuf::conformance::Varint (conformance/binary_wireformat.cc)');
});
it('keeps a call off a function its arguments cannot fill', () => {
const calls = callsFrom('src/logging.cc');
expect(calls).toContain('google::GetStackTrace (src/utilities.cc)');
expect(calls).toContain('google::glog_internal_namespace_::GetStackTrace (src/stacktrace_generic-inl.h)');
});
it('reads a source as implementing the header beside it, not a namesake elsewhere', () => {
const calls = callsFrom('src/google/protobuf/descriptor.pb.cc');
expect(calls.some((c) => c.includes('pyext/message.cc'))).toBe(false);
});
});