AST: Use availability to control decl visibility in public swiftinterfaces.

Declarations that are unavailable at runtime because of an `@available`
attribute referencing a custom domain that was imported `@_spiOnly` should be
hidden from public swiftinterface files in `-library-level=api` modules. For
remaining declarations that do get printed in the public swiftinterface, skip
printing any `@available` attribute that refers to the domains from those
`@_spiOnly` dependencies. This allows API developers to control declaration
visibility using availability defined by another module.

Resolves rdar://156512028.
This commit is contained in:
Allan Shortlidge
2025-09-02 16:38:22 -07:00
parent ddca4b7404
commit b047396246
7 changed files with 185 additions and 4 deletions

View File

@@ -20,6 +20,8 @@
#include "swift/AST/ASTMangler.h"
#include "swift/AST/ASTVisitor.h"
#include "swift/AST/Attr.h"
#include "swift/AST/AvailabilityConstraint.h"
#include "swift/AST/AvailabilityContext.h"
#include "swift/AST/Builtins.h"
#include "swift/AST/ClangModuleLoader.h"
#include "swift/AST/Comment.h"
@@ -189,6 +191,38 @@ static bool shouldPrintAllSemanticDetails(const PrintOptions &options) {
return false;
}
static bool shouldSkipDeclInPublicInterface(const Decl *D) {
// @_spi should be skipped in the public interface.
if (D->isSPI())
return true;
// Decls that are unavailable at runtime in an availability domain that has
// been @_spiOnly imported should be hidden from the public interface of
// a -library-level=api module.
auto &ctx = D->getDeclContext()->getASTContext();
if (ctx.LangOpts.LibraryLevel != LibraryLevel::API)
return false;
auto *SF = D->getDeclContext()->getParentSourceFile();
if (!SF)
return false;
auto constraints = getAvailabilityConstraintsForDecl(
D, AvailabilityContext::forDeploymentTarget(ctx));
llvm::SmallVector<AvailabilityDomain, 4> unavailableDomains;
getRuntimeUnavailableDomains(constraints, unavailableDomains, ctx);
for (auto domain : unavailableDomains) {
if (auto *domainDecl = domain.getDecl()) {
if (SF->getRestrictedImportKind(domainDecl->getModuleContext()) ==
RestrictedImportKind::SPIOnly)
return true;
}
}
return false;
}
/// Get the non-recursive printing options that should be applied when
/// printing the type of a value decl.
static NonRecursivePrintOptions getNonRecursiveOptions(const ValueDecl *D) {
@@ -293,8 +327,7 @@ PrintOptions PrintOptions::printSwiftInterfaceFile(ModuleDecl *ModuleToPrint,
if (D->getAttrs().hasAttribute<ImplementationOnlyAttr>())
return false;
// Skip SPI decls if `PrintSPIs`.
if (options.printPublicInterface() && D->isSPI())
if (options.printPublicInterface() && shouldSkipDeclInPublicInterface(D))
return false;
if (auto *VD = dyn_cast<ValueDecl>(D)) {
@@ -410,8 +443,6 @@ PrintOptions PrintOptions::printSwiftInterfaceFile(ModuleDecl *ModuleToPrint,
result.CurrentPrintabilityChecker =
std::make_shared<ShouldPrintForModuleInterface>();
// FIXME: We don't really need 'public' on everything; we could just change
// the default to 'public' and mark the 'internal' things.
result.PrintAccess = true;
result.ExcludeAttrList = {

View File

@@ -805,6 +805,7 @@ void DeclAttributes::print(ASTPrinter &Printer, const PrintOptions &Options,
AttributeVector modifiers;
bool libraryLevelAPI =
D && D->getASTContext().LangOpts.LibraryLevel == LibraryLevel::API;
auto *SF = D ? D->getDeclContext()->getParentSourceFile() : nullptr;
for (auto DA : llvm::reverse(FlattenedAttrs)) {
// Don't skip implicit custom attributes. Custom attributes like global
@@ -849,6 +850,16 @@ void DeclAttributes::print(ASTPrinter &Printer, const PrintOptions &Options,
if (!semanticAttr)
continue;
// In the public interfaces of -library-level=api modules, skip @available
// attributes that refer to domains imported from @_spiOnly modules.
if (Options.printPublicInterface() && libraryLevelAPI) {
if (auto *domainDecl = semanticAttr->getDomain().getDecl()) {
if (SF->getRestrictedImportKind(domainDecl->getModuleContext()) ==
RestrictedImportKind::SPIOnly)
continue;
}
}
if (isShortAvailable(*semanticAttr)) {
if (semanticAttr->isSwiftLanguageModeSpecific())
swiftVersionAvailableAttribute.emplace(*semanticAttr);

View File

@@ -2879,6 +2879,7 @@ bool SourceFile::hasTestableOrPrivateImport(
});
}
// FIXME: This should probably be requestified.
RestrictedImportKind SourceFile::getRestrictedImportKind(const ModuleDecl *module) const {
auto &imports = getASTContext().getImportCache();
RestrictedImportKind importKind = RestrictedImportKind::MissingImport;

View File

@@ -231,6 +231,8 @@ static bool shouldDiagnoseDeclAccess(const ValueDecl *D,
const ExportContext &where) {
auto reason = where.getExportabilityReason();
auto DC = where.getDeclContext();
if (!reason)
return false;
switch (*reason) {
case ExportabilityReason::ExtensionWithPublicMembers:
@@ -304,6 +306,15 @@ static bool diagnoseValueDeclRefExportability(SourceLoc loc, const ValueDecl *D,
break;
case DisallowedOriginKind::SPIOnly:
// Availability attributes referring to availability domains from modules
// that are imported @_spiOnly in a -library-level=api will not be printed
// in the public swiftinterface of the module and should therefore not be
// diagnosed for exportability.
if (reason && reason == ExportabilityReason::AvailableAttribute &&
ctx.LangOpts.LibraryLevel == LibraryLevel::API)
return false;
break;
case DisallowedOriginKind::ImplementationOnly:
case DisallowedOriginKind::SPIImported:
case DisallowedOriginKind::SPILocal:

View File

@@ -1,6 +1,10 @@
#include <availability_domain.h>
int huron_pred(void);
CLANG_ENABLED_AVAILABILITY_DOMAIN(Salt);
CLANG_DISABLED_AVAILABILITY_DOMAIN(Erie);
CLANG_DYNAMIC_AVAILABILITY_DOMAIN(Huron, huron_pred);
#define AVAIL 0
#define UNAVAIL 1

View File

@@ -1,7 +1,10 @@
#include <availability_domain.h>
int aegean_pred(void);
CLANG_ENABLED_AVAILABILITY_DOMAIN(Baltic);
CLANG_DISABLED_AVAILABILITY_DOMAIN(Mediterranean);
CLANG_DYNAMIC_AVAILABILITY_DOMAIN(Aegean, aegean_pred);
#define AVAIL 0
#define UNAVAIL 1

View File

@@ -0,0 +1,120 @@
// RUN: %empty-directory(%t)
// RUN: %target-swift-frontend -emit-module %s \
// RUN: -I %S/../Inputs/custom-modules/availability-domains \
// RUN: -enable-experimental-feature CustomAvailability \
// RUN: -experimental-spi-only-imports \
// RUN: -enable-library-evolution -swift-version 5 -library-level api \
// RUN: -package-name TestPackage -module-name Test \
// RUN: -emit-module-interface-path %t/Test.swiftinterface \
// RUN: -emit-private-module-interface-path %t/Test.private.swiftinterface \
// RUN: -emit-package-module-interface-path %t/Test.package.swiftinterface
// RUN: %target-swift-typecheck-module-from-interface(%t/Test.swiftinterface) \
// RUN: -I %S/../Inputs/custom-modules/availability-domains \
// RUN: -module-name Test
// RUN: %target-swift-typecheck-module-from-interface(%t/Test.private.swiftinterface) \
// RUN: -I %S/../Inputs/custom-modules/availability-domains \
// RUN: -module-name Test
// RUN: %target-swift-typecheck-module-from-interface(%t/Test.package.swiftinterface) \
// RUN: -I %S/../Inputs/custom-modules/availability-domains \
// RUN: -module-name Test
// RUN: %FileCheck %s --check-prefixes=CHECK --input-file %t/Test.swiftinterface
// RUN: %FileCheck %s --check-prefixes=CHECK-PUBLIC --input-file %t/Test.swiftinterface
// RUN: %FileCheck %s --check-prefixes=CHECK,CHECK-NONPUBLIC --input-file %t/Test.private.swiftinterface
// RUN: %FileCheck %s --check-prefixes=CHECK,CHECK-NONPUBLIC --input-file %t/Test.package.swiftinterface
// REQUIRES: swift_feature_CustomAvailability
import Lakes
@_spiOnly import Seas
// CHECK: @available(Salt)
// CHECK: public func availableInPublicEnabledDomain()
@available(Salt)
public func availableInPublicEnabledDomain() { }
// CHECK: @available(Erie)
// CHECK: public func availableInPublicDisabledDomain()
@available(Erie)
public func availableInPublicDisabledDomain() { }
// CHECK: @available(Huron)
// CHECK: public func availableInPublicDynamicDomain()
@available(Huron)
public func availableInPublicDynamicDomain() { }
// CHECK: @available(Salt, unavailable)
// CHECK: public func unavailableInPublicEnabledDomain()
@available(Salt, unavailable)
public func unavailableInPublicEnabledDomain() { }
// CHECK: @available(Erie, unavailable)
// CHECK: public func unavailableInPublicDisabledDomain()
@available(Erie, unavailable)
public func unavailableInPublicDisabledDomain() { }
// CHECK: @available(Huron, unavailable)
// CHECK: public func unavailableInPublicDynamicDomain()
@available(Huron, unavailable)
public func unavailableInPublicDynamicDomain() { }
// CHECK-NONPUBLIC: @available(Baltic)
// CHECK-PUBLIC-NOT: Baltic
// CHECK-LABEL: public func availableInSPIOnlyEnabledDomain()
@available(Baltic)
public func availableInSPIOnlyEnabledDomain() { }
// CHECK-NONPUBLIC: @available(Mediterranean)
// CHECK-NONPUBLIC-LABEL: public func availableInSPIOnlyDisabledDomain_Secret()
// CHECK-PUBLIC-NOT: Mediterranean
// CHECK-PUBLIC-NOT: availableInSPIOnlyDisabledDomain_Secret
@available(Mediterranean)
public func availableInSPIOnlyDisabledDomain_Secret() { }
// CHECK-NONPUBLIC: @available(Aegean)
// CHECK-PUBLIC-NOT: Aegean
// CHECK-LABEL: public func availableInSPIOnlyDynamicDomain()
@available(Aegean)
public func availableInSPIOnlyDynamicDomain() { }
// CHECK-NONPUBLIC: @available(Baltic, unavailable)
// CHECK-NONPUBLIC-LABEL: public func unavailableInSPIOnlyEnabledDomain_Secret()
// CHECK-PUBLIC-NOT: Baltic
// CHECK-PUBLIC-NOT: unavailableInSPIOnlyEnabledDomain_Secret
@available(Baltic, unavailable)
public func unavailableInSPIOnlyEnabledDomain_Secret() { }
// CHECK-NONPUBLIC: @available(Mediterranean, unavailable)
// CHECK-PUBLIC-NOT: Mediterranean
// CHECK-LABEL: public func unavailableInSPIOnlyDisabledDomain()
@available(Mediterranean, unavailable)
public func unavailableInSPIOnlyDisabledDomain() { }
// CHECK: @available(*, unavailable)
// CHECK-NONPUBLIC: @available(Baltic)
// CHECK-PUBLIC-NOT: Baltic
// CHECK-LABEL: public func availableInSPIOnlyEnabledDomainAndUnavailableUniversally()
@available(*, unavailable)
@available(Baltic)
public func availableInSPIOnlyEnabledDomainAndUnavailableUniversally() { }
// CHECK-NONPUBLIC: @available(*, unavailable)
// CHECK-NONPUBLIC: @available(Mediterranean)
// CHECK-NONPUBLIC-LABEL: public func availableInSPIOnlyDisabledDomainAndUnavailableUniversally_Secret()
// CHECK-PUBLIC-NOT: Mediterranean
// CHECK-PUBLIC-NOT: availableInSPIOnlyDisabledDomainAndUnavailableUniversally_Secret
@available(*, unavailable)
@available(Mediterranean)
public func availableInSPIOnlyDisabledDomainAndUnavailableUniversally_Secret() { }
// CHECK: @available(*, unavailable)
// CHECK-NONPUBLIC: @available(Aegean)
// CHECK-PUBLIC-NOT: Aegean
// CHECK-LABEL: public func availableInSPIOnlyDynamicDomainAndUnavailableUniversally()
@available(*, unavailable)
@available(Aegean)
public func availableInSPIOnlyDynamicDomainAndUnavailableUniversally() { }