Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In a project that uses Vue, an import written through an `@/` or `~/` alias, like `import { Button } from '@/components'`, now links the file it imports, as it does in any other project, so `codegraph sync` and the file watcher update the files that import a module when that module, or a barrel they import through, is added or deleted. Before, Vue support linked such an import to the first declaration or the component in that file, which sync does not follow, and an import through a `tsconfig` alias that starts like one of Nuxt's built-in modules, like `#app/where`, linked the importing file to itself. Re-index Vue projects after upgrading.
- `codegraph sync` and the file watcher now move an import written without an extension, like `import { sizeOf } from './button'`, to a file added beside the one it found before when the new file comes first, such as `button.ts` next to `button.vue`. Before, the import and the names it binds stayed linked to `button.vue` until the importing file changed or the project was re-indexed. JavaScript, TypeScript, Vue, Svelte and Astro.
- In C, C++ and Objective-C, a struct or class member declared as a pointer, reference, array or function, like `SharedState* shared;`, `jv elements[];` or `virtual Status Put(…) = 0;`, no longer makes the struct look like it inherits from the member's type. Before, `codegraph_explore` and impact listed those types as base classes, every struct holding a pointer to a type as one of its subclasses, and a method of that struct as an override of the type's method with the same name. Real base classes like `class Derived : public Base`, Objective-C superclasses and Go's embedded fields are unchanged. Re-index C, C++ and Objective-C projects after upgrading.
- In C and C++, a struct, class or union now links to the types its members are declared with, whether held by value, by pointer, in an array, through a function pointer or inside a template like `std::vector<Foo>`, and a `typedef` or `using` alias links to the type it names, so impact on a type reaches every struct that holds one, like redis's `redisServer` for `pause_event`. A member's type is the one C++ name lookup finds from the class, so a builtin, a macro or a function of the same name links nothing, and a type a macro renames is what the macro names where the file sees it, like hiredis's `#define sds hisds` in redis-cli.c. Re-index C and C++ projects after upgrading.
- In C, C++ and Objective-C, a struct, class or union now links to the types its members are declared with, whether held by value, by pointer, in an array, through a function pointer or inside a template like `std::vector<Foo>`, and a `typedef` or `using` alias links to the type it names, so impact on a type reaches every struct that holds one, like redis's `redisServer` for `pause_event`. A member's type is the one C++ name lookup finds from the class, so a builtin, a macro or a function of the same name links nothing, and a type a macro renames is what the macro names where the file sees it, like hiredis's `#define sds hisds` in redis-cli.c. Re-index C, C++ and Objective-C projects after upgrading.
- In C and C++, a function-pointer table's handler, like `.read = impl_read` in an ops struct, no longer links to a Python or Rust function that only shares its name, and such a function no longer takes the place of the C function the table names. Re-index C and C++ projects that also hold Python or Rust after upgrading. Thanks @bompus.
- In C++, a call on a variable, parameter or member declared through a `typedef` or `using` alias now links to the method of the type the alias names, with the alias looked up the way C++ does it: in the calling function, its class and the classes that class inherits from, then the namespaces around it. In google/leveldb, `table_.Insert(buf)` on a `Table table_;` declared next to `typedef SkipList<const char*, KeyComparator> Table;` now reaches `SkipList::Insert` instead of the unrelated `HandleTable::Insert`, and `Table::Iterator iter(&table_);` reaches `SkipList::Iterator`'s constructor. A class that only shares the alias's name is no longer taken for it, and when the aliased type has no such method, as with an alias of `std::vector` or of a template parameter, no method is guessed from the receiver's name. An alias of a type written from the global scope names that global type, also inside a namespace of the same name: protocolbuffers/protobuf's `using Arena = ::upb::Arena;` in `hpb::internal::backend::upb`, so hpb's `arena_.Fuse(…)` on a `backend::Arena` now reaches `upb::Arena::Fuse` instead of the calling method itself. Re-index C++ projects after upgrading.
- In C++, indexing no longer fails with "Maximum call stack size exceeded" on a call whose arguments call the same overloaded method, like darts-clone's `extras(extras(id).prev()).set_next(…)` on a class with a `const` and a non-`const` `extras(id)`. Choosing the overload read its arguments, which chose the same overload again without end, and the whole index run failed. Such a call now takes the first overload when the choice comes back to itself, as one whose argument types are unknown does. Thanks @JosefAschauer.
Expand Down
196 changes: 196 additions & 0 deletions __tests__/objc-member-type-refs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/**
* An Objective-C struct references the types its members are declared with, as
* a C or C++ one does (#3144), and an alias references what it names. 1.6.2
* linked many of them with a false `extends`; #2394 removed the label and left
* nothing, so on GNUstep's libs-base impact on a class missed every struct that
* holds one.
*
* Only a type can be named there. A struct, alias or enum an `.m` file defines
* is that file's own, and a class's node in some other `.m` (an `@implementation`
* or a category) yields to a header's.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import CodeGraph from '../src/index';
import { ToolHandler } from '../src/mcp/tools';

describe('an Objective-C struct and the types of its members', () => {
let root = '';
let cg: CodeGraph | undefined;

afterEach(() => {
cg?.destroy();
cg = undefined;
if (root) fs.rmSync(root, { recursive: true, force: true });
root = '';
});

/** Index `files` and return what a type references, as `kind name @file`. */
async function index(files: Record<string, string>): Promise<(holder: string, file?: string) => string[]> {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-objc-members-'));
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);
}
const graph = (cg = await CodeGraph.init(root, { index: true }));
return (holder, file) => {
const node = graph.getNodesByName(holder).find((n) =>
(n.kind === 'struct' || n.kind === 'union' || n.kind === 'type_alias') && (!file || n.filePath === file));
expect(node, holder).toBeDefined();
return graph
.getOutgoingEdges(node!.id)
.filter((e) => e.kind === 'references')
.map((e) => graph.getNode(e.target)!)
.map((n) => `${n.kind} ${n.name} @${n.filePath}`)
.sort();
};
}

