Skip to content

Commit d817f3c

Browse files
committed
[Conformance checking] Consider base name + first argument label together for near-miss checking.
The Grand API Renaming tends to split long base names into a shorter base name + first argument label. To account for this in near-miss checking, consider the base name + first argument label as a unit.
1 parent 7c3f50d commit d817f3c

File tree

2 files changed

+101
-57
lines changed

2 files changed

+101
-57
lines changed

lib/Sema/TypeCheckProtocol.cpp

Lines changed: 75 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "MiscDiagnostics.h"
2020
#include "TypeChecker.h"
2121
#include "swift/Basic/SourceManager.h"
22+
#include "swift/Basic/StringExtras.h"
2223
#include "swift/AST/ArchetypeBuilder.h"
2324
#include "swift/AST/ASTContext.h"
2425
#include "swift/AST/Decl.h"
@@ -4109,25 +4110,41 @@ void TypeChecker::checkConformance(NormalProtocolConformance *conformance) {
41094110

41104111
/// Determine the score when trying to match two identifiers together.
41114112
static unsigned scoreIdentifiers(Identifier lhs, Identifier rhs,
4112-
unsigned limit, bool isFirstParamOfFunc) {
4113+
unsigned limit) {
41134114
// Simple case: we have the same identifier.
41144115
if (lhs == rhs) return 0;
41154116

4116-
// One of the identifiers is empty.
4117-
if (lhs.empty() != rhs.empty()) {
4118-
// Attribute a score of "1" when the first argument of a function is
4119-
// present in one case but absent in the other, because this was a change
4120-
// from Swift 2 to Swift 3.
4121-
if (isFirstParamOfFunc) return 1;
4122-
4123-
// Otherwise, use the length of the non-empty identifier.
4117+
// One of the identifiers is empty. Use the length of the non-empty
4118+
// identifier.
4119+
if (lhs.empty() != rhs.empty())
41244120
return lhs.empty() ? rhs.str().size() : lhs.str().size();
4125-
}
41264121

41274122
// Compute the edit distance between the two names.
41284123
return lhs.str().edit_distance(rhs.str(), true, limit);
41294124
}
41304125

4126+
/// Combine the given base name and first argument label into a single
4127+
/// name.
4128+
static StringRef
4129+
combineBaseNameAndFirstArgument(Identifier baseName,
4130+
Identifier firstArgName,
4131+
SmallVectorImpl<char> &scratch) {
4132+
// Handle cases where one or the other name is empty.
4133+
if (baseName.empty()) {
4134+
if (firstArgName.empty()) return "";
4135+
return firstArgName.str();
4136+
}
4137+
4138+
if (firstArgName.empty())
4139+
return baseName.str();
4140+
4141+
// Append the first argument name to the base name.
4142+
scratch.clear();
4143+
scratch.append(baseName.str().begin(), baseName.str().end());
4144+
camel_case::appendSentenceCase(scratch, firstArgName.str());
4145+
return StringRef(scratch.data(), scratch.size());
4146+
}
4147+
41314148
/// Compute the scope between two potentially-matching names, which is
41324149
/// effectively the sum of the edit distances between the corresponding
41334150
/// argument labels.
@@ -4138,17 +4155,33 @@ static unsigned scorePotentiallyMatchingNames(DeclName lhs, DeclName rhs,
41384155
if (lhs.getArgumentNames().size() != rhs.getArgumentNames().size())
41394156
return limit;
41404157

4141-
// Score the base name match.
4142-
unsigned score = scoreIdentifiers(lhs.getBaseName(), rhs.getBaseName(),
4143-
limit, false);
4158+
// Score the base name match. If there is a first argument for a
4159+
// function, include its text along with the base name's text.
4160+
unsigned score;
4161+
if (lhs.getArgumentNames().empty() || !isFunc) {
4162+
score = scoreIdentifiers(lhs.getBaseName(), rhs.getBaseName(), limit);
4163+
} else {
4164+
llvm::SmallString<16> lhsScratch;
4165+
StringRef lhsFirstName =
4166+
combineBaseNameAndFirstArgument(lhs.getBaseName(),
4167+
lhs.getArgumentNames()[0],
4168+
lhsScratch);
4169+
4170+
llvm::SmallString<16> rhsScratch;
4171+
StringRef rhsFirstName =
4172+
combineBaseNameAndFirstArgument(rhs.getBaseName(),
4173+
rhs.getArgumentNames()[0],
4174+
rhsScratch);
4175+
4176+
score = lhsFirstName.edit_distance(rhsFirstName.str(), true, limit);
4177+
}
41444178
if (score >= limit) return limit;
41454179

41464180
// Compute the edit distance between matching argument names.
4147-
for (unsigned i = 0; i != lhs.getArgumentNames().size(); ++i) {
4181+
for (unsigned i = isFunc ? 1 : 0; i < lhs.getArgumentNames().size(); ++i) {
41484182
score += scoreIdentifiers(lhs.getArgumentNames()[i],
41494183
rhs.getArgumentNames()[i],
4150-
limit - score,
4151-
isFunc && i == 0);
4184+
limit - score);
41524185
if (score >= limit) return limit;
41534186
}
41544187

@@ -4270,15 +4303,14 @@ static bool shouldWarnAboutPotentialWitness(ValueDecl *req,
42704303
if (!attr->isImplicit()) return false;
42714304
}
42724305

4273-
// If the score is relatively high, don't warn: this is probably unrelated.
4274-
// The heuristic we use here is to ignore outright omissions and determine
4275-
// whether there was more than one typo for every two characters. If so,
4276-
// consider it "different".
4306+
// If the score is relatively high, don't warn: this is probably
4307+
// unrelated. Allow about one typo for every two properly-typed
4308+
// characters, which prevents completely-wacky suggestions in many
4309+
// cases.
42774310
unsigned reqNameLen = getNameLength(req->getFullName());
42784311
unsigned witnessNameLen = getNameLength(witness->getFullName());
4279-
unsigned lengthDiff =
4280-
std::max(reqNameLen, witnessNameLen) - std::min(reqNameLen, witnessNameLen);
4281-
if ((score - lengthDiff) > (witnessNameLen + 1) / 3) return false;
4312+
if (score > (std::min(reqNameLen, witnessNameLen) + 1) / 3)
4313+
return false;
42824314

42834315
return true;
42844316
}
@@ -4380,8 +4412,7 @@ void TypeChecker::checkConformancesInContext(DeclContext *dc,
43804412
/*sorted=*/true);
43814413
// Catalog all of members of this declaration context that satisfy
43824414
// requirements of conformances in this context.
4383-
llvm::MapVector<DeclName, llvm::TinyPtrVector<ValueDecl *>>
4384-
unsatisfiedReqs;
4415+
SmallVector<ValueDecl *, 16> unsatisfiedReqs;
43854416

43864417
bool anyInvalid = false;
43874418
for (auto conformance : conformances) {
@@ -4405,7 +4436,7 @@ void TypeChecker::checkConformancesInContext(DeclContext *dc,
44054436
// If the requirement is unsatisfied, we might want to warn
44064437
// about near misses; record it.
44074438
if (isUnsatisfiedReq(normal, req)) {
4408-
unsatisfiedReqs[req->getBaseName()].push_back(req);
4439+
unsatisfiedReqs.push_back(req);
44094440
continue;
44104441
}
44114442
}
@@ -4462,16 +4493,11 @@ void TypeChecker::checkConformancesInContext(DeclContext *dc,
44624493
if (knownWitnesses.count(value) > 0) continue;
44634494
if (!value->getFullName()) continue;
44644495

4465-
// Consider any unsatisfied requirements with the same base
4466-
// name.
4467-
auto reqs = unsatisfiedReqs.find(value->getBaseName());
4468-
if (reqs == unsatisfiedReqs.end()) continue;
4469-
44704496
// Find the unsatisfied requirements with the nearest-matching
44714497
// names.
44724498
SmallVector<ValueDecl *, 4> bestOptionalReqs;
44734499
unsigned bestScore = UINT_MAX;
4474-
for (auto req : reqs->second) {
4500+
for (auto req : unsatisfiedReqs) {
44754501
// Score this particular optional requirement.
44764502
auto score = scorePotentiallyMatchingNames(value->getFullName(),
44774503
req->getFullName(),
@@ -4511,37 +4537,31 @@ void TypeChecker::checkConformancesInContext(DeclContext *dc,
45114537

45124538
// Remove this optional requirement from the list. We don't want to
45134539
// complain about it twice.
4514-
if (reqs->second.size() == 1) {
4515-
unsatisfiedReqs.erase(reqs);
4516-
} else {
4517-
reqs->second.erase(std::find(reqs->second.begin(),
4518-
reqs->second.end(),
4519-
req));
4520-
}
4540+
unsatisfiedReqs.erase(std::find(unsatisfiedReqs.begin(),
4541+
unsatisfiedReqs.end(),
4542+
req));
45214543
}
45224544
}
45234545

45244546
// For any unsatified optional @objc requirements that remain
45254547
// unsatisfied, note them in the AST for @objc selector collision
45264548
// checking.
4527-
for (const auto &unsatisfied : unsatisfiedReqs) {
4528-
for (auto req : unsatisfied.second) {
4529-
// Skip non-@objc requirements.
4530-
if (!req->isObjC()) continue;
4549+
for (auto req : unsatisfiedReqs) {
4550+
// Skip non-@objc requirements.
4551+
if (!req->isObjC()) continue;
45314552

4532-
// Skip unavailable requirements.
4533-
if (req->getAttrs().isUnavailable(Context)) continue;
4553+
// Skip unavailable requirements.
4554+
if (req->getAttrs().isUnavailable(Context)) continue;
45344555

4535-
// Record this requirement.
4536-
if (auto funcReq = dyn_cast<AbstractFunctionDecl>(req)) {
4537-
Context.recordObjCUnsatisfiedOptReq(dc, funcReq);
4538-
} else {
4539-
auto storageReq = cast<AbstractStorageDecl>(req);
4540-
if (auto getter = storageReq->getGetter())
4541-
Context.recordObjCUnsatisfiedOptReq(dc, getter);
4542-
if (auto setter = storageReq->getSetter())
4543-
Context.recordObjCUnsatisfiedOptReq(dc, setter);
4544-
}
4556+
// Record this requirement.
4557+
if (auto funcReq = dyn_cast<AbstractFunctionDecl>(req)) {
4558+
Context.recordObjCUnsatisfiedOptReq(dc, funcReq);
4559+
} else {
4560+
auto storageReq = cast<AbstractStorageDecl>(req);
4561+
if (auto getter = storageReq->getGetter())
4562+
Context.recordObjCUnsatisfiedOptReq(dc, getter);
4563+
if (auto setter = storageReq->getSetter())
4564+
Context.recordObjCUnsatisfiedOptReq(dc, setter);
45454565
}
45464566
}
45474567
}

test/decl/protocol/conforms/near_miss_objc.swift

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ class C1e : P1 {
4141
@nonobjc func doSomething(a: Int, c: Double) { } // don't warn
4242
}
4343

44-
// Don't try to match an
45-
44+
// Don't try to match an optional requirement against a declaration
45+
// that already satisfies one witness.
4646
@objc protocol P2 {
4747
optional func doSomething(a: Int, b: Double)
4848
optional func doSomething(a: Int, d: Double)
@@ -52,3 +52,27 @@ class C2a : P2 {
5252
@objc func doSomething(a: Int, b: Double) { } // don't warn: this matches something
5353
}
5454

55+
// Cope with base names that change.
56+
@objc protocol P3 {
57+
optional func doSomethingWithPriority(_ a: Int, d: Double) // expected-note{{requirement 'doSomethingWithPriority(_:d:)' declared here}}
58+
}
59+
60+
class C3a : P3 {
61+
func doSomething(priority: Int, d: Double) { }
62+
// expected-warning@-1{{instance method 'doSomething(priority:d:)' nearly matches optional requirement 'doSomethingWithPriority(_:d:)' of protocol 'P3'}}
63+
// expected-note@-2{{rename to 'doSomethingWithPriority(_:d:)' to satisfy this requirement}}{{20-20=_ }}
64+
// expected-note@-3{{move 'doSomething(priority:d:)' to an extension to silence this warning}}
65+
// expected-note@-4{{make 'doSomething(priority:d:)' private to silence this warning}}{{3-3=private }}
66+
}
67+
68+
@objc protocol P4 {
69+
optional func doSomething(priority: Int, d: Double) // expected-note{{requirement 'doSomething(priority:d:)' declared here}}
70+
}
71+
72+
class C4a : P4 {
73+
func doSomethingWithPriority(_ a: Int, d: Double) { }
74+
// expected-warning@-1{{instance method 'doSomethingWithPriority(_:d:)' nearly matches optional requirement 'doSomething(priority:d:)' of protocol 'P4'}}
75+
// expected-note@-2{{rename to 'doSomething(priority:d:)' to satisfy this requirement}}{{32-33=priority}}
76+
// expected-note@-3{{move 'doSomethingWithPriority(_:d:)' to an extension to silence this warning}}
77+
// expected-note@-4{{make 'doSomethingWithPriority(_:d:)' private to silence this warning}}{{3-3=private }}
78+
}

0 commit comments

Comments
 (0)