Skip to content

Commit 8c9a6ce

Browse files
committed
Increase determinism of selector conflict errors
Refactor ObjC conflict diagnosis code to sort conflict data more thoroughly, filter out unwanted declarations earlier, and just generally behave in ways that are more likely to work correctly. This change increases the determinism of the ordering of diagnostics and the selection of the “correct” declaration that the others are considered to conflict with, increasing my confidence that the diagnostics will work correctly in untested corner cases or if the compiler is refactored so that declarations are recorded in a different order. It also adds a new selection rule—@objc without vs. with explicit selector—that I believe will slightly improve the diagnostics we produce. And it replaces a lot of really dodgy-looking logic that may have only worked reliably when a conflict involved exactly two methods.
1 parent d3c7005 commit 8c9a6ce

File tree

5 files changed

+128
-106
lines changed

5 files changed

+128
-106
lines changed

lib/Sema/TypeCheckDeclObjC.cpp

Lines changed: 121 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -2257,12 +2257,6 @@ namespace {
22572257
/// Produce a deterministic ordering of the given declarations.
22582258
struct OrderDeclarations {
22592259
bool operator()(ValueDecl *lhs, ValueDecl *rhs) const {
2260-
// If one declaration is imported from ObjC and the other is native Swift,
2261-
// put the imported Clang one first.
2262-
if (lhs->hasClangNode() != rhs->hasClangNode()) {
2263-
return lhs->hasClangNode();
2264-
}
2265-
22662260
// If the declarations come from different modules, order based on the
22672261
// module.
22682262
ModuleDecl *lhsModule = lhs->getDeclContext()->getParentModule();
@@ -2425,13 +2419,6 @@ bool swift::diagnoseUnintendedObjCMethodOverrides(SourceFile &sf) {
24252419
return diagnosedAny;
24262420
}
24272421

2428-
/// Retrieve the source file for the given Objective-C member conflict.
2429-
static TinyPtrVector<AbstractFunctionDecl *>
2430-
getObjCMethodConflictDecls(const SourceFile::ObjCMethodConflict &conflict) {
2431-
return conflict.typeDecl->lookupDirect(conflict.selector,
2432-
conflict.isInstanceMethod);
2433-
}
2434-
24352422
static ObjCAttr *getObjCAttrIfFromAccessNote(ValueDecl *VD) {
24362423
if (auto objc = VD->getAttrs().getAttribute<ObjCAttr>())
24372424
if (objc->getAddedByAccessNote())
@@ -2443,99 +2430,138 @@ static ObjCAttr *getObjCAttrIfFromAccessNote(ValueDecl *VD) {
24432430
return nullptr;
24442431
}
24452432

2433+
static bool hasCustomObjCName(AbstractFunctionDecl *afd) {
2434+
if (auto objc = afd->getAttrs().getAttribute<ObjCAttr>())
2435+
return objc->hasName();
2436+
return false;
2437+
}
2438+
2439+
/// Retrieve the methods involved in a specific Objective-C selector
2440+
/// conflict. The list will be sorted so that the first method is the "best" one
2441+
/// and the others can be diagnosed as conflicts with that one.
2442+
static TinyPtrVector<AbstractFunctionDecl *>
2443+
getObjCMethodConflictDecls(const SourceFile::ObjCMethodConflict &conflict) {
2444+
// Look up all methods involved in the conflict.
2445+
auto methods = conflict.typeDecl->lookupDirect(conflict.selector,
2446+
conflict.isInstanceMethod);
2447+
2448+
// Erase any invalid or stub declarations. We don't want to complain about
2449+
// them, because we might already have complained about redeclarations
2450+
// based on Swift matching.
2451+
llvm::erase_if(methods, [](AbstractFunctionDecl *afd) -> bool {
2452+
if (afd->isInvalid())
2453+
return true;
2454+
2455+
if (auto ad = dyn_cast<AccessorDecl>(afd))
2456+
return ad->getStorage()->isInvalid();
2457+
2458+
if (auto *ctor = dyn_cast<ConstructorDecl>(afd)) {
2459+
if (ctor->hasStubImplementation())
2460+
return true;
2461+
}
2462+
return false;
2463+
});
2464+
2465+
// Sort the conflicting methods from the "strongest" claim to the "weakest".
2466+
// This puts the "best" method at methods.front() so that others will be
2467+
// diagnosed as conflicts with that one, and it helps ensure that individual
2468+
// methods in a conflict set are diagnosed in a deterministic order.
2469+
llvm::stable_sort(methods,
2470+
[](AbstractFunctionDecl *a, AbstractFunctionDecl *b) {
2471+
#define RULE(aCriterion, bCriterion) do { \
2472+
bool _aCriterion = (aCriterion), _bCriterion = (bCriterion); \
2473+
if (!_aCriterion && _bCriterion) \
2474+
return false; \
2475+
if (_aCriterion && !_bCriterion) \
2476+
return true; \
2477+
} while (0)
2478+
2479+
// Is one of these from Objective-C and the other from Swift?
2480+
// NOTE: Inserting another rule above this will break the hasClangNode()
2481+
// filtering below.
2482+
RULE(a->hasClangNode(),
2483+
b->hasClangNode());
2484+
2485+
// Is one of these async and the other not?
2486+
RULE(a->hasAsync(),
2487+
b->hasAsync());
2488+
2489+
// Is one of these explicit and the other from an access note?
2490+
RULE(!getObjCAttrIfFromAccessNote(a),
2491+
!getObjCAttrIfFromAccessNote(b));
2492+
2493+
// Is one of these from the main decl and the other an extension?
2494+
RULE(!isa<ExtensionDecl>(a->getDeclContext()),
2495+
!isa<ExtensionDecl>(b->getDeclContext()));
2496+
2497+
// Does one of these use plain @objc and the other @objc(selector)?
2498+
RULE(!hasCustomObjCName(a),
2499+
!hasCustomObjCName(b));
2500+
2501+
// Neither has a "stronger" claim, so just try to put them in some sort of
2502+
// consistent order.
2503+
OrderDeclarations ordering;
2504+
return ordering(a, b);
2505+
2506+
#undef RULE
2507+
});
2508+
2509+
// If the best method is imported from ObjC, eliminate any other imported ObjC
2510+
// methods. Selector conflicts between imported ObjC methods are spurious;
2511+
// they're just the same ObjC method being imported under different names with
2512+
// different ImportNameVersions.
2513+
if (!methods.empty() && methods.front()->hasClangNode())
2514+
llvm::erase_if(methods, [&](AbstractFunctionDecl *afd) {
2515+
return afd != methods.front() && afd->hasClangNode();
2516+
});
2517+
2518+
return methods;
2519+
}
2520+
24462521
bool swift::diagnoseObjCMethodConflicts(SourceFile &sf) {
24472522
// If there were no conflicts, we're done.
24482523
if (sf.ObjCMethodConflicts.empty())
24492524
return false;
24502525

24512526
auto &Ctx = sf.getASTContext();
24522527
DiagnosticStateRAII diagState(Ctx.Diags);
2453-
OrderDeclarations ordering;
24542528

2455-
// Sort the set of conflicts so we get a deterministic order for
2456-
// diagnostics. We use the first conflicting declaration in each set to
2457-
// perform the sort.
2458-
llvm::SmallVector<SourceFile::ObjCMethodConflict, 4> localConflicts;
2459-
llvm::copy(sf.ObjCMethodConflicts, std::back_inserter(localConflicts));
2460-
std::sort(localConflicts.begin(), localConflicts.end(),
2461-
[&](const SourceFile::ObjCMethodConflict &lhs,
2462-
const SourceFile::ObjCMethodConflict &rhs) {
2463-
return ordering(getObjCMethodConflictDecls(lhs)[1],
2464-
getObjCMethodConflictDecls(rhs)[1]);
2465-
});
2466-
2467-
// Diagnose each conflict.
2468-
bool anyConflicts = false;
2469-
for (const auto &conflict : localConflicts) {
2529+
// Build a list of all the conflicts and the methods involved in them.
2530+
using ConflictSet = std::pair<SourceFile::ObjCMethodConflict,
2531+
TinyPtrVector<AbstractFunctionDecl *>>;
2532+
llvm::SmallVector<ConflictSet, 4> conflictSets;
2533+
for (auto conflict : sf.ObjCMethodConflicts) {
24702534
auto methods = getObjCMethodConflictDecls(conflict);
2471-
2472-
// Erase any invalid or stub declarations. We don't want to complain about
2473-
// them, because we might already have complained about redeclarations
2474-
// based on Swift matching.
2475-
llvm::erase_if(methods, [](AbstractFunctionDecl *afd) -> bool {
2476-
if (afd->isInvalid())
2477-
return true;
2478-
2479-
if (auto ad = dyn_cast<AccessorDecl>(afd))
2480-
return ad->getStorage()->isInvalid();
2481-
2482-
if (auto *ctor = dyn_cast<ConstructorDecl>(afd)) {
2483-
if (ctor->hasStubImplementation())
2484-
return true;
2485-
}
2486-
return false;
2487-
});
2488-
24892535
if (methods.size() < 2)
24902536
continue;
2537+
conflictSets.emplace_back(conflict, methods);
2538+
}
2539+
2540+
// Sort the set of conflicts so the different conflict sets are diagnosed in
2541+
// the same order. We use the first conflicting declaration in each set to
2542+
// perform the sort.
2543+
llvm::stable_sort(conflictSets,
2544+
[](const ConflictSet &lhs, const ConflictSet &rhs) {
2545+
OrderDeclarations ordering;
2546+
return ordering(lhs.second[1], rhs.second[1]);
2547+
});
24912548

2549+
// Diagnose each conflict.
2550+
bool anyConflicts = false;
2551+
for (const auto &conflictSet : conflictSets) {
24922552
// Diagnose the conflict.
24932553
anyConflicts = true;
2554+
2555+
const auto &conflict = conflictSet.first;
2556+
const auto &methods = conflictSet.second;
24942557

2495-
/// If true, \p a has a "weaker" claim on the selector than \p b, and the
2496-
/// conflict diagnostic should appear on \p a instead of \p b.
2497-
auto areBackwards =
2498-
[&](AbstractFunctionDecl *a, AbstractFunctionDecl *b) -> bool {
2499-
#define RULE(aCriterion, bCriterion) do { \
2500-
bool _aCriterion = (aCriterion), _bCriterion = (bCriterion); \
2501-
if (!_aCriterion && _bCriterion) \
2502-
return true; \
2503-
if (_aCriterion && !_bCriterion) \
2504-
return false; \
2505-
} while (0)
2506-
2507-
// Is one of these from an access note?
2508-
RULE(getObjCAttrIfFromAccessNote(a),
2509-
getObjCAttrIfFromAccessNote(b));
2510-
2511-
// Is one of these from the main declaration?
2512-
RULE(!isa<ExtensionDecl>(a->getDeclContext()),
2513-
!isa<ExtensionDecl>(b->getDeclContext()));
2514-
2515-
// Is one of these imported from Objective-C?
2516-
RULE(a->hasClangNode(),
2517-
b->hasClangNode());
2518-
2519-
// Are these from different source files? If so, fall back to the order in
2520-
// which the declarations were type checked.
2521-
// FIXME: This is gross and nondeterministic.
2522-
if (a->getParentSourceFile() != b->getParentSourceFile())
2523-
return false;
2524-
2525-
// Handle them in source order.
2526-
return !ordering(a, b);
2527-
2528-
#undef RULE
2529-
};
2530-
2531-
MutableArrayRef<AbstractFunctionDecl *> methodsRef(methods);
2532-
if (areBackwards(methods[0], methods[1]))
2533-
std::swap(methodsRef[0], methodsRef[1]);
2534-
2558+
ArrayRef<AbstractFunctionDecl *> methodsRef(methods);
25352559
auto originalMethod = methods.front();
2536-
auto conflictingMethods = methodsRef.slice(1);
2537-
25382560
auto origDiagInfo = getObjCMethodDiagInfo(originalMethod);
2561+
bool originalIsImportedAsync = originalMethod->hasClangNode() &&
2562+
originalMethod->hasAsync();
2563+
2564+
auto conflictingMethods = methodsRef.slice(1);
25392565
for (auto conflictingDecl : conflictingMethods) {
25402566
auto diagInfo = getObjCMethodDiagInfo(conflictingDecl);
25412567

@@ -2544,17 +2570,13 @@ bool swift::diagnoseObjCMethodConflicts(SourceFile &sf) {
25442570
if (auto accessor = dyn_cast<AccessorDecl>(originalMethod))
25452571
originalDecl = accessor->getStorage();
25462572

2547-
// Conflicts between two imported ObjC methods are caused by the same
2548-
// clang decl having several Swift names.
2549-
if (originalDecl->hasClangNode() && conflictingDecl->hasClangNode())
2550-
continue;
2573+
// In Swift 5.7, we discovered cases which inadvertently bypassed selector
2574+
// conflict checking and have to be diagnosed as warnings in Swift 5:
25512575

2552-
bool breakingInSwift5 = false;
2553-
// Imported ObjC async methods weren't fully checked for selector
2554-
// conflicts until 5.7.
2555-
if (originalMethod->hasClangNode() && originalMethod->hasAsync())
2556-
breakingInSwift5 = true;
2557-
// Protocols weren't checked for selector conflicts in 5.0.
2576+
// * Selectors for imported methods with async variants.
2577+
bool breakingInSwift5 = originalIsImportedAsync;
2578+
2579+
// * Protocol requirements
25582580
if (!isa<ClassDecl>(conflict.typeDecl))
25592581
breakingInSwift5 = true;
25602582

test/decl/Inputs/objc_redeclaration_multi_2.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ extension Redecl1 {
55
@objc(init)
66
func initialize() { } // expected-error{{method 'initialize()' with Objective-C selector 'init' conflicts with initializer 'init()' with the same Objective-C selector}}
77

8-
@objc func method2() { } // expected-error{{method 'method2()' with Objective-C selector 'method2' conflicts with method 'method2_alias()' with the same Objective-C selector}}
8+
@objc func method2() { } // expected-note{{method 'method2()' declared here}}
99
}
1010

1111
@objc class Redecl2 {

test/decl/objc_redeclaration.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ extension Redecl1 {
2626
@objc var method1_var_alias: Int {
2727
@objc(method1) get { return 5 } // expected-error{{getter for 'method1_var_alias' with Objective-C selector 'method1' conflicts with method 'method1()' with the same Objective-C selector}}
2828

29-
@objc(method2:) set { } // expected-note{{setter for 'method1_var_alias' declared here}}
29+
@objc(method2:) set { } // expected-error{{setter for 'method1_var_alias' with Objective-C selector 'method2:' conflicts with method 'method2' with the same Objective-C selector}}
3030
}
3131

3232
@objc subscript (i: Int) -> Redecl1 {
@@ -37,7 +37,7 @@ extension Redecl1 {
3737

3838
extension Redecl1 {
3939
@objc
40-
func method2(_ x: Int) { } // expected-error{{method 'method2' with Objective-C selector 'method2:' conflicts with setter for 'method1_var_alias' with the same Objective-C selector}}
40+
func method2(_ x: Int) { } // expected-note{{method 'method2' declared here}}
4141

4242
@objc(objectAtIndexedSubscript:)
4343
func indexed(_ x: Int) { } // expected-error{{method 'indexed' with Objective-C selector 'objectAtIndexedSubscript:' conflicts with subscript getter with the same Objective-C selector}}

test/decl/objc_redeclaration_multi.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ extension Redecl2 {
1616
}
1717

1818
extension Redecl1 {
19-
@objc(method2) func method2_alias() { } // expected-note{{method 'method2_alias()' declared here}}
19+
@objc(method2) func method2_alias() { } // expected-error{{method 'method2_alias()' with Objective-C selector 'method2' conflicts with method 'method2()' with the same Objective-C selector}}
2020
}

test/decl/protocol/objc.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ class C8SubRW2: C8Base {
261261

262262
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
263263
@objc protocol P9 {
264-
@objc(custom:) func f(_: Any) // expected-note 2 {{method 'f' declared here}}
265-
@objc(custom:) func g(_: Any) // expected-warning {{method 'g' with Objective-C selector 'custom:' conflicts with method 'f' with the same Objective-C selector; this is an error in Swift 6}}
266-
@objc(custom:) func h() async // expected-warning {{method 'h()' with Objective-C selector 'custom:' conflicts with method 'f' with the same Objective-C selector; this is an error in Swift 6}}
264+
@objc(custom:) func f(_: Any) // expected-warning {{method 'f' with Objective-C selector 'custom:' conflicts with method 'h()' with the same Objective-C selector; this is an error in Swift 6}}
265+
@objc(custom:) func g(_: Any) // expected-warning {{method 'g' with Objective-C selector 'custom:' conflicts with method 'h()' with the same Objective-C selector; this is an error in Swift 6}}
266+
@objc(custom:) func h() async // expected-note 2 {{method 'h()' declared here}}
267267
}

0 commit comments

Comments
 (0)