[SymbolGraphGen] check implicit clang decls for having extension parents (#83143)

Resolves rdar://151911998

This PR addresses an uncommon situation in the symbol graph when
importing Objective-C symbols into Swift. When a class conforms to a
protocol that includes an initializer, that initializer is automatically
generated for that class in the AST. This initializer has the same USR
as the original protocol symbol, but is housed in a different
DeclContext to indicate the membership.

Right now, we catch this situation when the protocol conformance is
declared on the class definition: There's a branch to check for
"implicit" decls with an underlying Clang symbol, and creates a
synthesized USR if that symbols DeclContext points to a type. However,
when the protocol conformance is added in a category extension, the
DeclContext for the generated initializer points to the extension,
causing the symbol to bypass that check and get added to the symbol
graph with a duplicated USR. This PR adds a check to look for
ExtensionDecls as the DeclContext so that the symbol can correctly
receive a synthesized USR.

One of the tests in this PR
(`SymbolGraph/ClangImporter/ObjCInitializer.swift`) tests a similar
situation where this "implicit decl with a Clang node" is created: Some
initializers in Objective-C get imported into Swift twice, with
differently-adapted parameter names. This is covered by the original
code, but i wanted to leave the test in because i broke this case in my
initial investigation! 😅 The other test
(`SymbolGraph/ClangImporter/ProtocolInitializer.swift`) tests the new
behavior that is fixed by this PR.
This commit is contained in:
QuietMisdreavus
2025-07-18 09:04:16 -06:00
committed by GitHub
parent 0cd57218aa
commit 235f140b4c
3 changed files with 68 additions and 5 deletions

View File

@@ -308,7 +308,7 @@ bool SymbolGraphASTWalker::walkToDeclPre(Decl *D, CharSourceRange Range) {
// We also might establish some synthesized members because we
// extended an external type.
if (ExtendedNominal->getModuleContext() != &M) {
if (!areModulesEqual(ExtendedNominal->getModuleContext(), &M)) {
ExtendedSG->recordConformanceSynthesizedMemberRelationships(Source);
}
}
@@ -341,10 +341,13 @@ bool SymbolGraphASTWalker::walkToDeclPre(Decl *D, CharSourceRange Range) {
// symbol, regardless of which class it's actually appearing on. To prevent
// multiple of these symbols colliding with each other, treat them as
// synthesized symbols and use their parent type as the base type.
if (VD->isImplicit() && VD->hasClangNode() &&
VD->getClangNode().getAsDecl()) {
if (const auto *Parent =
dyn_cast_or_null<NominalTypeDecl>(VD->getDeclContext())) {
if (VD->isImplicit() && VD->getClangDecl()) {
const NominalTypeDecl *Parent = dyn_cast_or_null<NominalTypeDecl>(VD->getDeclContext());
if (!Parent) {
if (const auto *ParentExt = dyn_cast_or_null<ExtensionDecl>(VD->getDeclContext()))
Parent = ParentExt->getExtendedNominal();
}
if (Parent) {
SG->recordNode(Symbol(SG, VD, Parent));
return true;
}

View File

@@ -0,0 +1,28 @@
// REQUIRES: objc_interop
// RUN: %empty-directory(%t)
// RUN: split-file %s %t
// RUN: %target-swift-symbolgraph-extract -sdk %clang-importer-sdk -module-name ObjCInitializer -Fsystem %t -output-dir %t -pretty-print -v
// RUN: %FileCheck %s --input-file %t/ObjCInitializer.symbols.json
// RUN: %FileCheck %s --input-file %t/ObjCInitializer.symbols.json --check-prefix SYNTH
//--- ObjCInitializer.framework/Modules/module.modulemap
framework module ObjCInitializer [system] {
header "ObjCInitializer.h"
}
//--- ObjCInitializer.framework/Headers/ObjCInitializer.h
@import Foundation;
@interface NSScrubberLayoutAttributes : NSObject <NSCopying>
// This initializer gets imported twice - once as `init(forItemAt:)` and once as
// `init(forItemAtIndex:)`. Make sure one of them gets a synthesized USR
// CHECK: "precise": "c:objc(cs)NSScrubberLayoutAttributes(cm)layoutAttributesForItemAtIndex:"
// CHECK-NOT: "precise": "c:objc(cs)NSScrubberLayoutAttributes(cm)layoutAttributesForItemAtIndex:"
// SYNTH: "precise": "c:objc(cs)NSScrubberLayoutAttributes(cm)layoutAttributesForItemAtIndex:::SYNTHESIZED::c:objc(cs)NSScrubberLayoutAttributes",
// SYNTH-NOT: "precise": "c:objc(cs)NSScrubberLayoutAttributes(cm)layoutAttributesForItemAtIndex:::SYNTHESIZED::c:objc(cs)NSScrubberLayoutAttributes",
+ (instancetype)layoutAttributesForItemAtIndex:(NSInteger)index;
@end

View File

@@ -0,0 +1,32 @@
// REQUIRES: objc_interop
// RUN: %empty-directory(%t)
// RUN: split-file %s %t
// RUN: %target-swift-symbolgraph-extract -sdk %clang-importer-sdk -module-name ProtocolInitializer -I %t/ProtocolInitializer -output-dir %t -pretty-print -v
// RUN: %FileCheck %s --input-file %t/ProtocolInitializer.symbols.json
// the initializer's base USR should only appear as the source of one relationship
// CHECK: "source": "c:objc(pl)MyProtocol(im)initWithSomeNumber:"
// CHECK-NOT: "source": "c:objc(pl)MyProtocol(im)initWithSomeNumber:"
//--- ProtocolInitializer/module.modulemap
module ProtocolInitializer {
header "ProtocolInitializer.h"
}
//--- ProtocolInitializer/ProtocolInitializer.h
@import Foundation;
@protocol MyProtocol <NSObject>
@optional
- (nullable id)initWithSomeNumber:(NSInteger)number;
@end
@interface MyClass : NSObject
@end
@interface MyClass () <MyProtocol>
@end