const types = [
'@interface NSObject',
'@end',
'@interface NSString : NSObject',
'@end',
'@protocol Delegate',
'@end',
'typedef struct pause_event { int paused; } pause_event;',
'struct client { int fd; };',
'enum Color { Red, Green };',
'typedef union { int i; } value;',
'typedef long MyInt;',
'',
].join('\n');

it('links a member declared by value, by pointer, by tag and through a callback', async () => {
const referenced = await index({
'Types.h': types,
'Server.h': [
'#import "Types.h"',
'struct server {',
' pause_event pauses[3];',
' struct client *current;',
' enum Color color;',
' value in_union;',
' MyInt count;',
' NSString *name;',
' pause_event (*callback)(MyInt);',
' int port;',
' char *text;',
'};',
'typedef struct server Server;',
'@interface Controller : NSObject',
'@end',
'',
].join('\n'),
});
expect(referenced('server')).toEqual([
'class NSString @Types.h',
'enum Color @Types.h',
'struct client @Types.h',
'struct pause_event @Types.h',
'struct pause_event @Types.h',
'type_alias MyInt @Types.h',
'type_alias MyInt @Types.h',
'union value @Types.h',
]);
expect(referenced('Server')).toEqual(['struct server @Server.h']);
});

it('names no extends edge, and links no function or variable of the name', async () => {
await index({
'Types.h': [
types,
'void helper(void);',
'int counter;',
'',
].join('\n'),
'Holder.h': [
'#import "Types.h"',
'struct holder { helper *a; counter *b; pause_event c; };',
'@interface H : NSObject',
'@end',
'',
].join('\n'),
});
const holder = cg!.getNodesByName('holder').find((n) => n.kind === 'struct')!;
const edges = cg!.getOutgoingEdges(holder.id);
expect(edges.filter((e) => e.kind === 'extends')).toEqual([]);
expect(edges.filter((e) => e.kind === 'references').map((e) => cg!.getNode(e.target)!.name)).toEqual(['pause_event']);
});

it('a type an `.m` file defines is its own', async () => {
const referenced = await index({
'Types.h': types,
'One.m': [
'#import "Types.h"',
'struct local { int a; };',
'struct mine { struct local *l; };',
'@implementation NSString',
'@end',
'',
].join('\n'),
'Two.m': [
'#import "Types.h"',
'struct other { struct local *l; NSString *s; };',
'',
].join('\n'),
});
expect(referenced('mine')).toEqual(['struct local @One.m']);
expect(referenced('other')).toEqual(['class NSString @Types.h']);
});

it('explore spends no window on a member: a struct\'s types are not its flow', async () => {
const members = Array.from({ length: 600 }, (_, i) => ` Type${i} member_with_a_long_name_${i};`).join('\n');
const types = Array.from({ length: 600 }, (_, i) => `typedef struct Type${i} { int x; } Type${i};`).join('\n');
const referenced = await index({
'Types.h': `@interface NSObject\n@end\n${types}\n`,
'Reader.h': [
'#import "Types.h"',
'@interface ReaderHost : NSObject',
'@end',
'struct Reader {',
members,
'};',
'',
].join('\n'),
'Reader.m': [
'#import "Reader.h"',
'static int lookup(int key) { return key + 1; }',
'static int reader_size(struct Reader *r) { return 1; }',
'int reader_get(struct Reader *r, int key) {',
' return lookup(key) + reader_size(r);',
'}',
'',
].join('\n'),
});
expect(referenced('Reader')).toHaveLength(600);
const result = await new ToolHandler(cg!).execute('codegraph_explore', { query: 'Reader reader_get reader_size lookup' });
const text = result.content[0]!.text;
expect(text).toContain('return lookup(key) + reader_size(r);');
expect(text).not.toContain('(references)');
expect(text).not.toContain('member_with_a_long_name_50');
});

