Skip to content

Commit 78127ce

Browse files
committed
Diagnose possible enum common-prefix mistakes
Clang Importer strips prefixes from enum and option set case names. The logic to do this computes a common prefix from the type name and all non-deprecated case names (to oversimplify), which means that adding, removing, or changing one case can change the prefix that is removed from *all* cases. This typically causes the prefix to become shorter, meaning that additional words are prepended to each existing case name. Existing diagnostics make it look like the case has disappeared, when in fact it still exists under a different name. A little more information may help developers to figure out what happened. Add a tailored diagnostic for this scenario which kicks in when (a) a missing member is diagnosed, (b) the base is an imported enum or option set’s metatype, and (c) an enum case or static property exists which has the name we attempted to look up as a suffix. Fixes rdar://116251319.
1 parent e23f26a commit 78127ce

File tree

7 files changed

+203
-1
lines changed

7 files changed

+203
-1
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ ERROR(could_not_find_enum_case,none,
104104
"enum type %0 has no case %1; did you mean %2?",
105105
(Type, DeclNameRef, DeclName))
106106

107+
ERROR(could_not_find_imported_enum_case,none,
108+
"type %0 has no case %1, but it does have a case named %2; if %1 worked "
109+
"before, a recent change to the underlying C enum may have affected how "
110+
"prefixes are stripped from its case names",
111+
(Type, DeclNameRef, ValueDecl *))
112+
107113
NOTE(did_you_mean_raw_type,none,
108114
"did you mean to specify a raw type on the enum declaration?", ())
109115

