Skip to content

[Sema] Add dedicated fix-it for NSObject.hashValue overrides #22081

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions include/swift/AST/DiagnosticsSema.def
Original file line number Diff line number Diff line change
Expand Up @@ -4245,9 +4245,13 @@ WARNING(non_exhaustive_switch_unknown_only,none,
"switch covers known cases, but %0 may have additional unknown values"
"%select{|, possibly added in future versions}1", (Type, bool))

WARNING(override_nsobject_hashvalue,none,
WARNING(override_nsobject_hashvalue_warning,none,
"override of 'NSObject.hashValue' is deprecated; "
"override 'NSObject.hash' to get consistent hashing behavior", ())
"did you mean to override 'NSObject.hash'?", ())
ERROR(override_nsobject_hashvalue_error,none,
"'NSObject.hashValue' is not overridable; "
"did you mean to override 'NSObject.hash'?", ())

WARNING(hashvalue_implementation,none,
"'Hashable.hashValue' is deprecated as a protocol requirement; "
"conform type %0 to 'Hashable' by implementing 'hash(into:)' instead",
Expand Down
50 changes: 38 additions & 12 deletions lib/Sema/TypeCheckDeclOverride.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,22 @@ static bool parameterTypesMatch(const ValueDecl *derivedDecl,
return true;
}

/// Returns true if the given declaration is for the `NSObject.hashValue`
/// property.
static bool isNSObjectHashValue(ValueDecl *baseDecl) {
ASTContext &ctx = baseDecl->getASTContext();

if (auto baseVar = dyn_cast<VarDecl>(baseDecl)) {
if (auto classDecl = baseVar->getDeclContext()->getSelfClassDecl()) {
return baseVar->getName() == ctx.Id_hashValue &&
classDecl->getName().is("NSObject") &&
(classDecl->getModuleContext()->getName() == ctx.Id_Foundation ||
classDecl->getModuleContext()->getName() == ctx.Id_ObjectiveC);
}
}
return false;
}

namespace {
/// Class that handles the checking of a particular declaration against
/// superclass entities that it could override.
Expand Down Expand Up @@ -786,9 +802,15 @@ static void checkOverrideAccessControl(ValueDecl *baseDecl, ValueDecl *decl,
baseDecl->getModuleContext() != decl->getModuleContext() &&
!isa<ConstructorDecl>(decl) &&
!isa<ProtocolDecl>(decl->getDeclContext())) {
diags.diagnose(decl, diag::override_of_non_open,
decl->getDescriptiveKind());

// NSObject.hashValue was made non-overridable in Swift 5; one should
// override NSObject.hash instead.
if (isNSObjectHashValue(baseDecl)) {
diags.diagnose(decl, diag::override_nsobject_hashvalue_error)
.fixItReplace(SourceRange(decl->getNameLoc()), "hash");
} else {
diags.diagnose(decl, diag::override_of_non_open,
decl->getDescriptiveKind());
}
} else if (baseHasOpenAccess &&
classDecl->hasOpenAccess(dc) &&
decl->getFormalAccess() != AccessLevel::Open &&
Expand Down Expand Up @@ -1526,6 +1548,13 @@ static bool checkSingleOverride(ValueDecl *override, ValueDecl *base) {
(isa<ExtensionDecl>(base->getDeclContext()) ||
isa<ExtensionDecl>(override->getDeclContext())) &&
!base->isObjC()) {
// Suppress this diagnostic for overrides of a non-open NSObject.hashValue
// property; these are diagnosed elsewhere. An error message complaining
// about extensions would be misleading in this case; the correct fix is to
// override NSObject.hash instead.
if (isNSObjectHashValue(base) &&
!base->hasOpenAccess(override->getDeclContext()))
return true;
bool baseCanBeObjC = canBeRepresentedInObjC(base);
diags.diagnose(override, diag::override_decl_extension, baseCanBeObjC,
!isa<ExtensionDecl>(base->getDeclContext()));
Expand Down Expand Up @@ -1626,15 +1655,12 @@ static bool checkSingleOverride(ValueDecl *override, ValueDecl *base) {

// Overrides of NSObject.hashValue are deprecated; one should override
// NSObject.hash instead.
if (auto baseVar = dyn_cast<VarDecl>(base)) {
if (auto classDecl = baseVar->getDeclContext()->getSelfClassDecl()) {
if (baseVar->getName() == ctx.Id_hashValue &&
classDecl->getName().is("NSObject") &&
(classDecl->getModuleContext()->getName() == ctx.Id_Foundation ||
classDecl->getModuleContext()->getName() == ctx.Id_ObjectiveC)) {
override->diagnose(diag::override_nsobject_hashvalue);
}
}
// FIXME: Remove this when NSObject.hashValue becomes non-open in
// swift-corelibs-foundation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably it's correct to warn even before the corelibs-foundation change, yeah?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! We've had this warning on all platforms since 4.2. But I see no reason to keep it once NSObject.hashValue becomes non-open everywhere; it would never trigger in practice.

if (isNSObjectHashValue(base) &&
base->hasOpenAccess(override->getDeclContext())) {
override->diagnose(diag::override_nsobject_hashvalue_warning)
.fixItReplace(SourceRange(override->getNameLoc()), "hash");
}

/// Check attributes associated with the base; some may need to merged with
Expand Down
3 changes: 2 additions & 1 deletion test/ClangImporter/objc_override.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ class CallbackSubC : CallbackBase {

//
class MyHashableNSObject: NSObject {
override var hashValue: Int { // expected-error{{overriding non-open property outside of its defining module}} expected-error{{overriding non-@objc declarations from extensions is not supported}}
override var hashValue: Int {
// expected-error@-1 {{'NSObject.hashValue' is not overridable; did you mean to override 'NSObject.hash'?}}
return 0
}
}
Expand Down
14 changes: 14 additions & 0 deletions test/Migrator/fixit-NSObject-hashValue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// RUN: %empty-directory(%t)
// RUN: not %target-swift-frontend -typecheck -update-code -primary-file %s -emit-migrated-file-path %t/fixit-NSObject-hashValue.result
// RUN: diff -u %s.expected %t/fixit-NSObject-hashValue.result
// RUN: %target-swift-frontend -typecheck %s.expected

// REQUIRES: objc_interop

import ObjectiveC

class MyClass: NSObject {
override var hashValue: Int {
return 42
}
}
14 changes: 14 additions & 0 deletions test/Migrator/fixit-NSObject-hashValue.swift.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// RUN: %empty-directory(%t)
// RUN: not %target-swift-frontend -typecheck -update-code -primary-file %s -emit-migrated-file-path %t/fixit-NSObject-hashValue.result
// RUN: diff -u %s.expected %t/fixit-NSObject-hashValue.result
// RUN: %target-swift-frontend -typecheck %s.expected

// REQUIRES: objc_interop

import ObjectiveC

class MyClass: NSObject {
override var hash: Int {
return 42
}
}
7 changes: 5 additions & 2 deletions test/stdlib/NSObject-hashing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
import ObjectiveC

class Foo: NSObject {
override var hashValue: Int { // expected-error {{overriding non-open property outside of its defining module}} expected-error {{overriding non-@objc declarations from extensions is not supported}}
override var hashValue: Int {
// expected-error@-1 {{'NSObject.hashValue' is not overridable; did you mean to override 'NSObject.hash'?}}
return 0
}

override func hash(into hasher: inout Hasher) { // expected-error {{overriding non-open instance method outside of its defining module}} expected-error {{overriding declarations in extensions is not supported}}
override func hash(into hasher: inout Hasher) {
// expected-error@-1 {{overriding non-open instance method outside of its defining module}}
// expected-error@-2 {{overriding declarations in extensions is not supported}}
}
}