Ports the #645/#608 chained-receiver mechanism to Objective-C. A message send whose receiver is itself a message send — `[[Foo create] doIt]` — used to drop the receiver, so `doIt` name-matched a same-named method on an unrelated class (commonly a test helper's `init` or an Apple-SDK method). - objc.ts: getReturnType reads the method's `method_type`, SKIPPING nullability / ARC qualifiers (`nonnull instancetype` must yield instancetype, not `nonnull`). - tree-sitter.ts: the message_expression branch now re-encodes a chained send `[[Foo create] doIt]` as `Foo.create().doIt` when the inner receiver is a capitalized class and the outer selector is unary. - name-matcher.ts: `objc` joins the dotted-chain gate + CHAIN_LANGUAGES. A class-message factory returns an instance of the RECEIVER class by convention (`instancetype`), so when the factory's own return type isn't recoverable (`alloc`/`new`/`shared…` return instancetype, or aren't user nodes), the receiver's type is the class itself — this resolves the ubiquitous `[[X alloc] init]` and singleton chains. resolveMethodOnType validates against the class and its supertypes, so a wrong inference yields no edge. Validation: 4 synthetic tests (factory+decoy, superclass conformance, absent-method safety, the nonnull-instancetype singleton). Real-repo A/B on SDWebImage (208 files): +35 / -75 — all corrections (the -75 are wrong `init` mis-matches to a test helper / wrong class, retargeted to the right class's init in the +35, plus 2 Apple-SDK chains on unindexed classes). db stable, no node explosion. EXTRACTION_VERSION 14->15. Full suite green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
178 lines
7.0 KiB
TypeScript
178 lines
7.0 KiB
TypeScript
import type { Node as SyntaxNode } from 'web-tree-sitter';
|
|
import { getChildByField, getNodeText } from '../tree-sitter-helpers';
|
|
import type { ExtractorContext, LanguageExtractor } from '../tree-sitter-types';
|
|
|
|
function findCompoundStatement(node: SyntaxNode): SyntaxNode | null {
|
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
const child = node.namedChild(i);
|
|
if (child?.type === 'compound_statement') {
|
|
return child;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Build ObjC selector: `greet`, `doThing:`, or `doThing:with:`. */
|
|
function extractObjcMethodName(node: SyntaxNode, source: string): string | undefined {
|
|
if (node.type !== 'method_definition' && node.type !== 'method_declaration') {
|
|
return undefined;
|
|
}
|
|
|
|
const identifiers = node.namedChildren.filter((c) => c.type === 'identifier');
|
|
if (identifiers.length === 0) return undefined;
|
|
|
|
const hasParameters = node.namedChildren.some((c) => c.type === 'method_parameter');
|
|
const firstIdentifier = identifiers[0];
|
|
if (!firstIdentifier) return undefined;
|
|
if (!hasParameters) {
|
|
return getNodeText(firstIdentifier, source);
|
|
}
|
|
|
|
return identifiers.map((id) => `${getNodeText(id, source)}:`).join('');
|
|
}
|
|
|
|
/** Nullability / ARC qualifiers that sit where a return type's first type
|
|
* identifier does (`(nonnull instancetype)`, `(nullable Bar *)`) — never the type. */
|
|
const OBJC_TYPE_QUALIFIERS = new Set([
|
|
'nonnull', 'nullable', 'null_unspecified', 'null_resettable',
|
|
'_Nonnull', '_Nullable', '_Null_unspecified', '__nonnull', '__nullable',
|
|
'const', 'volatile', 'strong', 'weak', 'copy', 'assign', 'retain', 'oneway',
|
|
'__strong', '__weak', '__unsafe_unretained', '__autoreleasing', '__kindof',
|
|
]);
|
|
|
|
/** Collect the type identifiers under a `method_type`, in document order. */
|
|
function collectTypeIdentifiers(node: SyntaxNode, source: string, out: string[]): void {
|
|
if (node.type === 'type_identifier') out.push(getNodeText(node, source).trim());
|
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
const child = node.namedChild(i);
|
|
if (child) collectTypeIdentifiers(child, source, out);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Capture an ObjC method's declared return type as a bare class name, for the
|
|
* chained static-factory call mechanism (#750). `+ (Bar *)create` yields `Bar`;
|
|
* a nullability/ARC qualifier (`(nonnull instancetype)`, `(nullable Bar *)`) is
|
|
* skipped to reach the real type. `void` / `id` / `instancetype` / primitives
|
|
* yield undefined — for a class-message factory that means the receiver's type
|
|
* is the class itself (handled in resolution), so `[[X alloc] init]` and
|
|
* singleton chains still resolve.
|
|
*/
|
|
function extractObjcReturnType(node: SyntaxNode, source: string): string | undefined {
|
|
if (node.type !== 'method_definition' && node.type !== 'method_declaration') return undefined;
|
|
const methodType = node.namedChildren.find((c) => c.type === 'method_type');
|
|
if (!methodType) return undefined;
|
|
const ids: string[] = [];
|
|
collectTypeIdentifiers(methodType, source, ids);
|
|
const name = ids.find((n) => !OBJC_TYPE_QUALIFIERS.has(n));
|
|
if (!name || !/^[A-Za-z_]\w*$/.test(name) || name === 'void' || name === 'id' || name === 'instancetype') {
|
|
return undefined;
|
|
}
|
|
return name;
|
|
}
|
|
|
|
function extractObjcPropertyName(node: SyntaxNode, source: string): string | null {
|
|
if (node.type !== 'property_declaration') return null;
|
|
|
|
const structDecl = node.namedChildren.find((c) => c.type === 'struct_declaration');
|
|
if (!structDecl) return null;
|
|
|
|
const structDeclarator = structDecl.namedChildren.find((c) => c.type === 'struct_declarator');
|
|
if (!structDeclarator) return null;
|
|
|
|
let current: SyntaxNode | null = structDeclarator;
|
|
while (current) {
|
|
const inner: SyntaxNode | undefined =
|
|
getChildByField(current, 'declarator') ||
|
|
current.namedChildren.find((c) => c.type === 'identifier' || c.type === 'pointer_declarator');
|
|
if (!inner) break;
|
|
if (inner.type === 'identifier') {
|
|
return getNodeText(inner, source);
|
|
}
|
|
current = inner;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export const objcExtractor: LanguageExtractor = {
|
|
functionTypes: ['function_definition'],
|
|
// Only @interface emits a class node; @implementation reuses it via visitNode.
|
|
classTypes: ['class_interface'],
|
|
methodTypes: ['method_definition'],
|
|
interfaceTypes: ['protocol_declaration'],
|
|
interfaceKind: 'protocol',
|
|
structTypes: ['struct_specifier'],
|
|
enumTypes: ['enum_specifier'],
|
|
enumMemberTypes: ['enumerator'],
|
|
typeAliasTypes: ['type_definition'],
|
|
importTypes: ['preproc_include'],
|
|
callTypes: ['call_expression', 'message_expression'],
|
|
variableTypes: ['declaration'],
|
|
propertyTypes: ['property_declaration'],
|
|
nameField: 'declarator',
|
|
bodyField: 'body',
|
|
paramsField: 'parameters',
|
|
getReturnType: extractObjcReturnType,
|
|
resolveName: extractObjcMethodName,
|
|
extractPropertyName: extractObjcPropertyName,
|
|
resolveBody: (node, bodyField) => {
|
|
const fromField = getChildByField(node, bodyField);
|
|
if (fromField) {
|
|
return fromField;
|
|
}
|
|
return findCompoundStatement(node);
|
|
},
|
|
resolveTypeAliasKind: (node, _source) => {
|
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
const child = node.namedChild(i);
|
|
if (!child) continue;
|
|
if (child.type === 'enum_specifier' && getChildByField(child, 'body')) return 'enum';
|
|
if (child.type === 'struct_specifier' && getChildByField(child, 'body')) return 'struct';
|
|
}
|
|
return undefined;
|
|
},
|
|
isStatic: (node) => /^\s*\+/.test(node.text),
|
|
visitNode: (node, ctx: ExtractorContext) => {
|
|
if (node.type !== 'class_implementation') return false;
|
|
|
|
const classNameNode = node.namedChildren.find((c) => c.type === 'identifier');
|
|
if (!classNameNode) return true;
|
|
|
|
const className = getNodeText(classNameNode, ctx.source);
|
|
const classNode =
|
|
ctx.nodes.find(
|
|
(n) => n.name === className && n.filePath === ctx.filePath && n.kind === 'class'
|
|
) ?? ctx.createNode('class', className, node, {});
|
|
if (!classNode) return true;
|
|
|
|
ctx.pushScope(classNode.id);
|
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
const child = node.namedChild(i);
|
|
if (child?.type === 'implementation_definition') {
|
|
for (let j = 0; j < child.namedChildCount; j++) {
|
|
const implChild = child.namedChild(j);
|
|
if (implChild) ctx.visitNode(implChild);
|
|
}
|
|
}
|
|
}
|
|
ctx.popScope();
|
|
return true;
|
|
},
|
|
extractImport: (node, source) => {
|
|
const importText = source.substring(node.startIndex, node.endIndex).trim();
|
|
const systemLib = node.namedChildren.find((c: SyntaxNode) => c.type === 'system_lib_string');
|
|
if (systemLib) {
|
|
return { moduleName: getNodeText(systemLib, source).replace(/^<|>$/g, ''), signature: importText };
|
|
}
|
|
const stringLiteral = node.namedChildren.find((c: SyntaxNode) => c.type === 'string_literal');
|
|
if (stringLiteral) {
|
|
const stringContent = stringLiteral.namedChildren.find((c: SyntaxNode) => c.type === 'string_content');
|
|
if (stringContent) {
|
|
return { moduleName: getNodeText(stringContent, source), signature: importText };
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
};
|