it('a member\'s type links once its file is added', async () => {
const referenced = await index({
'Holder.h': '#import "Late.h"\nstruct holder { late_t *l; };\n@interface H\n@end\n',
});
expect(referenced('holder')).toEqual([]);
fs.writeFileSync(path.join(root, 'Late.h'), 'typedef struct late { int x; } late_t;\n@interface Late\n@end\n');
await cg!.sync();
expect(referenced('holder')).toEqual(['struct late_t @Late.h']);
});

it('a class is the one in its own header before a category in another', async () => {
const referenced = await index({
'Base.h': '@interface NSObject\n@end\n',
'Widget.h': '#import "Base.h"\n@interface Widget : NSObject\n@end\n',
'Widget+Extra.h': '#import "Widget.h"\n@interface Widget (Extra)\n@end\n',
'Aaa.h': '#import "Widget.h"\n@interface Widget (Aaa)\n@end\n',
'Holder.h': '#import "Widget.h"\nstruct holder { Widget *w; };\n@interface Holder : NSObject\n@end\n',
});
expect(referenced('holder')).toEqual(['class Widget @Widget.h']);
});
});
4 changes: 2 additions & 2 deletions src/extraction/tree-sitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3011,7 +3011,7 @@ export class TreeSitterExtractor {
* #2327). Mirrored in the kernel (ccpp/mod.rs).
*/
private extractCMemberTypeRefs(specifier: SyntaxNode, owner: Node): void {
if ((this.language !== 'c' && this.language !== 'cpp') || isUnnamedMemberType(specifier)) return;
if ((this.language !== 'c' && this.language !== 'cpp' && this.language !== 'objc') || isUnnamedMemberType(specifier)) return;
this.pushCTypeRefs(cMemberTypeNames(specifier, owner.name, this.source), owner);
}

Expand Down Expand Up @@ -4579,7 +4579,7 @@ export class TreeSitterExtractor {

// A C or C++ alias references what it names, so a change to a struct
// reaches the members declared through `typedef struct redisObject robj;`.
if (typeAliasNode && (this.language === 'c' || this.language === 'cpp')) {
if (typeAliasNode && (this.language === 'c' || this.language === 'cpp' || this.language === 'objc')) {
this.pushCTypeRefs(cAliasTypeNames(node, name, this.source), typeAliasNode);
}

Expand Down
2 changes: 1 addition & 1 deletion src/mcp/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4942,7 +4942,7 @@ export class ToolHandler {
// names, says who holds what, not where the code goes: rocksdb's classes
// reach everything through them, and they would pull a flow's rank and its
// file budget away from the code on the flow.
const isCFamily = (node: Node | undefined): boolean => node?.language === 'c' || node?.language === 'cpp';
const isCFamily = (node: Node | undefined): boolean => node?.language === 'c' || node?.language === 'cpp' || node?.language === 'objc';
const isCMemberTypeEdge = (edge: Edge): boolean =>
edge.kind === 'references' && isTypeDeclaration(edge.source) && isCFamily(subgraph.nodes.get(edge.source));
const connectedToEntry = new Set<string>();
Expand Down
4 changes: 4 additions & 0 deletions src/resolution/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { gateDartLocal, clearDartLocalScopeMemos } from './dart-local-scope';
import { clearCppTypeAliasMemos } from './cpp-type-aliases';
import { clearCppIncluderMemos } from './cpp-includers';
import { clearCMacroRenames } from './c-macro-renames';
import { isObjcDeclaredTypeRef, matchByExactName } from './name-matcher';
import { clearCFeatureVariantMemos } from './c-feature-variants';
import { clearAmbientModuleMemos, ambientModuleDeclarations } from './ambient-modules';
import { matchShopifyThemeFile } from './shopify-themes';
Expand Down Expand Up @@ -1593,6 +1594,9 @@ export class ReferenceResolver {
// So is the type a member of one or an alias is declared with, which only
// a type can be: a function or macro of that name is no candidate.
if (isCppDeclaredTypeRef(ref, this.context)) return matchCppDeclaredType(ref, this.context);
// In Objective-C it is the type of that name the file can see: a function
// or variable is no candidate, and neither is a guess by any other strategy.
if (isObjcDeclaredTypeRef(ref)) return matchByExactName(ref, this.context);

// A Pascal name written without parentheses (#2514) links only to a
// routine its scope reaches, or through a receiver to a method of the
Expand Down
29 changes: 28 additions & 1 deletion src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2711,6 +2711,30 @@ function isObjcFileLocalElsewhere(n: Node, ref: UnresolvedRef): boolean {
return n.language === 'objc' && n.filePath !== ref.filePath && OBJC_FILE_LOCAL_KINDS.has(n.kind) && C_SOURCE_EXT.test(n.filePath);
}

/** What an Objective-C struct's member or an alias can be declared with. */
const OBJC_DECLARED_TYPE_KINDS: ReadonlySet<string> = new Set(['class', 'protocol', 'struct', 'union', 'enum', 'type_alias']);

/**
* Does `ref` name the type an Objective-C struct's member or an alias is
* declared with? Extraction writes no other `references` from either: a value
* read is its function's. (A node's id starts with its kind.)
*/
export function isObjcDeclaredTypeRef(ref: UnresolvedRef): boolean {
return ref.language === 'objc' && ref.referenceKind === 'references' && /^(?:struct|union|type_alias):/.test(ref.fromNodeId);
}

/**
* The declarations an Objective-C type name stands for: a class's own header
* (`NSString.h`), then any header or the file itself. A class has a node per
* `@implementation` and category, and the one in some other source file is the
* least of them: GNUstep's `NSString` has 14, in test files too.
*/
function objcTypeDeclarations(candidates: Node[], ref: UnresolvedRef): Node[] {
const visible = candidates.filter((n) => n.filePath === ref.filePath || !C_SOURCE_EXT.test(n.filePath));
const own = visible.filter((n) => n.filePath.endsWith(`/${ref.referenceName}.h`) || n.filePath === `${ref.referenceName}.h`);
return own.length > 0 ? own : visible.length > 0 ? visible : candidates;
}

/** Per context: a C/C++ type's qualified name → its declarations. */
const CPP_TYPE_DECLS = new WeakMap<ResolutionContext, Map<string, Node[]>>();
/** Per context: a C/C++ file → the line and resolved file of each `#include` it writes. */
Expand Down Expand Up @@ -14080,6 +14104,7 @@ export function matchByExactName(
const cMacroCall = ref.referenceKind === 'calls' && (ref.language === 'c' || ref.language === 'cpp') &&
sameName.some((n) => n.kind === 'constant' && CPP_DEFINE_SIGNATURE.test(n.signature ?? ''));
const typeRef = isDotNetTypeRef(ref, context);
const objcDeclaredType = isObjcDeclaredTypeRef(ref);
const rustBare = ref.language === 'rust' && /^[A-Za-z_]\w*$/.test(ref.referenceName);
const pythonShape = pythonCallShape(ref, context);
const javaBare = ref.language === 'java' && ref.referenceKind === 'calls' && /^[A-Za-z_$][\w$]*$/.test(ref.referenceName);
Expand Down Expand Up @@ -14116,6 +14141,7 @@ export function matchByExactName(
!(objcShape === 'self-send' && !isObjcSelfSendTarget(n, ref, context)) &&
!(objcShape === 'super-send' && !isObjcSelfSendTarget(n, ref, context, true)) &&
!isObjcFileLocalElsewhere(n, ref) &&
!(objcDeclaredType && !OBJC_DECLARED_TYPE_KINDS.has(n.kind)) &&
!(vbReceiver !== null && !isVbMemberReachable(n, vbReceiver)) &&
!(vbReceiver && !/^(?:me|mybase|myclass)$/i.test(vbReceiver) && !isVbTypeQualifiedBy(n, vbReceiver, ref.filePath, context)) &&
!(vbScoped && !isVbMemberInScope(n, ref, context)) &&
Expand Down Expand Up @@ -14222,7 +14248,8 @@ export function matchByExactName(
: dartTypeRef && dartPrefix === null ? preferOwnDartLibrary(filtered, ref, context)
: swiftShape && swiftShape.shape !== 'chained' ? nearestSwiftMembers(filtered, ref, context)
: kotlinBare ? lexicalKotlinMembers(filtered, ref, context)
: rubyBare ? rubyMethodsOfCallerKind(filtered, ref, context) : cppSpecializationPool(filtered, ref, context);
: rubyBare ? rubyMethodsOfCallerKind(filtered, ref, context)
: objcDeclaredType ? objcTypeDeclarations(filtered, ref) : cppSpecializationPool(filtered, ref, context);
// (Above the ceiling below nothing is picked, so no translation unit is read.)
const candidates = ranked.length <= AMBIGUOUS_NAME_CEILING ? withinCppTranslationUnit(ranked, ref, context, true) : ranked;

Expand Down