mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 19:30:10 +08:00
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:
co-authored by
Claude Opus 5.5
parent
f2f5dee044
commit
70783f31f1
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user