Skip to content

Commit c1df6db

Browse files
authored
Merge pull request #68881 from apple/prefix-smashing
Diagnose possible enum common-prefix mistakes
2 parents 26d65b5 + 78127ce commit c1df6db

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

@@ -1728,6 +1734,9 @@ NOTE(fixit_add_private_for_objc_implementation,none,
17281734
NOTE(fixit_add_final_for_objc_implementation,none,
17291735
"add 'final' to define a Swift %0 that cannot be overridden",
17301736
(DescriptiveDeclKind))
1737+
NOTE(fixit_add_nonobjc_for_objc_implementation,none,
1738+
"add '@nonobjc' to define a Swift-only %0",
1739+
(DescriptiveDeclKind))
17311740

17321741
ERROR(objc_implementation_wrong_category,none,
17331742
"%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
@@ -4136,6 +4136,84 @@ DeclName MissingMemberFailure::findCorrectEnumCaseName(
41364136
return (candidate ? candidate->getName() : DeclName());
41374137
}
41384138

4139+
/// If \p instanceTy is an imported Clang enum, find the best imported case (if
4140+
/// any) which has \p name as a (capitalization-adjusted) suffix.
4141+
ValueDecl *MissingMemberFailure::
4142+
findImportedCaseWithMatchingSuffix(Type instanceTy, DeclNameRef name) {
4143+
IterableDeclContext *idc = nullptr;
4144+
4145+
if (auto ED = instanceTy->getEnumOrBoundGenericEnum()) {
4146+
idc = ED;
4147+
}
4148+
else if (auto SD = instanceTy->getStructOrBoundGenericStruct()) {
4149+
// Did ClangImporter add OptionSet to this struct?
4150+
for (auto protoAttr :
4151+
SD->getAttrs().getAttributes<SynthesizedProtocolAttr>()) {
4152+
auto knownProto = protoAttr->getProtocol()->getKnownProtocolKind();
4153+
if (knownProto == KnownProtocolKind::OptionSet) {
4154+
idc = SD;
4155+
break;
4156+
}
4157+
}
4158+
}
4159+
4160+
if (!idc || !idc->getDecl()->hasClangNode())
4161+
// Not an imported clang enum.
4162+
return nullptr;
4163+
4164+
auto betterMatch = [](ValueDecl *a, ValueDecl *b) -> ValueDecl * {
4165+
#define WORSE(BAD_CONDITION) do { \
4166+
auto aBad = a BAD_CONDITION; \
4167+
auto bBad = b BAD_CONDITION; \
4168+
if (aBad > bBad) \
4169+
return b; \
4170+
if (aBad < bBad) \
4171+
return a; \
4172+
} while (false)
4173+
4174+
// Is one null? Return the other.
4175+
WORSE(== nullptr);
4176+
4177+
assert((a && b) && "neither should be null here");
4178+
ASTContext &ctx = a->getASTContext();
4179+
4180+
// Is one more available than the other?
4181+
WORSE(->getAttrs().isUnavailable(ctx));
4182+
WORSE(->getAttrs().getDeprecated(ctx));
4183+
4184+
// Does one have a shorter name (so the non-matching prefix is shorter)?
4185+
WORSE(->getName().getBaseName().userFacingName().size());
4186+
#undef WORSE
4187+
4188+
// Arbitrary tiebreaker: compare the names.
4189+
if (a->getName().compare(b->getName()) > 0)
4190+
return b;
4191+
return a;
4192+
};
4193+
4194+
StringRef needle = name.getBaseName().userFacingName();
4195+
4196+
ValueDecl *bestMatch = nullptr;
4197+
for (auto member : idc->getMembers()) {
4198+
auto VD = dyn_cast<ValueDecl>(member);
4199+
if (!VD)
4200+
continue;
4201+
if (!(isa<EnumElementDecl>(VD) || isa<VarDecl>(VD)))
4202+
continue;
4203+
if (VD->isInstanceMember())
4204+
continue;
4205+
if (!VD->getInterfaceType()->isEqual(instanceTy))
4206+
continue;
4207+
if (!camel_case::hasWordSuffix(VD->getName().getBaseName().userFacingName(),
4208+
needle))
4209+
continue;
4210+
4211+
// This member matches. Is it the best match?
4212+
bestMatch = betterMatch(bestMatch, VD);
4213+
}
4214+
return bestMatch;
4215+
}
4216+
41394217
bool MissingMemberFailure::diagnoseAsError() {
41404218
auto anchor = getRawAnchor();
41414219
auto memberBase = getAnchor();
@@ -4254,7 +4332,16 @@ bool MissingMemberFailure::diagnoseAsError() {
42544332
} else {
42554333
emitBasicError(baseType);
42564334
}
4257-
} else {
4335+
} else if (ValueDecl *bestMatch =
4336+
findImportedCaseWithMatchingSuffix(instanceTy, getName())) {
4337+
// Sometimes Clang Importer's case prefix stripping unexpectedly
4338+
// changes the name of unrelated cases when someone adds a new case or
4339+
// changes the deprecation of an existing case. If this is such an
4340+
// imported type and we can find a case which has `getName()` as a
4341+
// suffix, mention this possibility.
4342+
emitDiagnostic(diag::could_not_find_imported_enum_case, instanceTy,
4343+
getName(), bestMatch);
4344+
} else {
42584345
emitBasicError(baseType);
42594346
}
42604347
} 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)