fix(objc): a message to an ivar or a property reaches the class it is declared as (#2703)

An Objective-C message to a local or a parameter was typed by its
declaration (`FMResultSet *rs` makes `[rs close]` FMResultSet's). One to
an instance variable or a property was not, since that declaration sits
outside the calling method: in a header's @interface, an @implementation's
ivar block, a superclass, or synthesized for a property. The receiver's
name decided instead. ObjFW's tar tool's `[_archive nextEntry]` reached
OFLHAArchive::nextEntry, and React Native's `[_displayLink invalidate]` on
a CADisplayLink reached RN's own RCTDisplayLink::invalidate.

- objcFileMembers reads what each class declares, per file: the ivar
  block right after an @interface head (seen by subclasses; the head may
  go on over several lines, a protocol list on a line of its own), a
  class extension's or an @implementation's ivar block (the class's own),
  @property declarations, including ObjFW's two-line ones and IBOutlet
  ones, and @synthesize lines. Types take protocol lists, type arguments
  and ObjFW's OF_GENERIC(...).
- objcMemberReceiverType, after the local inferrer, follows the language.
  `self.name` is a property of the class or a superclass. A bare name is
  an ivar: the class's own, one its own property synthesizes (`_name`,
  never a category property's), one @synthesize names, or a superclass's
  @interface ivar. A bare name is never a property's: `arcs` in
  `initWithArcs: (...)arcs` is the parameter. A capitalized bare name is a
  class, and `self.name` needs a property node, which skips most lookups.
  The type then takes the typed path (resolveMethodOnType at 0.9), and a
  class from outside the project has no project method, as for locals.
- objcClassFiles: when more than one file declares `@interface Name`
  itself, those are namesakes in separate targets, and only the ones in
  the calling file's directory count. SDWebImage's TV demo ViewController
  was otherwise typed by the macOS demo's `NSImageView *imageView1`.
- localReceiverTypePatterns('objc'): a local's or a parameter's type may
  carry OF_GENERIC(...), like `OFMutableArray OF_GENERIC(OFNumber *) *arcs`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-20 16:35:36 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent f2f5dee044
commit 70783f31f1
3 changed files with 468 additions and 2 deletions
+1
View File
@@ -186,6 +186,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Objective-C, and in the C and C++ headers that sit beside it, an enum declared with Foundation's macros, like `typedef NS_ENUM(NSInteger, SDImageCacheType) { … };`, `NS_OPTIONS`, `NS_CLOSED_ENUM`, `NS_ERROR_ENUM` or CoreFoundation's `CF_ENUM` and `CF_OPTIONS`, is now indexed with its values. Before, the parser read the macro as a broken typedef, so these enums were missing (SDWebImage had none at all) and a type alias named after the first value stood in their place. A Swift annotation naming such an enum, like KSCrash's `let unwindMethods: KSBacktraceUnwindMethods`, now links to it. Re-index Objective-C, C and C++ projects after upgrading.
- In Objective-C, the doc comment above a method, function or static written inside an `@implementation` is now read as its description, as the one above an `@interface` property already was. Before, the parser's wrapper around each item of an `@implementation` hid the comment, so search and `codegraph_explore` showed these methods without their documentation. Re-index Objective-C projects after upgrading.
- In Objective-C, a file-scope constant or global, like `NSString *const SDWebImageErrorDomain = @"SDWebImageErrorDomain";` or `static const NSInteger kMaxRetries = 3;`, is now indexed with its value, and so is one written inside an `@implementation`. Before, only a global declared with a bare name, like `int count;`, was, so error domains, notification names, keys and shared queues were missing from search and `codegraph_explore`. A global that a `.m` file defines is reached from other files through the header that declares it, and a call of a block parameter, like `completionHandler(nil, image)`, no longer links to a namesake that another `.m` file keeps. Re-index Objective-C projects after upgrading.
- In Objective-C, a message to an instance variable or a property now reaches a method of the class it is declared as, as a message to a local variable already did. That covers an instance variable a header, an `@implementation` or a superclass declares, the one a property synthesizes (`_name`, or the one `@synthesize` names), `self.name`, and a local or parameter whose type ObjFW writes with `OF_GENERIC(…)`. Before, such a message was matched by the receiver's name: in ObjFW's archive tool, `[_archive nextEntry]` on a tar archive reached the LHA archive's method, and in React Native, `[_displayLink invalidate]` on a `CADisplayLink` reached React Native's own `RCTDisplayLink`. A message to a variable whose class comes from outside your project, like `NSMapTable` or `CADisplayLink`, no longer links to a project method of the same name. 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<WrapperProps>` followed by its CSS, or `styled(Base)<Props>`, 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 `<Text>` 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 `<Route path="/docs" element={<Docs />} />`, 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 `<Team />`, 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.
+253
View File
@@ -0,0 +1,253 @@
/**
* An Objective-C message to an instance variable or a property of the class
* around it is typed by that declaration, as one to a local or a parameter
* already was. The ivar is declared outside the calling method, in a
* header's `@interface`, an `@implementation`'s ivar block, a superclass, or
* synthesized for a property, so the method's own lines never typed it:
* ObjFW's `[_headers mutableCopy]` reached nothing, and RN's `[_registry
* objectForKey:]` on an `NSMapTable` reached a project class named after the
* receiver. A declared class from outside the project has no project method.
* A local or a parameter whose type ObjFW writes with `OF_GENERIC(…)` is
* typed too.
*/
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';
describe('an Objective-C receiver declared by its class', () => {
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 the methods a method's calls reach, as `Class::selector`. */
async function indexProject(files: Record<string, string>): Promise<(method: string, file?: string) => string[]> {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-objc-receivers-'));
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 (method: string, file?: string) => {
const node = graph.getNodesByName(method).find((n) => n.kind === 'method' && (!file || n.filePath === file))!;
expect(node, method).toBeDefined();
return graph
.getOutgoingEdges(node.id)
.filter((e) => e.kind === 'calls')
.map((e) => graph.getNode(e.target)!.qualifiedName)
.sort();
};
}
const collections = [
'@interface OFDictionary : OFObject',
'- (id)mutableCopy;',
'- (id)objectForKey: (id)key;',
'@end',
'@interface OFArray : OFObject',
'- (id)mutableCopy;',
'@end',
'@interface RCTComposedViewRegistry : OFDictionary',
'- (id)objectForKey: (id)key;',
'@end',
'',
].join('\n');
const collectionsImpl = [
'#import "Collections.h"',
'@implementation OFDictionary',
'- (id)mutableCopy { return self; }',
'- (id)objectForKey: (id)key { return nil; }',
'@end',
'@implementation OFArray',
'- (id)mutableCopy { return self; }',
'@end',
'@implementation RCTComposedViewRegistry',
'- (id)objectForKey: (id)key { return nil; }',
'@end',
'',
].join('\n');
for (const crlf of [false, true]) {
const eol = (s: string) => (crlf ? s.replace(/\n/g, '\r\n') : s);
const label = crlf ? ' (CRLF)' : '';
it(`types ivars, synthesized ivars and self. properties, its own and its superclass's${label}`, async () => {
const targetsOf = await indexProject({
'src/Collections.h': eol(collections),
'src/Collections.m': eol(collectionsImpl),
'src/OFHTTPResponse.h': eol([
'#import "Collections.h"',
'@interface OFHTTPResponse : OFObject',
' <OFCopying>',
'{',
'\tOFDictionary *_headers;',
'}',
'@property (nonatomic, copy) OFArray *cookies;',
'@end',
'',
].join('\n')),
'src/OFHTTPServerResponse.h': eol([
'#import "OFHTTPResponse.h"',
'@interface OFHTTPServerResponse : OFHTTPResponse <OFReadyForReadingObserving,',
' OFReadyForWritingObserving>',
'/**',
' * @brief The trailer fields.',
' */',
'@property OF_NULLABLE_PROPERTY (copy, nonatomic)',
' OFArray *trailerFields;',
'@end',
'',
].join('\n')),
'src/OFHTTPServerResponse.m': eol([
'#import "OFHTTPServerResponse.h"',
'@interface OFHTTPServerResponse ()',
'@property (nonatomic) OFArray *pending;',
'@end',
'',
'@implementation OFHTTPServerResponse',
'{',
'\tOFArray *_trailers, *_parts;',
'\tOFDictionary *mFields;',
'}',
'@synthesize pending = mPending;',
'',
'- (id)inherited',
'{',
'\treturn [_headers mutableCopy];',
'}',
'',
'- (id)fromBlock',
'{',
'\t[_parts mutableCopy];',
'\treturn [mFields objectForKey: @"Server"];',
'}',
'',
'- (id)synthesizedByName',
'{',
'\treturn [mPending mutableCopy];',
'}',
'',
'- (id)synthesized',
'{',
'\treturn [_trailerFields mutableCopy];',
'}',
'',
'- (id)throughSelf',
'{',
'\treturn [self.cookies mutableCopy];',
'}',
'@end',
'',
].join('\n')),
});
expect(targetsOf('inherited')).toEqual(['OFDictionary::mutableCopy']);
expect(targetsOf('fromBlock')).toEqual(['OFArray::mutableCopy', 'OFDictionary::objectForKey:']);
expect(targetsOf('synthesizedByName')).toEqual(['OFArray::mutableCopy']);
expect(targetsOf('synthesized')).toEqual(['OFArray::mutableCopy']);
expect(targetsOf('throughSelf')).toEqual(['OFArray::mutableCopy']);
}, 60_000);
}
it('reaches no project method through a class from outside the project', async () => {
const targetsOf = await indexProject({
'React/Collections.h': collections,
'React/Collections.m': collectionsImpl,
'React/RCTSurfaceRegistry.m': [
'#import "Collections.h"',
'@implementation RCTSurfaceRegistry {',
' NSMapTable<id, id> *_registry;',
'}',
'- (id)surfaceForRootTag:(id)rootTag',
'{',
' return [_registry objectForKey:rootTag];',
'}',
'@end',
'',
].join('\n'),
});
// NSMapTable's own objectForKey:, not the registry class's by name.
expect(targetsOf('surfaceForRootTag:')).toEqual([]);
}, 60_000);
it('types a parameter by its own declaration, and a bare name never by a property', async () => {
const targetsOf = await indexProject({
'src/Collections.h': collections,
'src/Collections.m': collectionsImpl,
'src/OFASN1ObjectIdentifier.m': [
'#import "Collections.h"',
'@interface OFASN1ObjectIdentifier : OFObject',
'@property (readonly, nonatomic) OFDictionary *arcs;',
'@end',
'',
'@implementation OFASN1ObjectIdentifier',
'- (instancetype)initWithArcs: (OFArray OF_GENERIC(OFNumber *) *)arcs',
'{',
'\t[arcs mutableCopy];',
'\treturn self;',
'}',
'',
'- (instancetype)initWithAnyArcs: (id)arcs',
'{',
'\t[arcs mutableCopy];',
'\treturn self;',
'}',
'@end',
'',
].join('\n'),
});
expect(targetsOf('initWithArcs:')).toEqual(['OFArray::mutableCopy']);
// The property is `self.arcs` or `_arcs`, never `arcs`.
expect(targetsOf('initWithAnyArcs:')).not.toContain('OFDictionary::mutableCopy');
}, 60_000);
it('reads the class the call means when separate apps each declare one of its name', async () => {
const viewController = (type: string) => [
'#import "Gadgets.h"',
'@interface ViewController : OFObject',
`@property (weak, nonatomic) IBOutlet ${type} *imageView1;`,
'@end',
'',
].join('\n');
const targetsOf = await indexProject({
'Shared/Gadgets.h': [
'@interface Widget : OFObject',
'- (void)refresh;',
'@end',
'@interface Gadget : OFObject',
'- (void)refresh;',
'@end',
'',
].join('\n'),
'Shared/Gadgets.m': [
'#import "Gadgets.h"',
'@implementation Widget',
'- (void)refresh { }',
'@end',
'@implementation Gadget',
'- (void)refresh { }',
'@end',
'',
].join('\n'),
'Examples/Demo A/ViewController.h': viewController('Widget'),
'Examples/Demo B/ViewController.h': viewController('Gadget'),
'Examples/Demo B/ViewController.m': [
'#import "ViewController.h"',
'@implementation ViewController',
'- (void)viewDidLoad',
'{',
'\t[self.imageView1 refresh];',
'}',
'@end',
'',
].join('\n'),
});
expect(targetsOf('viewDidLoad')).toEqual(['Gadget::refresh']);
}, 60_000);
});
+214 -2
View File
@@ -7081,6 +7081,212 @@ function objcPropertyTypes(name: string, context: ResolutionContext): string[] {
return [...types];
}
/**
* What one file declares of an Objective-C class: instance variables (an
* `@interface`'s ivar block, which subclasses see, or a class extension's or
* an `@implementation`'s, which they don't), properties (a category's can't
* be synthesized), and the ivar each `@synthesize` names for a property.
*/
interface ObjcClassMembers {
ivars: Map<string, string>;
ownIvars: Map<string, string>;
properties: Map<string, { type: string; category: boolean }>;
synthesized: Map<string, string>;
}
/** Per context: an Objective-C file → each class it declares → what it declares of it. */
const OBJC_MEMBER_TABLES = new WeakMap<ResolutionContext, Map<string, Map<string, ObjcClassMembers>>>();
/** Per context: class name + the calling file's directory → the files declaring that class. */
const OBJC_CLASS_FILES = new WeakMap<ResolutionContext, Map<string, string[]>>();
// A declared class as a member declaration writes it, past qualifiers, with
// its type arguments, protocol list or ObjFW's `OF_GENERIC(…)` after it, up to
// the `*`: `OFObject <OFHTTPClientDelegate> *`, `OFArray OF_GENERIC(OFString *) *`,
// `IBOutlet UIImageView *`.
const OBJC_MEMBER_TYPE = String.raw`(?:(?:__strong|__weak|__unsafe_unretained|__block|const|volatile|IBOutlet|IBInspectable)\s+)*([A-Z]\w*)\s*(?:<[^;{}]*?>\s*|[A-Z][A-Z0-9_]*\s*\([^;{}]*?\)\s*)?\*`;
// An instance variable, alone or listed: `OFDictionary *_headers;`, `OFString *_host, *_path;`.
const OBJC_IVAR_RE = new RegExp(String.raw`^\s*${OBJC_MEMBER_TYPE}\s*((?:(?:_Nullable|_Nonnull|const)\s+)*\w+(?:\s*,\s*\*\s*(?:(?:_Nullable|_Nonnull|const)\s+)*\w+)*)\s*;`);
// A property, past attribute macros and its attribute list:
// `@property OF_NULLABLE_PROPERTY (readonly, nonatomic) OFString *host;`.
const OBJC_PROPERTY_RE = new RegExp(String.raw`^\s*@property\b\s*(?:[A-Z][A-Z0-9_]+\s*)*(?:\([^)]*\)\s*)?(?:(?:nullable|nonnull|null_unspecified|__kindof)\s+)*${OBJC_MEMBER_TYPE}\s*(?:(?:_Nullable|_Nonnull|const)\s+)*(\w+)`);
/**
* What each Objective-C class `filePath` declares has, by name, with the
* class each is declared as (ObjcClassMembers): read from `@interface`s (a
* header's, a class extension's or a category's: the ivar block right after
* the head, then `@property` declarations, which may run over several lines)
* and from an `@implementation`'s ivar block and `@synthesize` lines, never a
* method body.
*/
function objcFileMembers(filePath: string, context: ResolutionContext): Map<string, ObjcClassMembers> {
let memo = OBJC_MEMBER_TABLES.get(context);
if (!memo) OBJC_MEMBER_TABLES.set(context, (memo = new Map()));
const hit = memo.get(filePath);
if (hit) return hit;
const tables = new Map<string, ObjcClassMembers>();
memo.set(filePath, tables);
const lines = context.getFileLines?.(filePath) ?? context.readFile(filePath)?.split(/\r?\n/) ?? [];
const count = (text: string, re: RegExp): number => (text.match(re) ?? []).length;
let members: ObjcClassMembers | null = null;
let inInterface = false;
// `@interface Name (Category)`, or a class extension `@interface Name ()`.
let category: string | null = null;
// A class declares ivars only in a block that is the first code after its
// head: 'head' until that code (`open` counts the head's unclosed `<` and
// `(`, as in a protocol list over several lines), 'ivars' inside the block
// (`depth` braces deep), 'body' past it.
let state: 'head' | 'ivars' | 'body' = 'body';
let open = 0;
let depth = 0;
// A `@property` declaration read so far, until its `;`.
let property: string | null = null;
for (const raw of lines) {
let line = raw.replace(/\/\/.*$/, '');
const head = /^\s*(?:[A-Z][A-Z0-9_]*(?:\([^)]*\))?\s+)*@(interface|implementation)\s+(\w+)\s*(?:<[^>]*>\s*|[A-Z][A-Z0-9_]*\([^)]*\)\s*)?(?:\(\s*(\w*)\s*\))?/.exec(line);
if (head) {
const cls = head[2]!;
members = tables.get(cls) ?? { ivars: new Map(), ownIvars: new Map(), properties: new Map(), synthesized: new Map() };
tables.set(cls, members);
inInterface = head[1] === 'interface';
category = head[3] ?? null;
state = 'head';
open = 0;
property = null;
line = line.slice(head.index + head[0].length);
} else if (/^\s*@end\b/.test(line)) {
members = null;
continue;
}
if (!members) continue;
if (state === 'head') {
// The head may go on with its superclass or protocol list on the next
// lines: ObjFW's `@interface OFKernelEventObserverTests: OTTestCase`
// over ` <OFKernelEventObserverDelegate>`.
const inHead = head !== null || open > 0 || /^\s*[<:]/.test(line);
open += count(line, /[<(]/g) - count(line, /[>)]/g);
const brace = line.indexOf('{');
if (brace >= 0) {
state = 'ivars';
depth = 0;
line = line.slice(brace);
} else if (inHead || open > 0 || /^\s*(?:$|\/\*|\*|#)/.test(line)) {
continue;
} else {
state = 'body';
}
}
if (state === 'ivars') {
depth += count(line, /\{/g) - count(line, /\}/g);
if (depth <= 0) state = 'body';
const ivar = OBJC_IVAR_RE.exec(line.replace(/^[^{]*\{/, '').replace(/\}.*$/, ''));
const ivars = inInterface && category === null ? members.ivars : members.ownIvars;
for (const name of ivar?.[2]!.split(',') ?? []) {
const bare = name.replace(/\*|_Nullable|_Nonnull|\bconst\b/g, '').trim();
if (/^\w+$/.test(bare) && !ivars.has(bare)) ivars.set(bare, ivar![1]!);
}
continue;
}
if (!inInterface) {
const synthesize = /^\s*@synthesize\s+([^;]+);/.exec(line);
for (const item of synthesize?.[1]!.split(',') ?? []) {
const [name, ivar] = item.split('=').map((s) => s.trim());
if (name && /^\w+$/.test(ivar ?? name)) members.synthesized.set(ivar ?? name, name);
}
continue;
}
if (property === null && !/^\s*@property\b/.test(line)) continue;
property = property === null ? line : `${property} ${line}`;
if (!property.includes(';') && property.length < 400) continue;
const declared = OBJC_PROPERTY_RE.exec(property);
property = null;
if (declared && !members.properties.has(declared[2]!)) {
members.properties.set(declared[2]!, { type: declared[1]!, category: !!category });
}
}
return tables;
}
/**
* The files declaring the Objective-C class `name`, the calling file first.
* When more than one file declares the class itself (`@interface Name`, not a
* category or an extension), they are namesakes in separate targets, like
* each SDWebImage demo app's `ViewController`, and only the declarations in
* the calling file's directory are the one the call means.
*/
function objcClassFiles(name: string, ref: UnresolvedRef, context: ResolutionContext): string[] {
const dir = ref.filePath.slice(0, ref.filePath.lastIndexOf('/') + 1);
let memo = OBJC_CLASS_FILES.get(context);
if (!memo) OBJC_CLASS_FILES.set(context, (memo = new Map()));
const key = `${name}\u0000${dir}`;
const hit = memo.get(key);
if (hit) return hit;
const decls = context.getNodesByName(name).filter((n) => n.kind === 'class' && n.language === 'objc');
const primary = new Set<string>();
const own = new RegExp(`@interface\\s+${name}\\b(?!\\s*\\()`);
for (const decl of decls) {
const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/) ?? [];
if (lines.slice(decl.startLine - 1, decl.endLine).some((l) => own.test(l))) primary.add(decl.filePath);
}
const inDir = (file: string) => file.startsWith(dir) && !file.includes('/', dir.length);
const files = [...new Set(decls.map((n) => n.filePath))]
.filter((file) => primary.size < 2 || inDir(file))
.sort((a, b) => Number(b === ref.filePath) - Number(a === ref.filePath));
memo.set(key, files);
return files;
}
/**
* The class an Objective-C receiver is declared as when it is an instance
* variable of the class around the call, or a property read through `self.`:
* ObjFW's `[_headers mutableCopy]` on an `OFMutableDictionary *_headers;`
* ivar, the ivar a property synthesizes (`_name`, or the one `@synthesize`
* names), or `[self.weakCache objectForKey:]` on an `NSMapTable` property. A
* class it inherits from lends the ivars its `@interface` declares and its
* properties. A local or a parameter, which the shared inferrer reads first,
* shadows an ivar, and a bare name is never a property's: `arcs` in
* `initWithArcs:` is the parameter. Null when nothing declares it.
*/
function objcMemberReceiverType(receiver: string, ref: UnresolvedRef, context: ResolutionContext): string | null {
const viaSelf = receiver.startsWith('self.');
const name = viaSelf ? receiver.slice(5) : receiver;
if (!/^[A-Za-z_]\w*$/.test(name) || name === 'self' || name === 'super') return null;
// A capitalized name is a class (`[OFString stringWithFormat:]`), not an
// ivar, and `self.name` needs a property of that name.
if (viaSelf ? !context.getNodesByName(name).some((n) => n.kind === 'property' && n.language === 'objc') : /^[A-Z]/.test(name)) {
return null;
}
const hierarchy = objcHierarchyAt(ref, context);
if (!hierarchy) return null;
// What each class of the hierarchy declares, the calling one first.
const levels = [...hierarchy].map((cls) => objcClassFiles(cls, ref, context)
.map((file) => objcFileMembers(file, context).get(cls))
.filter((t): t is ObjcClassMembers => t !== undefined));
// A property's type, from the calling class up; `own`: only one the calling
// class declares and may synthesize, never a category's.
const propertyType = (key: string, own: boolean): string | undefined => {
for (const level of own ? levels.slice(0, 1) : levels) {
for (const t of level) {
const p = t.properties.get(key);
if (p && !(own && p.category)) return p.type;
}
}
return undefined;
};
if (viaSelf) return propertyType(name, false) ?? null;
for (const [i, level] of levels.entries()) {
for (const t of level) {
const type = t.ivars.get(name) ?? (i === 0 ? t.ownIvars.get(name) : undefined);
if (type) return type;
}
if (i > 0) continue;
const synthesized = level.map((t) => t.synthesized.get(name)).find((p) => p !== undefined);
const type = synthesized !== undefined ? propertyType(synthesized, false)
: name.startsWith('_') ? propertyType(name.slice(1), true) : undefined;
if (type) return type;
}
return null;
}
/** The superclasses an Objective-C class's `@interface` declarations name. */
function objcSupertypesOf(name: string, context: ResolutionContext): string[] {
let memo = OBJC_SUPERS.get(context);
@@ -14297,9 +14503,10 @@ function buildLocalReceiverTypePatterns(language: Language, r: string): RegExp[]
case 'objc':
return [
// FMResultSet *rs = … / NSString * _Nullable name; / a block's ^(FMResultSet *rs)
new RegExp(`\\b([A-Z]\\w*)\\s*(?:<[^<>;]*>\\s*)?\\*\\s*(?:(?:_Nullable|_Nonnull|__strong|__weak|__unsafe_unretained|const)\\s+)*${r}\\b(?!\\s*\\()`),
// ObjFW writes type arguments with a macro: `OFArray OF_GENERIC(OFString *) *names`.
new RegExp(`\\b([A-Z]\\w*)\\s*(?:<[^<>;]*>\\s*|[A-Z][A-Z0-9_]*\\s*\\([^;{}]*?\\)\\s*)?\\*\\s*(?:(?:_Nullable|_Nonnull|__strong|__weak|__unsafe_unretained|const)\\s+)*${r}\\b(?!\\s*\\()`),
// a method parameter: - (void)read:(nullable FMResultSet *)rs
new RegExp(`\\(\\s*(?:(?:nullable|nonnull|__kindof)\\s+)*([A-Z]\\w*)\\s*(?:<[^<>)]*>\\s*)?\\*[^)]*\\)\\s*${r}\\b`),
new RegExp(`\\(\\s*(?:(?:nullable|nonnull|__kindof)\\s+)*([A-Z]\\w*)\\s*(?:<[^<>)]*>\\s*|[A-Z][A-Z0-9_]*\\s*\\([^()]*\\)\\s*)?\\*[^)]*\\)\\s*${r}\\b`),
];
case 'swift':
return [
@@ -15429,6 +15636,11 @@ export function matchMethodCall(
inferredType = null;
}
}
// An Objective-C receiver the calling method doesn't declare may be an
// instance variable or a property of its class (objcMemberReceiverType).
if (!inferredType && ref.language === 'objc' && dotMatch) {
inferredType = nmTimedT('mc-objcmember', ref, () => objcMemberReceiverType(objectOrClass!, ref, context));
}
// A pytest test's parameter is what its fixture returns: flaskbb's
// `cli_runner.invoke(…)` is click's `CliRunner`, not the project's one `invoke`.
if (!inferredType && ref.language === 'python' && dotMatch) inferredType = pythonFixtureReturnType(objectOrClass!, ref, context);