Skip to content

Commit f7f69c6

Browse files
committed
[Serialization] Load non-public transitive dependencies on @testable imports
A @testable import allows a client to call internal decls which may refer to non-public dependencies. To support such a use case, load non-public transitive dependencies of a module when it's imported @testable from the main module. This replaces the previous behavior where we loaded those dependencies for any modules built for testing. This was risky as we would load more module for any debug build, opening the door to a different behavior between debug and release builds. In contrast, applying this logic to @testable clients will only change the behavior of test targets. rdar://107329303
1 parent 87431a7 commit f7f69c6

File tree

9 files changed

+72
-13
lines changed

9 files changed

+72
-13
lines changed

include/swift/AST/FileUnit.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,9 @@ class FileUnit : public DeclContext, public ASTAllocated<FileUnit> {
294294
virtual void
295295
collectLinkLibraries(ModuleDecl::LinkLibraryCallback callback) const {}
296296

297+
/// Load extra dependencies of this module to satisfy a testable import.
298+
virtual void loadDependenciesForTestable(SourceLoc diagLoc) const {}
299+
297300
/// Returns the path of the file or directory that defines the module
298301
/// represented by this \c FileUnit, or empty string if there is none.
299302
/// Cross-import overlay specifiers are found relative to this path.

include/swift/Serialization/SerializedModuleLoader.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,8 @@ class SerializedASTFile final : public LoadedFile {
454454
virtual void
455455
collectLinkLibraries(ModuleDecl::LinkLibraryCallback callback) const override;
456456

457+
virtual void loadDependenciesForTestable(SourceLoc diagLoc) const override;
458+
457459
Identifier getDiscriminatorForPrivateValue(const ValueDecl *D) const override;
458460

459461
virtual StringRef getFilename() const override;

lib/Sema/ImportResolution.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,12 @@ void ImportResolver::bindImport(UnboundImport &&I) {
324324
return;
325325
}
326326

327+
// Load more dependencies for testable imports.
328+
if (I.import.options.contains(ImportFlags::Testable)) {
329+
for (auto file: M->getFiles())
330+
file->loadDependenciesForTestable(ID.get()->getStartLoc());
331+
}
332+
327333
auto topLevelModule = I.getTopLevelModule(M, SF);
328334

329335
I.validateOptions(topLevelModule, SF);

lib/Serialization/ModuleFile.cpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ ModuleFile::loadDependenciesForFileContext(const FileUnit *file,
133133

134134
bool missingDependency = false;
135135
for (auto &dependency : Dependencies) {
136+
if (forTestable && dependency.isLoaded())
137+
continue;
138+
136139
assert(!dependency.isLoaded() && "already loaded?");
137140

138141
if (dependency.isHeader()) {
@@ -156,7 +159,7 @@ ModuleFile::loadDependenciesForFileContext(const FileUnit *file,
156159
}
157160

158161
ModuleLoadingBehavior transitiveBehavior =
159-
getTransitiveLoadingBehavior(dependency);
162+
getTransitiveLoadingBehavior(dependency, forTestable);
160163

161164
if (ctx.LangOpts.EnableModuleLoadingRemarks) {
162165
ctx.Diags.diagnose(diagLoc,
@@ -282,7 +285,8 @@ Status ModuleFile::associateWithFileContext(FileUnit *file, SourceLoc diagLoc,
282285
}
283286

284287
ModuleLoadingBehavior
285-
ModuleFile::getTransitiveLoadingBehavior(const Dependency &dependency) const {
288+
ModuleFile::getTransitiveLoadingBehavior(const Dependency &dependency,
289+
bool forTestable) const {
286290
ASTContext &ctx = getContext();
287291
ModuleDecl *mod = FileContext->getParentModule();
288292

@@ -293,7 +297,8 @@ ModuleFile::getTransitiveLoadingBehavior(const Dependency &dependency) const {
293297
return Core->getTransitiveLoadingBehavior(dependency.Core,
294298
ctx.LangOpts.DebuggerSupport,
295299
isPartialModule,
296-
ctx.LangOpts.PackageName);
300+
ctx.LangOpts.PackageName,
301+
forTestable);
297302
}
298303

299304
bool ModuleFile::mayHaveDiagnosticsPointingAtBuffer() const {

lib/Serialization/ModuleFile.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,8 @@ class ModuleFile
664664

665665
/// How should \p dependency be loaded for a transitive import via \c this?
666666
ModuleLoadingBehavior
667-
getTransitiveLoadingBehavior(const Dependency &dependency) const;
667+
getTransitiveLoadingBehavior(const Dependency &dependency,
668+
bool forTestable) const;
668669

669670
/// Returns `true` if there is a buffer that might contain source code where
670671
/// other parts of the compiler could have emitted diagnostics, to indicate

lib/Serialization/ModuleFileSharedCore.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1688,7 +1688,8 @@ ModuleFileSharedCore::getTransitiveLoadingBehavior(
16881688
const Dependency &dependency,
16891689
bool debuggerMode,
16901690
bool isPartialModule,
1691-
StringRef packageName) const {
1691+
StringRef packageName,
1692+
bool forTestable) const {
16921693
if (isPartialModule) {
16931694
// Keep the merge-module behavior for legacy support. In that case
16941695
// we load all transitive dependencies from partial modules and
@@ -1717,7 +1718,7 @@ ModuleFileSharedCore::getTransitiveLoadingBehavior(
17171718
// Non-public imports are similar to implementation-only, the module
17181719
// loading behavior differs on loading those dependencies
17191720
// on testable imports.
1720-
if (isTestable() || !moduleIsResilient) {
1721+
if (forTestable || !moduleIsResilient) {
17211722
return ModuleLoadingBehavior::Required;
17221723
} else if (debuggerMode) {
17231724
return ModuleLoadingBehavior::Optional;
@@ -1730,6 +1731,7 @@ ModuleFileSharedCore::getTransitiveLoadingBehavior(
17301731
// Package dependencies are usually loaded only for import from the same
17311732
// package.
17321733
if ((!packageName.empty() && packageName == getModulePackageName()) ||
1734+
forTestable ||
17331735
!moduleIsResilient) {
17341736
return ModuleLoadingBehavior::Required;
17351737
} else if (debuggerMode) {

lib/Serialization/ModuleFileSharedCore.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,11 +614,17 @@ class ModuleFileSharedCore {
614614
///
615615
/// If \p packageName is set, transitive package dependencies are loaded if
616616
/// loaded from the same package.
617+
///
618+
/// If \p forTestable, get the desired loading behavior for a @testable
619+
/// import. Reports non-public dependencies as required for a testable
620+
/// client so it can access internal details, which in turn can reference
621+
/// those non-public dependencies.
617622
ModuleLoadingBehavior
618623
getTransitiveLoadingBehavior(const Dependency &dependency,
619624
bool debuggerMode,
620625
bool isPartialModule,
621-
StringRef packageName) const;
626+
StringRef packageName,
627+
bool forTestable) const;
622628
};
623629

624630
template <typename T, typename RawData>

lib/Serialization/SerializedModuleLoader.cpp

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,8 @@ llvm::ErrorOr<ModuleDependencyInfo> SerializedModuleLoaderBase::scanModuleFile(
430430
loadedModuleFile->getTransitiveLoadingBehavior(dependency,
431431
/*debuggerMode*/false,
432432
/*isPartialModule*/false,
433-
/*package*/Ctx.LangOpts.PackageName);
433+
/*package*/Ctx.LangOpts.PackageName,
434+
loadedModuleFile->isTestable());
434435
if (transitiveBehavior != ModuleLoadingBehavior::Required)
435436
continue;
436437

@@ -1039,11 +1040,12 @@ void swift::serialization::diagnoseSerializedASTLoadFailureTransitive(
10391040
std::copy_if(
10401041
loadedModuleFile->getDependencies().begin(),
10411042
loadedModuleFile->getDependencies().end(), std::back_inserter(missing),
1042-
[&duplicates, &loadedModuleFile](
1043+
[&duplicates, &loadedModuleFile, forTestable](
10431044
const ModuleFile::Dependency &dependency) -> bool {
10441045
if (dependency.isLoaded() || dependency.isHeader() ||
1045-
loadedModuleFile->getTransitiveLoadingBehavior(dependency) !=
1046-
ModuleLoadingBehavior::Required) {
1046+
loadedModuleFile->getTransitiveLoadingBehavior(dependency,
1047+
forTestable)
1048+
!= ModuleLoadingBehavior::Required) {
10471049
return false;
10481050
}
10491051
return duplicates.insert(dependency.Core.RawPath).second;
@@ -1472,6 +1474,18 @@ void SerializedASTFile::collectLinkLibraries(
14721474
}
14731475
}
14741476

1477+
void SerializedASTFile::loadDependenciesForTestable(SourceLoc diagLoc) const {
1478+
serialization::Status status =
1479+
File.loadDependenciesForFileContext(this, diagLoc, /*forTestable=*/true);
1480+
1481+
if (status != serialization::Status::Valid) {
1482+
if (diagLoc)
1483+
serialization::diagnoseSerializedASTLoadFailureTransitive(
1484+
getASTContext(), diagLoc, status, &File,
1485+
getParentModule()->getName(), /*forTestable*/true);
1486+
}
1487+
}
1488+
14751489
bool SerializedASTFile::isSIB() const {
14761490
return File.isSIB();
14771491
}

test/Serialization/access-level-import-dependencies.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ import PrivateDep
7474
// RUN: %target-swift-frontend -typecheck %t/ClientOfNonPublic.swift -I %t \
7575
// RUN: -Rmodule-loading 2>&1 | %FileCheck -check-prefix=VISIBLE-DEP %s
7676

77-
/// Even with resilience, all access-level dependencies are visible to clients
78-
/// for modules with testing enabled.
77+
/// Even with resilience and testing enabled, all non-public dependencies are
78+
/// hidden if there are no testable imports.
7979
// RUN: %target-swift-frontend -emit-module %t/PublicDep.swift -o %t -I %t \
8080
// RUN: -enable-library-evolution -enable-testing \
8181
// RUN: -enable-experimental-feature AccessLevelOnImport
@@ -95,4 +95,24 @@ import PrivateDep
9595
// RUN: %target-swift-frontend -typecheck %t/ClientOfPublic.swift -I %t \
9696
// RUN: -Rmodule-loading 2>&1 | %FileCheck -check-prefix=VISIBLE-DEP %s
9797
// RUN: %target-swift-frontend -typecheck %t/ClientOfNonPublic.swift -I %t \
98+
// RUN: -Rmodule-loading 2>&1 | %FileCheck -check-prefix=HIDDEN-DEP %s
99+
100+
/// With testable imports, transitive dependencies are required.
101+
//--- TestableClientOfPublic.swift
102+
@testable import PublicDep
103+
104+
//--- TestableClientOfNonPublic.swift
105+
@testable import PackageDep // expected-error {{missing required module 'HiddenDep'}}
106+
@testable import InternalDep // expected-error {{missing required module 'HiddenDep'}}
107+
@testable import FileprivateDep // expected-error {{missing required module 'HiddenDep'}}
108+
@testable import PrivateDep // expected-error {{missing required module 'HiddenDep'}}
109+
110+
// RUN: %target-swift-frontend -typecheck %t/TestableClientOfPublic.swift -I %t \
111+
// RUN: -Rmodule-loading 2>&1 | %FileCheck -check-prefix=VISIBLE-DEP %s
112+
// RUN: %target-swift-frontend -typecheck %t/TestableClientOfNonPublic.swift -I %t \
98113
// RUN: -Rmodule-loading 2>&1 | %FileCheck -check-prefix=VISIBLE-DEP %s
114+
115+
/// Fail if the transitive dependency is missing.
116+
// RUN: rm %t/HiddenDep.swiftmodule
117+
// RUN: %target-swift-frontend -typecheck %t/TestableClientOfNonPublic.swift -I %t \
118+
// RUN: -verify -show-diagnostics-after-fatal

0 commit comments

Comments
 (0)