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 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-20 00:03:37 +00:00
committed by GitHub
parent 12ea27faf4
commit 6c202bca32
6 changed files with 271 additions and 19 deletions
+5 -4
View File
@@ -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;
+9 -1
View File
@@ -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);
+20 -14
View File
@@ -636,12 +636,16 @@ export class TreeSitterExtractor {
private cppBraceScopes: CppBraceScopes | null = null;
private cppClassScopes = new NestedIntervals<string>();
private cppClassScopeIds = new Set<string>();
// 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);
+10
View File
@@ -1998,6 +1998,9 @@ const C_STATIC_MEMO = new WeakMap<ResolutionContext, Map<string, boolean>>();
*/
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<string> = new Set(['struct', 'union', 'enum', 'type_alias']);
/** 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. */
@@ -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;