@@ -1712,6 +1718,9 @@ NOTE(fixit_add_private_for_objc_implementation,none,
17121718
NOTE(fixit_add_final_for_objc_implementation,none,
17131719
"add 'final' to define a Swift %0 that cannot be overridden",
17141720
(DescriptiveDeclKind))
1721+
NOTE(fixit_add_nonobjc_for_objc_implementation,none,
1722+
"add '@nonobjc' to define a Swift-only %0",
1723+
(DescriptiveDeclKind))
17151724

17161725
ERROR(objc_implementation_wrong_category,none,
17171726
"%kind0 should be implemented in extension for "

include/swift/Basic/StringExtras.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ namespace swift {
223223

224224
reverse_iterator rbegin() const { return reverse_iterator(end()); }
225225
reverse_iterator rend() const { return reverse_iterator(begin()); }
226+
227+
bool hasWordStartingAt(unsigned targetPosition) const;
226228
};
227229

228230
/// Retrieve the camelCase words in the given string.
@@ -236,6 +238,10 @@ namespace swift {
236238
/// case of the first letter.
237239
bool startsWithIgnoreFirstCase(StringRef word1, StringRef word2);
238240

241+
/// Check whether the first word ends with the second word, ignoring the
242+
/// case of the first word (handles initialisms).
243+
bool hasWordSuffix(StringRef haystack, StringRef needle);
244+
239245
/// Lowercase the first word within the given camelCase string.
240246
///
241247
/// \param string The string to lowercase.

lib/Basic/StringExtras.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,17 @@ void WordIterator::computePrevPosition() const {
215215
PrevPositionValid = true;
216216
}
217217

218+
bool camel_case::Words::hasWordStartingAt(unsigned targetPosition) const {
219+
// Iterate over the words until we see one at or past targetPosition.
220+
// FIXME: Is there a faster way to do this by looking at the characters around
221+
// the position?
222+
for (auto i = begin(); i != end() && i.getPosition() <= targetPosition; i++) {
223+
if (i.getPosition() == targetPosition)
224+
return true;
225+
}
226+
return false;
227+
}
228+
218229
StringRef camel_case::getFirstWord(StringRef string) {
219230
if (string.empty())
220231
return "";
@@ -249,6 +260,30 @@ bool camel_case::startsWithIgnoreFirstCase(StringRef word1, StringRef word2) {
249260
return word1.substr(1, word2.size() - 1) == word2.substr(1);
250261
}
251262

263+
bool camel_case::hasWordSuffix(StringRef haystack, StringRef needle) {
264+
// Is it even possible for one to be a suffix of the other?
265+
if (needle.empty() || haystack.size() <= needle.size())
266+
return false;
267+
268+
// Does haystack have a word boundary at the right position?
269+
auto targetPosition = haystack.size() - needle.size();
270+
if (!Words(haystack).hasWordStartingAt(targetPosition))
271+
return false;
272+
273+
StringRef suffix = haystack.substr(targetPosition);
274+
275+
// Fast path: Without potentially copying the strings, do they match?
276+
if (sameWordIgnoreFirstCase(suffix, needle))
277+
return true;
278+
279+
// Flatten out leading initialisms. Do they match?
280+
SmallString<32> suffixScratch, needleScratch;
281+
auto suffixFlat = toLowercaseInitialisms(suffix, suffixScratch);
282+
auto needleFlat = toLowercaseInitialisms(needle, needleScratch);
283+
284+
return suffixFlat == needleFlat;
285+
}
286+
252287
StringRef camel_case::toLowercaseWord(StringRef string,
253288
SmallVectorImpl<char> &scratch) {
254289
if (string.empty())

lib/Sema/CSDiagnostics.cpp

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4125,6 +4125,84 @@ DeclName MissingMemberFailure::findCorrectEnumCaseName(
41254125
return (candidate ? candidate->getName() : DeclName());
41264126
}
41274127

4128+
/// If \p instanceTy is an imported Clang enum, find the best imported case (if
4129+
/// any) which has \p name as a (capitalization-adjusted) suffix.
4130+
ValueDecl *MissingMemberFailure::
4131+
findImportedCaseWithMatchingSuffix(Type instanceTy, DeclNameRef name) {
4132+
IterableDeclContext *idc = nullptr;
4133+
4134+
if (auto ED = instanceTy->getEnumOrBoundGenericEnum()) {
4135+
idc = ED;
4136+
}
4137+
else if (auto SD = instanceTy->getStructOrBoundGenericStruct()) {
4138+
// Did ClangImporter add OptionSet to this struct?
4139+
for (auto protoAttr :
4140+
SD->getAttrs().getAttributes<SynthesizedProtocolAttr>()) {
4141+
auto knownProto = protoAttr->getProtocol()->getKnownProtocolKind();
4142+
if (knownProto == KnownProtocolKind::OptionSet) {
4143+
idc = SD;
4144+
break;
4145+
}
4146+
}
4147+
}
4148+
4149+
if (!idc || !idc->getDecl()->hasClangNode())
4150+
// Not an imported clang enum.
4151+
return nullptr;
4152+
4153+
auto betterMatch = [](ValueDecl *a, ValueDecl *b) -> ValueDecl * {
4154+
#define WORSE(BAD_CONDITION) do { \
4155+
auto aBad = a BAD_CONDITION; \
4156+
auto bBad = b BAD_CONDITION; \
4157+
if (aBad > bBad) \
4158+
return b; \
4159+
if (aBad < bBad) \
4160+
return a; \
4161+
} while (false)
4162+
4163+
// Is one null? Return the other.
4164+
WORSE(== nullptr);
4165+
4166+
assert((a && b) && "neither should be null here");
4167+
ASTContext &ctx = a->getASTContext();
4168+
4169+
// Is one more available than the other?
4170+
WORSE(->getAttrs().isUnavailable(ctx));
4171+
WORSE(->getAttrs().getDeprecated(ctx));
4172+
4173+
// Does one have a shorter name (so the non-matching prefix is shorter)?
4174+
WORSE(->getName().getBaseName().userFacingName().size());
4175+
#undef WORSE
4176+
4177+
// Arbitrary tiebreaker: compare the names.
4178+
if (a->getName().compare(b->getName()) > 0)
4179+
return b;
4180+
return a;
4181+
};
4182+
4183+
StringRef needle = name.getBaseName().userFacingName();
4184+
4185+
ValueDecl *bestMatch = nullptr;
4186+
for (auto member : idc->getMembers()) {
4187+
auto VD = dyn_cast<ValueDecl>(member);
4188+
if (!VD)
4189+
continue;
4190+
if (!(isa<EnumElementDecl>(VD) || isa<VarDecl>(VD)))
4191+
continue;
4192+
if (VD->isInstanceMember())
4193+
continue;
4194+
if (!VD->getInterfaceType()->isEqual(instanceTy))
4195+
continue;
4196+
if (!camel_case::hasWordSuffix(VD->getName().getBaseName().userFacingName(),
4197+
needle))
4198+
continue;
4199+
4200+
// This member matches. Is it the best match?
4201+
bestMatch = betterMatch(bestMatch, VD);
4202+
}
4203+
return bestMatch;
4204+
}
4205+
41284206
bool MissingMemberFailure::diagnoseAsError() {
41294207
auto anchor = getRawAnchor();
41304208
auto memberBase = getAnchor();
@@ -4243,7 +4321,16 @@ bool MissingMemberFailure::diagnoseAsError() {
42434321
} else {
42444322
emitBasicError(baseType);
42454323
}
4246-
} else {
4324+
} else if (ValueDecl *bestMatch =
4325+
findImportedCaseWithMatchingSuffix(instanceTy, getName())) {
4326+
// Sometimes Clang Importer's case prefix stripping unexpectedly
4327+
// changes the name of unrelated cases when someone adds a new case or
4328+
// changes the deprecation of an existing case. If this is such an
4329+
// imported type and we can find a case which has `getName()` as a
4330+
// suffix, mention this possibility.
4331+
emitDiagnostic(diag::could_not_find_imported_enum_case, instanceTy,
4332+
getName(), bestMatch);
4333+
} else {
42474334
emitBasicError(baseType);
42484335
}
42494336
} else if (auto moduleTy = baseType->getAs<ModuleType>()) {

lib/Sema/CSDiagnostics.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,6 +1276,9 @@ class MissingMemberFailure : public InvalidMemberRefFailure {
12761276
static DeclName findCorrectEnumCaseName(Type Ty,
12771277
TypoCorrectionResults &corrections,
12781278
DeclNameRef memberName);
1279+
1280+
static ValueDecl *findImportedCaseWithMatchingSuffix(Type instanceTy,
1281+
DeclNameRef name);
12791282
};
12801283

12811284
class UnintendedExtraGenericParamMemberFailure final

test/ClangImporter/enum.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,27 @@ _ = NSPrefixWordBreakReordered2Custom.problemCase == .goodCase
146146
_ = NSPrefixWordBreakReordered2Custom.problemCase == .PrefixWordBreakReordered2DeprecatedBadCase // expected-warning {{deprecated}}
147147
_ = NSPrefixWordBreakReordered2Custom.problemCase == .deprecatedGoodCase // expected-warning {{deprecated}}
148148

149+
#if !IRGEN
150+
_ = NSPrefixWordBreakForgotToDeprecate.newName1 // expected-error {{type 'NSPrefixWordBreakForgotToDeprecate' has no case 'newName1', but it does have a case named 'PrefixWordBreakNewName1'; if 'newName1' worked before, a recent change to the underlying C enum may have affected how prefixes are stripped from its case names}}
151+
_ = NSPrefixWordBreakForgotToDeprecate.newName2 // expected-error {{type 'NSPrefixWordBreakForgotToDeprecate' has no case 'newName2', but it does have a case named 'PrefixWordBreakNewName2'; if 'newName2' worked before, a recent change to the underlying C enum may have affected how prefixes are stripped from its case names}}
152+
_ = NSPrefixWordBreakForgotToDeprecate.noMatches // expected-error {{type 'NSPrefixWordBreakForgotToDeprecate' has no member 'noMatches'}}
153+
154+
_ = NSPrefixWordBreakInvalidWord.foo // expected-error {{type 'NSPrefixWordBreakInvalidWord' has no case 'foo', but it does have a case named 'BreakFoo'; if 'foo' worked before, a recent change to the underlying C enum may have affected how prefixes are stripped from its case names}}
155+
_ = NSPrefixWordBreakInvalidWord.bar // expected-error {{type 'NSPrefixWordBreakInvalidWord' has no case 'bar', but it does have a case named 'BreakBar'; if 'bar' worked before, a recent change to the underlying C enum may have affected how prefixes are stripped from its case names}}
156+
_ = NSPrefixWordBreakInvalidWord.noMatches // expected-error {{type 'NSPrefixWordBreakInvalidWord' has no member 'noMatches'}}
157+
158+
_ = NSPrefixWordBreakSuffixTieBreakers.forTest1 // expected-error {{Better}}
159+
_ = NSPrefixWordBreakSuffixTieBreakers.forTest2 // expected-error {{Better}}
160+
_ = NSPrefixWordBreakSuffixTieBreakers.forTest3 // expected-error {{Better}}
161+
_ = NSPrefixWordBreakSuffixTieBreakers.forTest4 // expected-error {{Better}}
162+
_ = NSPrefixWordBreakSuffixTieBreakers.forTest5 // expected-error {{Better}}
163+
164+
_ = [
165+
.newOption1, // expected-error {{type 'NSPrefixWordBreakOptions.ArrayLiteralElement' (aka 'NSPrefixWordBreakOptions') has no case 'newOption1', but it does have a case named 'prefixWordBreakNewOption1'; if 'newOption1' worked before, a recent change to the underlying C enum may have affected how prefixes are stripped from its case names}}
166+
.newOption2, // expected-error {{type 'NSPrefixWordBreakOptions.ArrayLiteralElement' (aka 'NSPrefixWordBreakOptions') has no case 'newOption2', but it does have a case named 'prefixWordBreakNewOption2'; if 'newOption2' worked before, a recent change to the underlying C enum may have affected how prefixes are stripped from its case names}}
167+
] as NSPrefixWordBreakOptions
168+
#endif
169+
149170
_ = NSSwiftNameAllTheThings.Foo == .Bar
150171
_ = NSSwiftNameBad.`class`
151172

test/Inputs/clang-importer-sdk/usr/include/Foundation.h

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,47 @@ typedef NS_ENUM(NSInteger, NSPrefixWordBreakReordered2Custom) {
387387
NSPrefixWordBreakReordered2DeprecatedGoodCase __attribute__((deprecated)),
388388
};
389389

390+
typedef NS_ENUM(NSInteger, NSPrefixWordBreakForgotToDeprecate) {
391+
NSPrefixWordBreakNewName1,
392+
NSPrefixWordBreakNewName2,
393+
NSOldName1PrefixWordBreak = NSPrefixWordBreakNewName1, // should have been deprecated
394+
};
395+
396+
typedef NS_ENUM(NSInteger, NSPrefixWordBreakInvalidWord) {
397+
NSPrefixWordBreakFoo,
398+
NSPrefixWordBreakBar,
399+
NSPrefixWordBreak42, // expected prefix would have left an invalid identifier
400+
};
401+
402+
// Check tiebreaking rules. In each case, the diagnostic should choose the 'Better' case over the 'Worse' case.
403+
typedef NS_ENUM(NSInteger, NSPrefixWordBreakSuffixTieBreakers) {
404+
// Available preferred over unavailable.
405+
NSPrefixWordBreakBetterForTest1,
406+
NSPrefixWordBreakWorseForTest1 __attribute__((unavailable)),
407+
408+
// Un-deprecated preferred over deprecated.
409+
NSPrefixWordBreakBetterForTest2,
410+
NSPrefixWordBreakWorseForTest2 __attribute__((deprecated)),
411+
412+
// Shorter preferred over longer (it's a closer match).
413+
NSPrefixWordBreakBetterForTest3,
414+
NSPrefixWordBreakWorseXXXForTest3,
415+
416+
// Alphabetically first preferred over second (arbitrary tiebreaker).
417+
NSPrefixWordBreakBetterForTest4,
418+
NSPrefixWordBreakWorseXForTest4,
419+
420+
// Make sure we're not choosing based on ordering.
421+
NSPrefixWordBreakWorseXForTest5,
422+
NSPrefixWordBreakBetterForTest5,
423+
};
424+
425+
typedef NS_OPTIONS(NSInteger, NSPrefixWordBreakOptions) {
426+
NSPrefixWordBreakNewOption1 = 0x1,
427+
NSPrefixWordBreakNewOption2 = 0x2,
428+
NSOldOption1PrefixWordBreak = NSPrefixWordBreakNewOption1, // should have been deprecated
429+
};
430+
390431
typedef NS_ENUM(NSInteger, NSSwiftNameAllTheThings) {
391432
NSSwiftNameAllTheThingsA __attribute__((swift_name("Foo"))),
392433
NSSwiftNameAllTheThingsB __attribute__((swift_name("Bar"))),

0 commit comments

Comments
 (0)