Skip to content

Sema: Relax availability checking for SPI and unavailable API decls #58707

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
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
74 changes: 42 additions & 32 deletions lib/Sema/TypeCheckAvailability.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ bool swift::isExported(const ValueDecl *VD) {
return false;
}

static bool hasConformancesToPublicProtocols(const ExtensionDecl *ED) {
auto protocols = ED->getLocalProtocols(ConformanceLookupKind::OnlyExplicit);
for (const ProtocolDecl *PD : protocols) {
AccessScope scope =
PD->getFormalAccessScope(/*useDC*/ nullptr,
/*treatUsableFromInlineAsPublic*/ true);
if (scope.isPublic())
return true;
}

return false;
}

bool swift::isExported(const ExtensionDecl *ED) {
// An extension can only be exported if it extends an exported type.
if (auto *NTD = ED->getExtendedNominal()) {
Expand All @@ -94,14 +107,8 @@ bool swift::isExported(const ExtensionDecl *ED) {

// If the extension declares a conformance to a public protocol then the
// extension is exported.
auto protocols = ED->getLocalProtocols(ConformanceLookupKind::OnlyExplicit);
for (const ProtocolDecl *PD : protocols) {
AccessScope scope =
PD->getFormalAccessScope(/*useDC*/nullptr,
/*treatUsableFromInlineAsPublic*/true);
if (scope.isPublic())
return true;
}
if (hasConformancesToPublicProtocols(ED))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was opportunistic refactoring; I noticed duplicated logic.

return true;

return false;
}
Expand Down Expand Up @@ -313,7 +320,7 @@ static bool hasActiveAvailableAttribute(Decl *D,
return getActiveAvailableAttribute(D, AC);
}

static bool bodyIsResilienceBoundary(Decl *D) {
static bool shouldConstrainBodyToDeploymentTarget(Decl *D) {
// The declaration contains code...
if (auto afd = dyn_cast<AbstractFunctionDecl>(D)) {
// And it has a location so we can check it...
Expand Down Expand Up @@ -526,9 +533,7 @@ class TypeRefinementContextBuilder : private ASTWalker {
AvailabilityContext DeclInfo = ExplicitDeclInfo;
DeclInfo.intersectWith(getCurrentTRC()->getAvailabilityInfo());

// If the entire declaration is surrounded by a resilience boundary, it is
// also constrained by the deployment target.
if (signatureIsResilienceBoundary(D))
if (shouldConstrainSignatureToDeploymentTarget(D))
DeclInfo.intersectWith(AvailabilityContext::forDeploymentTarget(Context));

SourceRange Range = refinementSourceRangeForDecl(D);
Expand Down Expand Up @@ -562,9 +567,10 @@ class TypeRefinementContextBuilder : private ASTWalker {
}

// No need to introduce a context if the declaration does not have an
// availability attribute and the signature is not a resilience boundary.
// availability attribute and the signature does not constrain availability
// to the deployment target.
if (!hasActiveAvailableAttribute(D, Context) &&
!signatureIsResilienceBoundary(D)) {
!shouldConstrainSignatureToDeploymentTarget(D)) {
return false;
}

Expand All @@ -581,12 +587,23 @@ class TypeRefinementContextBuilder : private ASTWalker {
return true;
}

/// A declaration's signature is a resilience boundary if the entire
/// declaration--not just the body--is not ABI-public and it's in a context
/// where ABI-public declarations would be available below the minimum
/// deployment target.
bool signatureIsResilienceBoundary(Decl *D) {
return !isCurrentTRCContainedByDeploymentTarget() && !::isExported(D);
/// Checks whether the entire declaration, including its signature, should be
/// constrained to the deployment target. Generally public API declarations
/// are not constrained since they appear in the interface of the module and
/// may be consumed by clients with lower deployment targets, but there are
/// some exceptions.
bool shouldConstrainSignatureToDeploymentTarget(Decl *D) {
if (isCurrentTRCContainedByDeploymentTarget())
return false;

// As a convenience, SPI decls and explicitly unavailable decls are
// constrained to the deployment target. There's not much benefit to
// checking these declarations at a lower availability version floor since
// neither can be used by API clients.
if (D->isSPI() || AvailableAttr::isUnavailable(D))
return true;

return !::isExported(D);
}

/// Returns the source range which should be refined by declaration. This
Expand Down Expand Up @@ -633,14 +650,14 @@ class TypeRefinementContextBuilder : private ASTWalker {
}

bool bodyIntroducesNewContext(Decl *D) {
// Are we already effectively in a resilience boundary? If not, adding one
// wouldn't change availability.
// Are we already constrained by the deployment target? If not, adding a
// new context wouldn't change availability.
if (isCurrentTRCContainedByDeploymentTarget())
return false;

// If we're in a function, is its body a resilience boundary?
// If we're in a function, check if it ought to use the deployment target.
if (auto afd = dyn_cast<AbstractFunctionDecl>(D))
return bodyIsResilienceBoundary(afd);
return shouldConstrainBodyToDeploymentTarget(afd);

// The only other case we care about is top-level code.
return isa<TopLevelCodeDecl>(D);
Expand Down Expand Up @@ -4089,14 +4106,7 @@ void swift::checkExplicitAvailability(Decl *decl) {
return false;
});

auto protocols = extension->getLocalProtocols(ConformanceLookupKind::OnlyExplicit);
auto hasProtocols = std::any_of(protocols.begin(), protocols.end(),
[](const ProtocolDecl *PD) -> bool {
AccessScope scope =
PD->getFormalAccessScope(/*useDC*/nullptr,
/*treatUsableFromInlineAsPublic*/true);
return scope.isPublic();
});
auto hasProtocols = hasConformancesToPublicProtocols(extension);

if (!hasMembers && !hasProtocols) return;

Expand Down
105 changes: 42 additions & 63 deletions test/attr/attr_inlinable_available.swift
Original file line number Diff line number Diff line change
Expand Up @@ -321,13 +321,12 @@ public func alwaysUnavailable(
}

@_spi(Private)
public func spiDeployedUseNoAvailable( // expected-note 5 {{add @available attribute}}
public func spiDeployedUseNoAvailable( // expected-note 3 {{add @available attribute}}
_: NoAvailable,
_: BeforeInliningTarget,
_: AtInliningTarget,
// FIXME: Next two should be accepted (SPI)
_: BetweenTargets, // expected-error {{'BetweenTargets' is only available in}}
_: AtDeploymentTarget, // expected-error {{'AtDeploymentTarget' is only available in}}
_: BetweenTargets,
_: AtDeploymentTarget,
_: AfterDeploymentTarget // expected-error {{'AfterDeploymentTarget' is only available in}}
) {
defer {
Expand Down Expand Up @@ -533,16 +532,14 @@ public func spiDeployedUseNoAvailable( // expected-note 5 {{add @available attri
_: Unavailable
) {
defer {
// FIXME: Should be allowed for compatibility (unavailable)
_ = AtDeploymentTarget() // expected-error {{'AtDeploymentTarget' is only available in}} expected-note {{add 'if #available'}}
_ = AtDeploymentTarget()
_ = AfterDeploymentTarget() // expected-error {{'AfterDeploymentTarget' is only available in}} expected-note {{add 'if #available'}}
}
_ = NoAvailable()
_ = BeforeInliningTarget()
_ = AtInliningTarget()
// FIXME: Next two should be accepted for compatibility (unavailable)
_ = BetweenTargets() // expected-error {{'BetweenTargets' is only available in}} expected-note {{add 'if #available'}}
_ = AtDeploymentTarget() // expected-error {{'AtDeploymentTarget' is only available in}} expected-note {{add 'if #available'}}
_ = BetweenTargets()
_ = AtDeploymentTarget()
_ = AfterDeploymentTarget() // expected-error {{'AfterDeploymentTarget' is only available in}} expected-note {{add 'if #available'}}
_ = Unavailable()

Expand All @@ -558,26 +555,23 @@ public func spiDeployedUseNoAvailable( // expected-note 5 {{add @available attri
}

@_spi(Private)
@inlinable public func spiInlinedUseNoAvailable( // expected-note 8 {{add @available attribute}}
@inlinable public func spiInlinedUseNoAvailable( // expected-note 3 {{add @available attribute}}
_: NoAvailable,
_: BeforeInliningTarget,
_: AtInliningTarget,
// FIXME: Next two should be accepted (SPI)
_: BetweenTargets, // expected-error {{'BetweenTargets' is only available in}}
_: AtDeploymentTarget, // expected-error {{'AtDeploymentTarget' is only available in}}
_: BetweenTargets,
_: AtDeploymentTarget,
_: AfterDeploymentTarget // expected-error {{'AfterDeploymentTarget' is only available in}}
) {
defer {
// FIXME: Should be allowed (SPI)
_ = AtDeploymentTarget() // expected-error {{'AtDeploymentTarget' is only available in}} expected-note {{add 'if #available'}}
_ = AtDeploymentTarget()
_ = AfterDeploymentTarget() // expected-error {{'AfterDeploymentTarget' is only available in}} expected-note {{add 'if #available'}}
}
_ = NoAvailable()
_ = BeforeInliningTarget()
_ = AtInliningTarget()
// FIXME: Next two should be accepted (SPI)
_ = BetweenTargets() // expected-error {{'BetweenTargets' is only available in}} expected-note {{add 'if #available'}}
_ = AtDeploymentTarget() // expected-error {{'AtDeploymentTarget' is only available in}} expected-note {{add 'if #available'}}
_ = BetweenTargets()
_ = AtDeploymentTarget()
_ = AfterDeploymentTarget() // expected-error {{'AfterDeploymentTarget' is only available in}} expected-note {{add 'if #available'}}

if #available(macOS 11, iOS 14, tvOS 14, watchOS 7, *) {
Expand Down Expand Up @@ -679,21 +673,19 @@ public func defaultArgsUseUnavailable(
_: Any = NoAvailable.self,
_: Any = BeforeInliningTarget.self,
_: Any = AtInliningTarget.self,
// FIXME: Next two should be accepted for compatibility (unavailable)
_: Any = BetweenTargets.self, // expected-error {{'BetweenTargets' is only available in}}
_: Any = AtDeploymentTarget.self, // expected-error {{'AtDeploymentTarget' is only available in}}
_: Any = BetweenTargets.self,
_: Any = AtDeploymentTarget.self,
_: Any = AfterDeploymentTarget.self, // expected-error {{'AfterDeploymentTarget' is only available in}}
_: Any = Unavailable.self
) {}

@_spi(Private)
public func spiDefaultArgsUseNoAvailable( // expected-note 3 {{add @available attribute}}
public func spiDefaultArgsUseNoAvailable( // expected-note 1 {{add @available attribute}}
_: Any = NoAvailable.self,
_: Any = BeforeInliningTarget.self,
_: Any = AtInliningTarget.self,
// FIXME: Next two should be accepted (SPI)
_: Any = BetweenTargets.self, // expected-error {{'BetweenTargets' is only available in}}
_: Any = AtDeploymentTarget.self, // expected-error {{'AtDeploymentTarget' is only available in}}
_: Any = BetweenTargets.self,
_: Any = AtDeploymentTarget.self,
_: Any = AfterDeploymentTarget.self // expected-error {{'AfterDeploymentTarget' is only available in}}
) {}

Expand Down Expand Up @@ -806,18 +798,16 @@ public struct UnavailablePublicStruct {
public var aPublic: NoAvailable
public var bPublic: BeforeInliningTarget
public var cPublic: AtInliningTarget
// FIXME: Next two should be accepted for compatibility (unavailable)
public var dPublic: BetweenTargets // expected-error {{'BetweenTargets' is only available in}}
public var ePublic: AtDeploymentTarget // expected-error {{'AtDeploymentTarget' is only available in}}
public var dPublic: BetweenTargets
public var ePublic: AtDeploymentTarget
public var fPublic: AfterDeploymentTarget // expected-error {{'AfterDeploymentTarget' is only available in}}
public var gPublic: Unavailable

public var aPublicInit: Any = NoAvailable()
public var bPublicInit: Any = BeforeInliningTarget()
public var cPublicInit: Any = AtInliningTarget()
// FIXME: The next two should not be accepted, the default initializer is not inlined
public var dPublicInit: Any = BetweenTargets() // expected-error {{'BetweenTargets' is only available in}}
public var ePublicInit: Any = AtDeploymentTarget() // expected-error {{'AtDeploymentTarget' is only available in}}
public var dPublicInit: Any = BetweenTargets()
public var ePublicInit: Any = AtDeploymentTarget()
public var fPublicInit: Any = AfterDeploymentTarget() // expected-error {{'AfterDeploymentTarget' is only available in}}
public var gPublicInit: Any = Unavailable()

Expand All @@ -831,21 +821,19 @@ public struct UnavailablePublicStruct {
}

@_spi(Private)
public struct SPIStruct { // expected-note 7 {{add @available attribute}}
public struct SPIStruct { // expected-note 3 {{add @available attribute}}
public var aPublic: NoAvailable
public var bPublic: BeforeInliningTarget
public var cPublic: AtInliningTarget
// FIXME: Next two should be accepted (SPI)
public var dPublic: BetweenTargets // expected-error {{'BetweenTargets' is only available in}}
public var ePublic: AtDeploymentTarget // expected-error {{'AtDeploymentTarget' is only available in}}
public var dPublic: BetweenTargets
public var ePublic: AtDeploymentTarget
public var fPublic: AfterDeploymentTarget // expected-error {{'AfterDeploymentTarget' is only available in}}

public var aPublicInit: Any = NoAvailable()
public var bPublicInit: Any = BeforeInliningTarget()
public var cPublicInit: Any = AtInliningTarget()
// FIXME: The next two should not be diagnosed, the initializers are not inlined
public var dPublicInit: Any = BetweenTargets() // expected-error {{'BetweenTargets' is only available in}}
public var ePublicInit: Any = AtDeploymentTarget() // expected-error {{'AtDeploymentTarget' is only available in}}
public var dPublicInit: Any = BetweenTargets()
public var ePublicInit: Any = AtDeploymentTarget()
public var fPublicInit: Any = AfterDeploymentTarget() // expected-error {{'AfterDeploymentTarget' is only available in}}

var aInternal: NoAvailable = .init()
Expand Down Expand Up @@ -917,15 +905,13 @@ extension BetweenTargets {
public func publicFunc3() {}
}

// FIXME: Should be accepted (SPI)
// expected-warning@+1 {{BetweenTargets' is only available in}} expected-note@+1 {{add @available attribute to enclosing extension}}
// FIXME: Can we prevent this warning when SPI members are the reason the extension is exported?
// expected-warning@+1 {{'BetweenTargets' is only available in}} expected-note@+1 {{add @available attribute to enclosing extension}}
extension BetweenTargets {
@_spi(Private)
public func spiFunc1() {}
}

// FIXME: Should be accepted (SPI)
// expected-warning@+2 {{BetweenTargets' is only available in}} expected-note@+2 {{add @available attribute to enclosing extension}}
@_spi(Private)
extension BetweenTargets {
internal func internalFunc3() {}
Expand All @@ -950,17 +936,14 @@ extension BetweenTargets {
) { }
}

// FIXME: Should be accepted (SPI)
// expected-warning@+2 {{BetweenTargets' is only available in}}
@_spi(Private)
extension BetweenTargets { // expected-note 4 {{add @available attribute to enclosing extension}}
public func inheritsSPINoAvailable( // expected-note 3 {{add @available attribute to enclosing instance method}}
extension BetweenTargets { // expected-note 1 {{add @available attribute to enclosing extension}}
public func inheritsSPINoAvailable( // expected-note 1 {{add @available attribute to enclosing instance method}}
_: NoAvailable,
_: BeforeInliningTarget,
_: AtInliningTarget,
// FIXME: Next two should be accepted (SPI)
_: BetweenTargets, // expected-error {{'BetweenTargets' is only available in}}
_: AtDeploymentTarget, // expected-error {{'AtDeploymentTarget' is only available in}}
_: BetweenTargets,
_: AtDeploymentTarget,
_: AfterDeploymentTarget // expected-error {{'AfterDeploymentTarget' is only available in}}
) { }
}
Expand Down Expand Up @@ -1081,21 +1064,19 @@ public protocol UnavailableProtoWithAssoc {
associatedtype A: NoAvailableProto
associatedtype B: BeforeInliningTargetProto
associatedtype C: AtInliningTargetProto
// FIXME: Next two should be accepted for compatibility (unavailable)
associatedtype D: BetweenTargetsProto // expected-error {{'BetweenTargetsProto' is only available in}}
associatedtype E: AtDeploymentTargetProto // expected-error {{'AtDeploymentTargetProto' is only available in}}
associatedtype D: BetweenTargetsProto
associatedtype E: AtDeploymentTargetProto
associatedtype F: AfterDeploymentTargetProto // expected-error {{'AfterDeploymentTargetProto' is only available in}}
associatedtype G: UnavailableProto
}

@_spi(Private)
public protocol SPINoAvailableProtoWithAssoc { // expected-note 3 {{add @available attribute to enclosing protocol}}
public protocol SPINoAvailableProtoWithAssoc { // expected-note 1 {{add @available attribute to enclosing protocol}}
associatedtype A: NoAvailableProto
associatedtype B: BeforeInliningTargetProto
associatedtype C: AtInliningTargetProto
// FIXME: Next two should be accepted (SPI)
associatedtype D: BetweenTargetsProto // expected-error {{'BetweenTargetsProto' is only available in}}
associatedtype E: AtDeploymentTargetProto // expected-error {{'AtDeploymentTargetProto' is only available in}}
associatedtype D: BetweenTargetsProto
associatedtype E: AtDeploymentTargetProto
associatedtype F: AfterDeploymentTargetProto // expected-error {{'AfterDeploymentTargetProto' is only available in}}
}

Expand All @@ -1118,21 +1099,19 @@ public enum UnavailableEnumWithTypeAliases {
public typealias A = NoAvailable
public typealias B = BeforeInliningTarget
public typealias C = AtInliningTarget
// FIXME: Next two should be accepted for compatibility (unavailable)
public typealias D = BetweenTargets // expected-error {{'BetweenTargets' is only available in}} expected-note {{add @available attribute to enclosing type alias}}
public typealias E = AtDeploymentTarget // expected-error {{'AtDeploymentTarget' is only available in}} expected-note {{add @available attribute to enclosing type alias}}
public typealias D = BetweenTargets
public typealias E = AtDeploymentTarget
public typealias F = AfterDeploymentTarget // expected-error {{'AfterDeploymentTarget' is only available in}} expected-note {{add @available attribute to enclosing type alias}}
public typealias G = Unavailable
}

@_spi(Private)
public enum SPIEnumWithTypeAliases { // expected-note 3 {{add @available attribute to enclosing enum}}
public enum SPIEnumWithTypeAliases { // expected-note 1 {{add @available attribute to enclosing enum}}
public typealias A = NoAvailable
public typealias B = BeforeInliningTarget
public typealias C = AtInliningTarget
// FIXME: Next two should be accepted (SPI)
public typealias D = BetweenTargets // expected-error {{'BetweenTargets' is only available in}} expected-note {{add @available attribute to enclosing type alias}}
public typealias E = AtDeploymentTarget // expected-error {{'AtDeploymentTarget' is only available in}} expected-note {{add @available attribute to enclosing type alias}}
public typealias D = BetweenTargets
public typealias E = AtDeploymentTarget
public typealias F = AfterDeploymentTarget // expected-error {{'AfterDeploymentTarget' is only available in}} expected-note {{add @available attribute to enclosing type alias}}
}

Expand Down