Skip to content

Commit d4c90d6

Browse files
[DependencyScanning] Handle testable dependencies correctly
Teach scanner to pick and choose binary modules correctly based on if it is testable import or not. Some situations that scanner need to be careful when testable is involved: * When it is a regular import, it should not import binary modules that are built with -enable-testing, it should prefer interfaces if that is available. * When testable import, it should only load binary module and it should make sure the internal imports from binary modules are actually required for testable import to work. If a testable import only find a regular binary module, dependency scanner currently will just preceed with such module and leave the diagnostics to swift-frontend, because the alternative (failed to find module) can be confusing to users. rdar://125914165
1 parent 0e12f20 commit d4c90d6

File tree

6 files changed

+154
-64
lines changed

6 files changed

+154
-64
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,6 +1193,9 @@ REMARK(module_api_import_aliases,none,
11931193
"%select{, which reexports definition from %2|}3",
11941194
(const Decl *, ModuleDecl *, ModuleDecl *, bool))
11951195

1196+
REMARK(skip_module_invalid,none,"skip invalid swiftmodule: %0", (StringRef))
1197+
REMARK(skip_module_testable,none,"skip swiftmodule built with '-enable-testing': %0", (StringRef))
1198+
11961199
REMARK(macro_loaded,none,
11971200
"loaded macro implementation module %0 from "
11981201
"%select{shared library '%2'|executable '%2'|"

include/swift/Serialization/ScanningLoaders.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ class SwiftModuleScanner : public SerializedModuleLoaderBase {
3434

3535
/// Scan the given interface file to determine dependencies.
3636
llvm::ErrorOr<ModuleDependencyInfo>
37-
scanInterfaceFile(Twine moduleInterfacePath, bool isFramework);
37+
scanInterfaceFile(Twine moduleInterfacePath, bool isFramework,
38+
bool isTestableImport);
3839

3940
InterfaceSubContextDelegate &astDelegate;
4041

include/swift/Serialization/SerializedModuleLoader.h

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
namespace swift {
2525
class ModuleFile;
2626
class PathObfuscator;
27+
class ModuleFileSharedCore;
2728
enum class ModuleLoadingBehavior;
2829
namespace file_types {
2930
enum ID : uint8_t;
@@ -162,22 +163,18 @@ class SerializedModuleLoaderBase : public ModuleLoader {
162163
}
163164

164165
/// Scan the given serialized module file to determine dependencies.
165-
llvm::ErrorOr<ModuleDependencyInfo> scanModuleFile(Twine modulePath, bool isFramework);
166+
llvm::ErrorOr<ModuleDependencyInfo>
167+
scanModuleFile(Twine modulePath, bool isFramework, bool isTestableImport);
166168

167169
struct BinaryModuleImports {
168170
llvm::StringSet<> moduleImports;
169171
std::string headerImport;
170172
};
171173

172-
static llvm::ErrorOr<BinaryModuleImports>
173-
getImportsOfModule(Twine modulePath,
174+
static BinaryModuleImports
175+
getImportsOfModule(const ModuleFileSharedCore &loadedModule,
174176
ModuleLoadingBehavior transitiveBehavior,
175-
bool isFramework,
176-
bool isRequiredOSSAModules,
177-
StringRef SDKName,
178-
StringRef packageName,
179-
llvm::vfs::FileSystem *fileSystem,
180-
PathObfuscator &recoverer);
177+
StringRef packageName);
181178

182179
/// Load the module file into a buffer and also collect its module name.
183180
static std::unique_ptr<llvm::MemoryBuffer>

lib/Serialization/ScanningLoaders.cpp

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include "llvm/Support/Threading.h"
3434
#include "llvm/Support/VirtualFileSystem.h"
3535
#include <algorithm>
36+
#include <system_error>
3637

3738
using namespace swift;
3839

@@ -55,7 +56,8 @@ std::error_code SwiftModuleScanner::findModuleFilesInDirectory(
5556
if (LoadMode == ModuleLoadingMode::OnlySerialized || !InPath) {
5657
if (fs.exists(ModPath)) {
5758
// The module file will be loaded directly.
58-
auto dependencies = scanModuleFile(ModPath, IsFramework);
59+
auto dependencies =
60+
scanModuleFile(ModPath, IsFramework, isTestableDependencyLookup);
5961
if (dependencies) {
6062
this->dependencies = std::move(dependencies.get());
6163
return std::error_code();
@@ -66,7 +68,8 @@ std::error_code SwiftModuleScanner::findModuleFilesInDirectory(
6668
}
6769
assert(InPath);
6870

69-
auto dependencies = scanInterfaceFile(*InPath, IsFramework);
71+
auto dependencies =
72+
scanInterfaceFile(*InPath, IsFramework, isTestableDependencyLookup);
7073
if (dependencies) {
7174
this->dependencies = std::move(dependencies.get());
7275
return std::error_code();
@@ -133,7 +136,7 @@ static std::vector<std::string> getCompiledCandidates(ASTContext &ctx,
133136

134137
llvm::ErrorOr<ModuleDependencyInfo>
135138
SwiftModuleScanner::scanInterfaceFile(Twine moduleInterfacePath,
136-
bool isFramework) {
139+
bool isFramework, bool isTestableImport) {
137140
// Create a module filename.
138141
// FIXME: Query the module interface loader to determine an appropriate
139142
// name for the module, which includes an appropriate hash.
@@ -156,12 +159,16 @@ SwiftModuleScanner::scanInterfaceFile(Twine moduleInterfacePath,
156159
!Ctx.SearchPathOpts.NoScannerModuleValidation) {
157160
assert(compiledCandidates.size() == 1 &&
158161
"Should only have 1 candidate module");
159-
auto BinaryDep = scanModuleFile(compiledCandidates[0], isFramework);
160-
if (!BinaryDep)
161-
return BinaryDep.getError();
162+
auto BinaryDep = scanModuleFile(compiledCandidates[0], isFramework,
163+
isTestableImport);
164+
if (BinaryDep) {
165+
Result = *BinaryDep;
166+
return std::error_code();
167+
}
162168

163-
Result = *BinaryDep;
164-
return std::error_code();
169+
// If return no such file, just fallback to use interface.
170+
if (BinaryDep.getError() != std::errc::no_such_file_or_directory)
171+
return BinaryDep.getError();
165172
}
166173

167174
std::vector<std::string> Args(BaseArgs.begin(), BaseArgs.end());

lib/Serialization/SerializedModuleLoader.cpp

Lines changed: 50 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -392,29 +392,13 @@ std::error_code SerializedModuleLoaderBase::openModuleFile(
392392
return std::error_code();
393393
}
394394

395-
llvm::ErrorOr<SerializedModuleLoaderBase::BinaryModuleImports>
395+
SerializedModuleLoaderBase::BinaryModuleImports
396396
SerializedModuleLoaderBase::getImportsOfModule(
397-
Twine modulePath, ModuleLoadingBehavior transitiveBehavior,
398-
bool isFramework, bool isRequiredOSSAModules, StringRef SDKName,
399-
StringRef packageName, llvm::vfs::FileSystem *fileSystem,
400-
PathObfuscator &recoverer) {
401-
auto moduleBuf = fileSystem->getBufferForFile(modulePath);
402-
if (!moduleBuf)
403-
return moduleBuf.getError();
404-
397+
const ModuleFileSharedCore &loadedModuleFile,
398+
ModuleLoadingBehavior transitiveBehavior, StringRef packageName) {
405399
llvm::StringSet<> importedModuleNames;
406400
std::string importedHeader = "";
407-
std::shared_ptr<const ModuleFileSharedCore> loadedModuleFile;
408-
serialization::ValidationInfo loadInfo = ModuleFileSharedCore::load(
409-
"", "", std::move(moduleBuf.get()), nullptr, nullptr, isFramework,
410-
isRequiredOSSAModules,
411-
SDKName, recoverer, loadedModuleFile);
412-
413-
// If failed to load, just ignore and return do not found.
414-
if (loadInfo.status != serialization::Status::Valid)
415-
return std::make_error_code(std::errc::no_such_file_or_directory);
416-
417-
for (const auto &dependency : loadedModuleFile->getDependencies()) {
401+
for (const auto &dependency : loadedModuleFile.getDependencies()) {
418402
if (dependency.isHeader()) {
419403
assert(importedHeader.empty() &&
420404
"Unexpected more than one header dependency");
@@ -423,11 +407,11 @@ SerializedModuleLoaderBase::getImportsOfModule(
423407
}
424408

425409
ModuleLoadingBehavior dependencyTransitiveBehavior =
426-
loadedModuleFile->getTransitiveLoadingBehavior(
410+
loadedModuleFile.getTransitiveLoadingBehavior(
427411
dependency,
428412
/*debuggerMode*/ false,
429413
/*isPartialModule*/ false, packageName,
430-
loadedModuleFile->isTestable());
414+
loadedModuleFile.isTestable());
431415
if (dependencyTransitiveBehavior > transitiveBehavior)
432416
continue;
433417

@@ -441,41 +425,60 @@ SerializedModuleLoaderBase::getImportsOfModule(
441425
importedModuleNames.insert(moduleName);
442426
}
443427

444-
return SerializedModuleLoaderBase::BinaryModuleImports{importedModuleNames, importedHeader};
428+
return SerializedModuleLoaderBase::BinaryModuleImports{importedModuleNames,
429+
importedHeader};
445430
}
446431

447432
llvm::ErrorOr<ModuleDependencyInfo>
448-
SerializedModuleLoaderBase::scanModuleFile(Twine modulePath, bool isFramework) {
433+
SerializedModuleLoaderBase::scanModuleFile(Twine modulePath, bool isFramework,
434+
bool isTestableImport) {
449435
const std::string moduleDocPath;
450436
const std::string sourceInfoPath;
437+
438+
// Read and valid module.
439+
auto moduleBuf = Ctx.SourceMgr.getFileSystem()->getBufferForFile(modulePath);
440+
if (!moduleBuf)
441+
return moduleBuf.getError();
442+
443+
std::shared_ptr<const ModuleFileSharedCore> loadedModuleFile;
444+
serialization::ValidationInfo loadInfo = ModuleFileSharedCore::load(
445+
"", "", std::move(moduleBuf.get()), nullptr, nullptr, isFramework,
446+
isRequiredOSSAModules(), Ctx.LangOpts.SDKName,
447+
Ctx.SearchPathOpts.DeserializedPathRecoverer, loadedModuleFile);
448+
449+
if (!Ctx.SearchPathOpts.NoScannerModuleValidation) {
450+
// If failed to load, just ignore and return do not found.
451+
if (loadInfo.status != serialization::Status::Valid) {
452+
if (Ctx.LangOpts.EnableModuleLoadingRemarks)
453+
Ctx.Diags.diagnose(SourceLoc(), diag::skip_module_invalid,
454+
modulePath.str());
455+
return std::make_error_code(std::errc::no_such_file_or_directory);
456+
}
457+
458+
if (loadedModuleFile->isTestable() && !isTestableImport) {
459+
if (Ctx.LangOpts.EnableModuleLoadingRemarks)
460+
Ctx.Diags.diagnose(SourceLoc(), diag::skip_module_testable,
461+
modulePath.str());
462+
return std::make_error_code(std::errc::no_such_file_or_directory);
463+
}
464+
}
465+
451466
// Some transitive dependencies of binary modules are not required to be
452467
// imported during normal builds.
453468
// TODO: This is worth revisiting for debugger purposes where
454469
// loading the module is optional, and implementation-only imports
455470
// from modules with testing enabled where the dependency is
456471
// optional.
457-
ModuleLoadingBehavior transitiveLoadingBehavior =
458-
ModuleLoadingBehavior::Required;
459-
auto binaryModuleImports = getImportsOfModule(
460-
modulePath, transitiveLoadingBehavior, isFramework,
461-
isRequiredOSSAModules(),
462-
Ctx.LangOpts.SDKName, Ctx.LangOpts.PackageName,
463-
Ctx.SourceMgr.getFileSystem().get(),
464-
Ctx.SearchPathOpts.DeserializedPathRecoverer);
465-
if (!binaryModuleImports)
466-
return binaryModuleImports.getError();
472+
auto binaryModuleImports =
473+
getImportsOfModule(*loadedModuleFile, ModuleLoadingBehavior::Required,
474+
Ctx.LangOpts.PackageName);
467475

468476
// Lookup optional imports of this module also
469-
auto binaryModuleOptionalImports = getImportsOfModule(
470-
modulePath, ModuleLoadingBehavior::Optional, isFramework,
471-
isRequiredOSSAModules(),
472-
Ctx.LangOpts.SDKName, Ctx.LangOpts.PackageName,
473-
Ctx.SourceMgr.getFileSystem().get(),
474-
Ctx.SearchPathOpts.DeserializedPathRecoverer);
475-
if (!binaryModuleOptionalImports)
476-
return binaryModuleImports.getError();
477+
auto binaryModuleOptionalImports =
478+
getImportsOfModule(*loadedModuleFile, ModuleLoadingBehavior::Optional,
479+
Ctx.LangOpts.PackageName);
477480

478-
auto importedModuleSet = binaryModuleImports.get().moduleImports;
481+
auto importedModuleSet = binaryModuleImports.moduleImports;
479482
std::vector<std::string> importedModuleNames;
480483
importedModuleNames.reserve(importedModuleSet.size());
481484
llvm::transform(importedModuleSet.keys(),
@@ -484,8 +487,8 @@ SerializedModuleLoaderBase::scanModuleFile(Twine modulePath, bool isFramework) {
484487
return N.str();
485488
});
486489

487-
auto importedHeader = binaryModuleImports.get().headerImport;
488-
auto &importedOptionalModuleSet = binaryModuleOptionalImports.get().moduleImports;
490+
auto importedHeader = binaryModuleImports.headerImport;
491+
auto &importedOptionalModuleSet = binaryModuleOptionalImports.moduleImports;
489492
std::vector<std::string> importedOptionalModuleNames;
490493
for (const auto optionalImportedModule : importedOptionalModuleSet.keys())
491494
if (!importedModuleSet.contains(optionalImportedModule))
@@ -794,7 +797,8 @@ bool SerializedModuleLoaderBase::findModule(
794797
auto result = findModuleFilesInDirectory(
795798
moduleID, absoluteBaseName, moduleInterfacePath,
796799
moduleInterfaceSourcePath, moduleBuffer, moduleDocBuffer,
797-
moduleSourceInfoBuffer, skipBuildingInterface, isFramework);
800+
moduleSourceInfoBuffer, skipBuildingInterface, isFramework,
801+
isTestableDependencyLookup);
798802
if (!result)
799803
return true;
800804
if (result == std::errc::not_supported)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: split-file %s %t
3+
4+
// RUN: %target-swift-frontend -emit-module -module-name B -o %t/internal/B.swiftmodule -swift-version 5 \
5+
// RUN: -disable-implicit-string-processing-module-import -disable-implicit-concurrency-module-import -parse-stdlib \
6+
// RUN: -emit-module-interface-path %t/internal/B.swiftinterface -enable-library-evolution -I %t \
7+
// RUN: %t/B.swift
8+
9+
// RUN: %target-swift-frontend -emit-module -module-name A -o %t/testable/A.swiftmodule -swift-version 5 \
10+
// RUN: -disable-implicit-string-processing-module-import -disable-implicit-concurrency-module-import -parse-stdlib \
11+
// RUN: -emit-module-interface-path %t/testable/A.swiftinterface -enable-library-evolution -I %t/internal -enable-testing \
12+
// RUN: %t/A.swift
13+
14+
// RUN: %target-swift-frontend -emit-module -module-name A -o %t/regular/A.swiftmodule -swift-version 5 \
15+
// RUN: -disable-implicit-string-processing-module-import -disable-implicit-concurrency-module-import -parse-stdlib \
16+
// RUN: -emit-module-interface-path %t/regular/A.swiftinterface -enable-library-evolution -I %t/internal \
17+
// RUN: %t/A.swift
18+
19+
/// Import testable build, should use interface.
20+
// RUN: %target-swift-frontend -scan-dependencies -module-load-mode prefer-serialized -module-name Test %t/main.swift \
21+
// RUN: -disable-implicit-string-processing-module-import -disable-implicit-concurrency-module-import -parse-stdlib \
22+
// RUN: -o %t/deps1.json -I %t/testable -swift-version 5 -Rmodule-loading 2>&1 | %FileCheck %s --check-prefix DIAG
23+
// DIAG: remark: skip swiftmodule built with '-enable-testing'
24+
// RUN: %{python} %S/../CAS/Inputs/SwiftDepsExtractor.py %t/deps1.json Test directDependencies | %FileCheck %s --check-prefix TEST1
25+
// TEST1: "swift": "A"
26+
// RUN: %{python} %S/../CAS/Inputs/SwiftDepsExtractor.py %t/deps1.json A directDependencies | %FileCheck %s --check-prefix EMPTY --allow-empty
27+
// EMPTY-NOT: B
28+
29+
/// Import regular build, should use binary.
30+
// RUN: %target-swift-frontend -scan-dependencies -module-load-mode prefer-serialized -module-name Test %t/main.swift \
31+
// RUN: -disable-implicit-string-processing-module-import -disable-implicit-concurrency-module-import -parse-stdlib \
32+
// RUN: -o %t/deps2.json -I %t/regular -swift-version 5 -Rmodule-loading
33+
// RUN: %{python} %S/../CAS/Inputs/SwiftDepsExtractor.py %t/deps2.json Test directDependencies | %FileCheck %s --check-prefix TEST2
34+
// TEST2: "swiftPrebuiltExternal": "A"
35+
// RUN: %{python} %S/../CAS/Inputs/SwiftDepsExtractor.py %t/deps2.json swiftPrebuiltExternal:A directDependencies | %FileCheck %s --check-prefix EMPTY --allow-empty
36+
37+
/// Testable import testable build, should use binary, even interface is preferred.
38+
// RUN: %target-swift-frontend -scan-dependencies -module-load-mode prefer-interface -module-name Test %t/testable.swift \
39+
// RUN: -disable-implicit-string-processing-module-import -disable-implicit-concurrency-module-import -parse-stdlib -enable-testing \
40+
// RUN: -o %t/deps3.json -I %t/testable -I %t/internal -swift-version 5 -Rmodule-loading
41+
// RUN: %{python} %S/../CAS/Inputs/SwiftDepsExtractor.py %t/deps3.json Test directDependencies | %FileCheck %s --check-prefix TEST3
42+
// TEST3: "swiftPrebuiltExternal": "A"
43+
// RUN: %{python} %S/../CAS/Inputs/SwiftDepsExtractor.py %t/deps3.json swiftPrebuiltExternal:A directDependencies | %FileCheck %s --check-prefix TEST3-A
44+
// TEST3-A: "swift": "B"
45+
46+
/// Testable import non-testable build without enable testing.
47+
// RUN: %target-swift-frontend -scan-dependencies -module-load-mode prefer-interface -module-name Test %t/testable.swift \
48+
// RUN: -disable-implicit-string-processing-module-import -disable-implicit-concurrency-module-import -parse-stdlib \
49+
// RUN: -o %t/deps4.json -I %t/regular -swift-version 5
50+
// RUN: %{python} %S/../CAS/Inputs/SwiftDepsExtractor.py %t/deps4.json Test directDependencies | %FileCheck %s --check-prefix TEST4
51+
// TEST4: "swiftPrebuiltExternal": "A"
52+
// RUN: %{python} %S/../CAS/Inputs/SwiftDepsExtractor.py %t/deps4.json swiftPrebuiltExternal:A directDependencies | %FileCheck %s --check-prefix EMPTY --allow-empty
53+
54+
/// Testable import non-testable build enable testing, still succeed since swift-frontend can provide a better diagnostics when the module is actually imported.
55+
// RUN: %target-swift-frontend -scan-dependencies -module-load-mode prefer-interface -module-name Test %t/testable.swift \
56+
// RUN: -disable-implicit-string-processing-module-import -disable-implicit-concurrency-module-import -parse-stdlib -enable-testing \
57+
// RUN: -o %t/deps5.json -I %t/regular -swift-version 5 -Rmodule-loading
58+
59+
/// Regular import a testable module with no interface, this is a dependency scanning error.
60+
// RUN: rm %t/testable/A.swiftinterface
61+
// RUN: %target-swift-frontend -scan-dependencies -module-load-mode prefer-interface -module-name Test %t/main.swift \
62+
// RUN: -disable-implicit-string-processing-module-import -disable-implicit-concurrency-module-import -parse-stdlib -enable-testing \
63+
// RUN: -o %t/deps6.json -I %t/testable -swift-version 5 -Rmodule-loading 2>&1 | %FileCheck %s --check-prefix ERROR
64+
// ERROR: error: Unable to find module dependency: 'A'
65+
66+
//--- main.swift
67+
import A
68+
69+
//--- testable.swift
70+
@testable import A
71+
72+
//--- A.swift
73+
internal import B
74+
@_spi(Testing) public func a() {}
75+
76+
//--- B.swift
77+
public func b() {}
78+

0 commit comments

Comments
 (0)