From 6c202bca3233fa953c507482defae5c3210a653f Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Tue, 20 Oct 2026 00:03:37 +0000 Subject: [PATCH] fix(objc): a struct, union or enum defined in a variable declaration is indexed (#2690) Objective-C kept the gap that PR #2446 closed for C and C++: a struct, union or enum defined in the type of a file-scope declaration, like `static struct { Class isa; } placeholder;`, `struct Foo { int a; } foo;` or `enum { ModeA, ModeB } mode;`, never became a node. The walker's declaration branch extracted the variable and skipped the declaration's children, and Objective-C did not take the declared-type walk. - tree-sitter.ts: Objective-C walks a type defined in a declaration's type, as C does, also when the tree has errors. Most Objective-C trees have errors (a lone macro like NS_ASSUME_NONNULL_BEGIN is enough; 832 of ObjFW's 1087 Objective-C files), so a clean-tree gate would skip nearly every real site, and neither grammar has a namespace or a class written as a type that error recovery could close at the wrong brace. - objc.ts: resolveName composes selector naming with C's unnamedTypeVariableName, so an unnamed type takes the first variable its declaration declares (in function and method bodies too), and getDeclarationWrapper makes the declaration's comment the docstring. - c-cpp.ts: export definingDeclaration and unnamedTypeVariableName. - name-matcher.ts: a struct, union, enum or typedef that a .m or .mm file defines is visible only to that file, as isCppTranslationUnitLocal keeps a .c file's. Without it, ObjFW's calls of AmigaOS's Lock(path, mode) linked to the runtime's private struct Lock in synchronized.m. A class stays visible: it is the runtime's, wherever it is written. The walk also recovers a typedef written right after a line holding only a macro (OF_ASSUME_NONNULL_BEGIN, RCT_EXTERN_C_BEGIN), which the parser reads as a declaration of a variable: ObjFW's public enums with their values, and React Native's RCTMethodInfo, were missing. Objective-C has no kernel walker, so the kernel is unchanged. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/objc-declaration-types.test.ts | 226 +++++++++++++++++++++++ src/extraction/languages/c-cpp.ts | 9 +- src/extraction/languages/objc.ts | 10 +- src/extraction/tree-sitter.ts | 34 ++-- src/resolution/name-matcher.ts | 10 + 6 files changed, 271 insertions(+), 19 deletions(-) create mode 100644 __tests__/objc-declaration-types.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bbc06543..47ca1d532 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -180,6 +180,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In JavaScript, an import of a TypeScript module written without an extension, like `import AudioModal from './audio_modal'` in a `.jsx` file next to `audio_modal.tsx`, now links to that module, as it already did from a TypeScript file. Before, a `.js` or `.jsx` file's imports of TypeScript modules were never followed, so in a codebase moving to TypeScript they were matched by name instead: an import under another name linked nothing, and a name that repeats could link to the wrong file, so mastodon's `` and `` tags rendered other components of those names than the ones each file imports. A JavaScript module beside a TypeScript one of the same name still comes first. Re-index JavaScript projects after upgrading. - `codegraph_explore`'s Flow now goes through the implementation your query names when an interface method has several that lead to the same place. Before, it took whichever one came first in the index: asking about prometheus's `Engine.execEvalStmt Queryable.Querier fanout.Querier NewMergeQuerier` traced the call through the TSDB's `DB.Querier` and left the `fanout.Querier` you named off the Flow. Any two routes of the same length are now settled this way, in favor of the one that passes through more of the symbols you named. - In C and C++, a struct, class, union or enum defined in a variable declaration, like `struct Foo { … } foo;`, is now indexed with its methods and enum values, next to the variable. Before, at most the variable was, so the type was missing from search and `codegraph_explore`, a class deriving from it had no base, and calls to its methods linked to nothing: in redis, the Sentinel's `sentinelState` and `redis-cli`'s `config` struct were missing. A struct, union or enum with no name of its own now takes the name of the variable it declares, so redis's `static struct { … } SPT;` is found as `SPT`, and one declared inside a function is named after its variable instead of ``. The comment above the declaration becomes the type's description. Re-index C and C++ projects after upgrading. +- In Objective-C, a struct, union or enum defined in a variable declaration, like `static struct { Class isa; } placeholder;` or `enum { stateType, stateParamName } state;`, is now indexed with its enum values, next to the variable, as in C and C++. Before, only the variable was, so the type was missing from search and `codegraph_explore`. The same went for a `typedef enum { … } Name;` or `typedef struct … Name;` written after a line holding only a macro, like `OF_ASSUME_NONNULL_BEGIN` or `RCT_EXTERN_C_BEGIN`: ObjFW's `OFZIPArchiveEntryCompressionMethod` and every value in it were missing, and so was React Native's `RCTMethodInfo`. A struct, union or enum with no name of its own takes the name of the variable it declares, so ObjFW's class-cluster placeholders are found as `placeholder`, one declared inside a function or method is named after its variable instead of ``, and the comment above the declaration becomes the type's description. As in C, a type that a `.m` file defines links only from that file, so a call elsewhere that just shares its name, like ObjFW's calls of AmigaOS's `Lock(path, mode)`, doesn't link to the runtime's private `struct Lock`. Re-index Objective-C projects after upgrading. - In TypeScript React code, a styled-components or emotion component declared with a type argument, like `const Wrapper = styled.div` followed by its CSS, or `styled(Base)`, is now indexed as a component, as one written without a type argument already was. Before, it was indexed as a plain constant, so `codegraph callers` and impact found none of the code that renders it, and its tags could link to another file's component of the same name instead: in outline/outline, `NudeButton` and `HStack` had no callers, and `` tags linked to a different `Text` than the one each file imports. Re-index TypeScript React projects after upgrading. - `codegraph sync` and the file watcher now move a React Router route to its page when the router file loads that page lazily into a value of its own, like `const Docs = lazy(() => import('./pages/Docs'))` rendered by `} />`, and the page's file is added after the router was indexed, comes back after being deleted, or gets or changes its default export in a later edit. Before, the route kept pointing at the `Docs` declaration, or at the page's old component, until the router file itself changed or the project was re-indexed, unless the page's component happened to share the declaration's name. The same now holds for a `lazy: () => import('./pages/Docs')` route whose page switches its default export to another component. Re-index React projects after upgrading to correct routes an earlier version left behind. - `codegraph sync` and the file watcher now link a component to the JSX that renders it, like ``, when the component is added in a file with no JSX of its own, as in `export default function Team() { return null; }`. Before, the link waited for an unrelated change to a file with JSX, or for a re-index, so the new component looked unused. diff --git a/__tests__/objc-declaration-types.test.ts b/__tests__/objc-declaration-types.test.ts new file mode 100644 index 000000000..5eded4cc5 --- /dev/null +++ b/__tests__/objc-declaration-types.test.ts @@ -0,0 +1,226 @@ +/** + * A struct, union or enum DEFINED in the type of an Objective-C variable + * declaration was never indexed — the gap #2446 closed for C and C++: + * + * static struct { Class isa; } placeholder; // only the variable + * struct Foo { int a; } foo; // no struct Foo + * enum { ModeA, ModeB } mode; // no enumerators + * + * The walker handed such a declaration to the variable extractor and skipped + * its children, so the type and its enumerators never became nodes. The same + * happened to a `typedef enum { … } Name;` written after a macro such as + * `OF_ASSUME_NONNULL_BEGIN`: the parser reads the typedef as a declaration of + * a variable `Name`, so a public enum and every value in it went missing. + * + * The type is now walked beside the variables, the comment above the + * declaration documents it, and an unnamed one takes the name of the first + * variable its declaration declares, as in C. Most Objective-C trees have + * errors (that macro is enough), and as in C the type is walked anyway. + * Objective-C has no kernel walker, so only the wasm extractor runs. + */ +import { describe, it, expect, beforeAll, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; +import { extractFromSource } from '../src/extraction'; +import { getParser, initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars'; +import type { ExtractionResult } from '../src/types'; + +/** ` ` for every symbol, file and imports aside. */ +function symbols(result: ExtractionResult): string[] { + return result.nodes + .filter((n) => n.kind !== 'file' && n.kind !== 'import') + .map((n) => `${n.kind} ${n.qualifiedName}`); +} + +describe('a type defined in an Objective-C declaration', () => { + beforeAll(async () => { + await initGrammars(); + await loadGrammarsForLanguages(['objc']); + }); + + for (const crlf of [false, true]) { + const eol = (s: string) => (crlf ? s.replace(/\n/g, '\r\n') : s); + const label = crlf ? ' (CRLF)' : ''; + + it(`structs, unions and enums beside their variables; an unnamed one takes its variable's name${label}`, () => { + const source = [ + 'struct Foo { int a; } foo;', + 'static struct { Class isa; } placeholder;', + 'union { int i; float f; } u1, u2;', + 'enum { ModeA, ModeB } mode;', + 'typedef struct Bar { int c; } Bar;', + '', + ].join('\n'); + const result = extractFromSource('src/decl.m', eol(source), 'objc'); + expect(symbols(result)).toEqual([ + 'struct Foo', + 'variable foo', + 'struct placeholder', + 'variable placeholder', + 'union u1', + 'variable u1', + 'variable u2', + 'enum mode', + 'enum_member mode::ModeA', + 'enum_member mode::ModeB', + 'variable mode', + 'struct Bar', + ]); + }); + + it(`the comment above the declaration documents the type${label}`, () => { + const source = '/** The shared cache. */\nstatic struct { int hits; } cache;\n'; + const result = extractFromSource('src/cache.m', eol(source), 'objc'); + const docs = result.nodes.filter((n) => n.name === 'cache').map((n) => `${n.kind} ${n.docstring}`); + expect(docs).toEqual(['struct The shared cache.', 'variable The shared cache.']); + }); + + it(`an unnamed type in a method body takes its variable's name too${label}`, () => { + const source = '@implementation Widget\n- (void)spin {\n struct { int z; } local;\n local.z = 1;\n}\n@end\n'; + const result = extractFromSource('src/Widget.m', eol(source), 'objc'); + expect(symbols(result)).toEqual(['class Widget', 'method Widget::spin', 'struct Widget::spin::local']); + }); + + it(`a file the parser misreads still has its types walked${label}`, () => { + // The macro makes the parser read the typedef as a declaration of a + // variable named after the enum (and one named `typedef`). + const header = eol([ + 'OF_ASSUME_NONNULL_BEGIN', + '', + 'typedef enum {', + '\tOFZIPArchiveEntryCompressionMethodNone = 0,', + '\tOFZIPArchiveEntryCompressionMethodDeflate = 8', + '} OFZIPArchiveEntryCompressionMethod;', + '', + '@interface OFZIPArchiveEntry: OFObject', + '@end', + '', + 'OF_ASSUME_NONNULL_END', + '', + ].join('\n')); + expect(getParser('objc')!.parse(header)!.rootNode.hasError).toBe(true); + expect(symbols(extractFromSource('src/OFZIPArchiveEntry.h', header, 'objc'))).toEqual( + expect.arrayContaining([ + 'enum OFZIPArchiveEntryCompressionMethod', + 'enum_member OFZIPArchiveEntryCompressionMethod::OFZIPArchiveEntryCompressionMethodNone', + 'enum_member OFZIPArchiveEntryCompressionMethod::OFZIPArchiveEntryCompressionMethodDeflate', + 'class OFZIPArchiveEntry', + ]) + ); + // A member type the grammar can't read errs inside the struct's own body. + const atomic = eol('static struct {\n _Atomic(int) installedState;\n atomic_bool isEnabled;\n} g_state;\n'); + expect(getParser('objc')!.parse(atomic)!.rootNode.hasError).toBe(true); + expect(symbols(extractFromSource('src/Monitor.m', atomic, 'objc'))).toEqual(['struct g_state', 'variable g_state']); + }); + } +}); + +describe('an indexed Objective-C project with types defined in declarations', () => { + let root = ''; + let cg: CodeGraph | undefined; + + afterEach(() => { + cg?.destroy(); + cg = undefined; + if (root) fs.rmSync(root, { recursive: true, force: true }); + root = ''; + }); + + it('finds a class cluster placeholder and the values of an enum a macro hid', async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-objc-declaration-types-')); + const files: Record = { + 'src/OFArray.m': [ + '#import "OFObject.h"', + '', + '/* The object every +alloc of the cluster returns. */', + 'static struct {', + '\tClass isa;', + '} placeholder;', + '', + '@interface OFArray: OFObject', + '@end', + '', + '@implementation OFArray', + '+ (instancetype)alloc', + '{', + '\treturn (id)&placeholder;', + '}', + '@end', + '', + ].join('\n'), + 'src/OFZIPArchiveEntry.h': [ + 'OF_ASSUME_NONNULL_BEGIN', + '', + 'typedef enum {', + '\tOFZIPArchiveEntryCompressionMethodNone = 0,', + '\tOFZIPArchiveEntryCompressionMethodDeflate = 8', + '} OFZIPArchiveEntryCompressionMethod;', + '', + '@interface OFZIPArchiveEntry: OFObject', + '- (OFZIPArchiveEntryCompressionMethod)method;', + '@end', + '', + 'OF_ASSUME_NONNULL_END', + '', + ].join('\n'), + }; + 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 }); + + const placeholder = cg.searchNodes('placeholder', { limit: 5 }).map((r) => `${r.node.kind} ${r.node.name}`); + expect(placeholder).toContain('struct placeholder'); + const struct = cg.getNodesByName('placeholder').find((n) => n.kind === 'struct'); + expect(struct?.docstring).toBe('The object every +alloc of the cluster returns.'); + + const deflate = cg.getNodesByName('OFZIPArchiveEntryCompressionMethodDeflate').map((n) => `${n.kind} ${n.qualifiedName} ${n.language}`); + expect(deflate).toEqual([ + 'enum_member OFZIPArchiveEntryCompressionMethod::OFZIPArchiveEntryCompressionMethodDeflate objc', + ]); + }, 60_000); + + it('keeps a struct a .m file defines to that file, as C keeps a .c file\'s', async () => { + // An Objective-C source file is a translation unit: no other file can name + // the struct it defines, so a call of an outside function that shares its + // name (AmigaOS's `Lock(path, mode)` in ObjFW) makes no such struct. + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-objc-declaration-types-')); + const files: Record = { + 'src/runtime/synchronized.m': [ + 'static struct Lock {', + '\tid object;', + '\tint count;', + '} *locks = NULL;', + '', + 'int objc_sync_count(void)', + '{', + '\treturn locks->count;', + '}', + '', + ].join('\n'), + 'src/OFFileManager.m': [ + '@implementation OFFileManager', + '- (long)lockPath: (const char *)path', + '{', + '\treturn Lock(path, 1);', + '}', + '@end', + '', + ].join('\n'), + }; + 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 }); + + const lock = cg.getNodesByName('Lock').map((n) => `${n.kind} ${n.filePath}`); + expect(lock).toEqual(['struct src/runtime/synchronized.m']); + const caller = cg.getNodesByName('lockPath:').find((n) => n.kind === 'method')!; + expect(caller).toBeDefined(); + expect(cg.getOutgoingEdges(caller.id).filter((e) => e.kind !== 'contains')).toEqual([]); + }, 60_000); +}); diff --git a/src/extraction/languages/c-cpp.ts b/src/extraction/languages/c-cpp.ts index 4106a953c..1d239028b 100644 --- a/src/extraction/languages/c-cpp.ts +++ b/src/extraction/languages/c-cpp.ts @@ -46,9 +46,9 @@ export function isClassLikeDefinition(node: SyntaxNode): boolean { /** * The `declaration` a class, struct, union or enum is defined in the type of — * `struct Foo { … } foo;` — or null for one written on its own, in a typedef or - * in a member's type. + * in a member's type. Objective-C shares it (objc.ts). */ -function definingDeclaration(node: SyntaxNode): SyntaxNode | null { +export function definingDeclaration(node: SyntaxNode): SyntaxNode | null { if (!isClassLikeDefinition(node)) return null; const parent = node.parent; return parent?.type === 'declaration' && getChildByField(parent, 'type')?.id === node.id ? parent : null; @@ -60,9 +60,10 @@ function definingDeclaration(node: SyntaxNode): SyntaxNode | null { * type of `static struct { … } SPT;` (only `SPT`), so the struct is indexed as * `SPT`, the way `typedef struct { … } Name;` is indexed as `Name`. Undefined * for a named type, and when the declaration declares no variable (a - * function's return type). Mirrored in the kernel (ccpp/mod.rs). + * function's return type). Mirrored in the kernel (ccpp/mod.rs). Objective-C + * shares it (objc.ts), and has no kernel walker. */ -function unnamedTypeVariableName(node: SyntaxNode, source: string): string | undefined { +export function unnamedTypeVariableName(node: SyntaxNode, source: string): string | undefined { if (getChildByField(node, 'name')) return undefined; const declaration = definingDeclaration(node); if (!declaration) return undefined; diff --git a/src/extraction/languages/objc.ts b/src/extraction/languages/objc.ts index 9bada343c..1074cbcdb 100644 --- a/src/extraction/languages/objc.ts +++ b/src/extraction/languages/objc.ts @@ -1,6 +1,7 @@ import type { Node as SyntaxNode } from 'web-tree-sitter'; import { getChildByField, getNodeText } from '../tree-sitter-helpers'; import type { ExtractorContext, LanguageExtractor } from '../tree-sitter-types'; +import { definingDeclaration, unnamedTypeVariableName } from './c-cpp'; function findCompoundStatement(node: SyntaxNode): SyntaxNode | null { for (let i = 0; i < node.namedChildCount; i++) { @@ -116,7 +117,14 @@ export const objcExtractor: LanguageExtractor = { bodyField: 'body', paramsField: 'parameters', getReturnType: extractObjcReturnType, - resolveName: extractObjcMethodName, + // A method is named by its selector. An unnamed struct, union or enum + // defined in a declaration's type takes the first variable's name, as in C: + // `static struct { … } placeholder;` is the struct `placeholder`. + resolveName: (node, source) => + extractObjcMethodName(node, source) ?? unnamedTypeVariableName(node, source), + // A struct, union or enum defined in a declaration's type is documented by + // the comment above the declaration. + getDeclarationWrapper: (node) => definingDeclaration(node) ?? undefined, extractPropertyName: extractObjcPropertyName, resolveBody: (node, bodyField) => { const fromField = getChildByField(node, bodyField); diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 0bee1075e..0824c5d26 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -636,12 +636,16 @@ export class TreeSitterExtractor { private cppBraceScopes: CppBraceScopes | null = null; private cppClassScopes = new NestedIntervals(); private cppClassScopeIds = new Set(); - // C/C++: whether a class, struct, union or enum defined in the type of a - // declaration outside any body is walked (see visitNode). Not in a C++ file - // whose tree has errors and whose braces don't balance: error recovery can - // close a namespace or a class at the wrong `}`, or run a class past its - // own, and with no brace scopes to correct it the walked class would land - // in the wrong scope. + // C, C++ and Objective-C: whether a class, struct, union or enum defined in + // the type of a declaration outside any body is walked (see visitNode). Not + // in a C++ file whose tree has errors and whose braces don't balance: error + // recovery can close a namespace or a class at the wrong `}`, or run a class + // past its own, and with no brace scopes to correct it the walked class + // would land in the wrong scope. C and Objective-C files take the walk even + // with errors: neither grammar has a namespace or a class written as a type + // (an @interface or @implementation is not one), and a struct, union or + // enum body holds fields and enumerators, not functions. Most Objective-C + // trees have errors; a macro such as `NS_ASSUME_NONNULL_BEGIN` is enough. private walkDeclaredTypes = false; // C++ local function-pointer bindings, per enclosing symbol: // `auto kernel = &flash_fwd_kernel<…>;` recorded as callerId → kernel → @@ -744,6 +748,7 @@ export class TreeSitterExtractor { } this.walkDeclaredTypes = this.language === 'c' || + this.language === 'objc' || (this.language === 'cpp' && (!this.tree.rootNode.hasError || this.cppBraceScopes !== null)); // Create file node representing the source file @@ -1514,14 +1519,15 @@ export class TreeSitterExtractor { this.extractor.variableTypes.includes(nodeType) && (!this.isInsideClassLikeNode() || this.isClassScopeConstantAssignment(node)) ) { - // C/C++: a class, struct, union or enum defined in the declaration's - // type (`struct Foo { … } foo;`, `static struct { … } SPT;`) is a - // definition like one written on its own, and the variables keep their - // nodes beside it. So is a class the tree reads as a declaration's type - // in a file walked in brace scopes (`class X {…}` glued to the tokens - // after it by error recovery). In a class or function body the children - // walk reaches it; here, this branch skips that walk. Mirrored in the - // kernel (ccpp/mod.rs visit_node). + // C, C++ and Objective-C: a class, struct, union or enum defined in the + // declaration's type (`struct Foo { … } foo;`, `static struct { … } + // SPT;`) is a definition like one written on its own, and the variables + // keep their nodes beside it. So is a class the tree reads as a + // declaration's type in a file walked in brace scopes (`class X {…}` + // glued to the tokens after it by error recovery). In a class or + // function body the children walk reaches it; here, this branch skips + // that walk. Mirrored in the kernel (ccpp/mod.rs visit_node), which has + // no Objective-C walker. const declaredType = this.walkDeclaredTypes ? getChildByField(node, 'type') : null; const definedType = declaredType && isClassLikeDefinition(declaredType) ? declaredType : null; if (definedType) this.visitNode(definedType); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index c24ce9579..d750b2cfb 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -1998,6 +1998,9 @@ const C_STATIC_MEMO = new WeakMap>(); */ const C_SOURCE_EXT = /\.(c|cc|cpp|cxx|c\+\+|m|mm)$/i; +/** The C types an Objective-C file can define, as opposed to its classes and protocols. */ +const OBJC_C_TYPE_KINDS: ReadonlySet = new Set(['struct', 'union', 'enum', 'type_alias']); + /** Per context: a C/C++ type's qualified name → its declarations. */ const CPP_TYPE_DECLS = new WeakMap>(); /** Per context: a C/C++ file → the line and resolved file of each `#include` it writes. */ @@ -3138,6 +3141,8 @@ function isGoNameVisible(candidate: Node, ref: UnresolvedRef): boolean { * whichever USB class file ranked first. So is what a source file's * unnamed namespace declares, and a type only source files declare, with * its members (isCppTranslationUnitLocal). + * - **Objective-C**: a struct, union, enum or typedef a `.m` / `.mm` source + * file defines is local to that file, as in C. A class is not. * - **Kotlin, Java, C#, Swift, Scala, Dart, PHP**: `private` is class- or * file-local. An Android `editor.apply()` resolved onto an unrelated class's * `private fun apply`. @@ -3208,6 +3213,11 @@ export function isVisibleAcrossFiles(candidate: Node, ref: UnresolvedRef, contex !cppIncluders(candidate.filePath, context).has(ref.filePath)) return false; return !isCppTranslationUnitLocal(candidate, ref, context); } + // An Objective-C source file is a translation unit too: a C type it defines + // is its own. ObjFW's runtime keeps `static struct Lock { … } *locks;` in + // synchronized.m, and the AmigaOS `Lock(path, mode)` calls in other files + // make no such struct. (A class is the runtime's, wherever it is written.) + if (lang === 'objc' && OBJC_C_TYPE_KINDS.has(candidate.kind) && C_SOURCE_EXT.test(candidate.filePath)) return false; if (lang === 'go') return isGoNameVisible(candidate, ref); if (lang === 'rust') { if (candidate.visibility !== 'private') return true;