Skip to content

Commit 2cb6d72

Browse files
authored
Merge pull request #61058 from xymus/spi-only-consistent
2 parents b410886 + 99d9beb commit 2cb6d72

File tree

7 files changed

+290
-49
lines changed

7 files changed

+290
-49
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -973,8 +973,11 @@ ERROR(module_not_testable,Fatal,
973973
ERROR(module_not_compiled_for_private_import,none,
974974
"module %0 was not compiled for private import", (Identifier))
975975

976-
ERROR(import_implementation_cannot_be_exported,none,
977-
"module %0 cannot be both exported and implementation-only", (Identifier))
976+
ERROR(import_restriction_conflict,none,
977+
"module %0 cannot be both "
978+
"%select{implementation-only|SPI only|exported}1 and "
979+
"%select{implementation-only|SPI only|exported}2 ",
980+
(Identifier, unsigned, unsigned))
978981

979982
WARNING(module_not_compiled_with_library_evolution,none,
980983
"module %0 was not compiled with library evolution support; "
@@ -3003,6 +3006,13 @@ WARNING(warn_implementation_only_conflict,none,
30033006
(Identifier))
30043007
NOTE(implementation_only_conflict_here,none,
30053008
"imported as implementation-only here", ())
3009+
3010+
ERROR(spi_only_import_conflict,none,
3011+
"%0 inconsistently imported for SPI only",
3012+
(Identifier))
3013+
NOTE(spi_only_import_conflict_here,none,
3014+
"imported for SPI only here", ())
3015+
30063016
ERROR(error_public_import_of_private_module,none,
30073017
"private module %0 is imported publicly from the public module %1",
30083018
(Identifier, Identifier))

include/swift/AST/TypeCheckRequests.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3289,6 +3289,25 @@ class CheckInconsistentImplementationOnlyImportsRequest
32893289
bool isCached() const { return true; }
32903290
};
32913291

3292+
/// Check that if one import in a file uses \c @_spiOnly, all imports from the
3293+
/// same file are consistently using \c @_spiOnly.
3294+
class CheckInconsistentSPIOnlyImportsRequest
3295+
: public SimpleRequest<CheckInconsistentSPIOnlyImportsRequest,
3296+
evaluator::SideEffect(SourceFile *),
3297+
RequestFlags::Cached> {
3298+
public:
3299+
using SimpleRequest::SimpleRequest;
3300+
3301+
private:
3302+
friend SimpleRequest;
3303+
3304+
evaluator::SideEffect evaluate(Evaluator &evaluator, SourceFile *mod) const;
3305+
3306+
public:
3307+
// Cached.
3308+
bool isCached() const { return true; }
3309+
};
3310+
32923311
/// Checks to see if any of the imports in a module use \c @_weakLinked
32933312
/// in one file and not in another.
32943313
///

include/swift/AST/TypeCheckerTypeIDZone.def

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ SWIFT_REQUEST(TypeChecker, CallerSideDefaultArgExprRequest,
3333
Expr *(DefaultArgumentExpr *), SeparatelyCached, NoLocationInfo)
3434
SWIFT_REQUEST(TypeChecker, CheckInconsistentImplementationOnlyImportsRequest,
3535
evaluator::SideEffect(ModuleDecl *), Cached, NoLocationInfo)
36+
SWIFT_REQUEST(TypeChecker, CheckInconsistentSPIOnlyImportsRequest,
37+
evaluator::SideEffect(SourceFile *), Cached, NoLocationInfo)
3638
SWIFT_REQUEST(TypeChecker, CheckInconsistentWeakLinkedImportsRequest,
3739
evaluator::SideEffect(ModuleDecl *), Cached, NoLocationInfo)
3840
SWIFT_REQUEST(TypeChecker, CheckRedeclarationRequest,

lib/Sema/ImportResolution.cpp

Lines changed: 136 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,11 @@ struct UnboundImport {
129129

130130
private:
131131
void validatePrivate(ModuleDecl *topLevelModule);
132-
void validateImplementationOnly(ASTContext &ctx);
132+
133+
/// Check that no import has more than one of the following modifiers:
134+
/// @_exported, @_implementationOnly, and @_spiOnly.
135+
void validateRestrictedImport(ASTContext &ctx);
136+
133137
void validateTestable(ModuleDecl *topLevelModule);
134138
void validateResilience(NullablePtr<ModuleDecl> topLevelModule,
135139
SourceFile &SF);
@@ -601,7 +605,7 @@ bool UnboundImport::checkModuleLoaded(ModuleDecl *M, SourceFile &SF) {
601605

602606
void UnboundImport::validateOptions(NullablePtr<ModuleDecl> topLevelModule,
603607
SourceFile &SF) {
604-
validateImplementationOnly(SF.getASTContext());
608+
validateRestrictedImport(SF.getASTContext());
605609

606610
if (auto *top = topLevelModule.getPtrOrNull()) {
607611
// FIXME: Having these two calls in this if condition seems dubious.
@@ -636,16 +640,62 @@ void UnboundImport::validatePrivate(ModuleDecl *topLevelModule) {
636640
import.sourceFileArg = StringRef();
637641
}
638642

639-
void UnboundImport::validateImplementationOnly(ASTContext &ctx) {
640-
if (!import.options.contains(ImportFlags::ImplementationOnly) ||
641-
!import.options.contains(ImportFlags::Exported))
643+
void UnboundImport::validateRestrictedImport(ASTContext &ctx) {
644+
static llvm::SmallVector<ImportFlags, 2> flags = {ImportFlags::Exported,
645+
ImportFlags::ImplementationOnly,
646+
ImportFlags::SPIOnly};
647+
llvm::SmallVector<ImportFlags, 2> conflicts;
648+
649+
for (auto flag : flags) {
650+
if (import.options.contains(flag))
651+
conflicts.push_back(flag);
652+
}
653+
654+
// Quit if there's no conflicting attributes.
655+
if (conflicts.size() < 2)
642656
return;
643657

644-
// Remove one flag to maintain the invariant.
645-
import.options -= ImportFlags::ImplementationOnly;
658+
// Remove all but one flag to maintain the invariant.
659+
for (auto iter = conflicts.begin(); iter != std::prev(conflicts.end()); iter ++)
660+
import.options -= *iter;
646661

647-
diagnoseInvalidAttr(DAK_ImplementationOnly, ctx.Diags,
648-
diag::import_implementation_cannot_be_exported);
662+
DeclAttrKind attrToRemove = conflicts[0] == ImportFlags::ImplementationOnly?
663+
DAK_Exported : DAK_ImplementationOnly;
664+
665+
// More dense enum with some cases of ImportFlags,
666+
// used by import_restriction_conflict.
667+
enum class ImportFlagForDiag : uint8_t {
668+
ImplementationOnly,
669+
SPIOnly,
670+
Exported
671+
};
672+
auto flagToDiag = [](ImportFlags flag) {
673+
switch (flag) {
674+
case ImportFlags::ImplementationOnly:
675+
return ImportFlagForDiag::ImplementationOnly;
676+
case ImportFlags::SPIOnly:
677+
return ImportFlagForDiag::SPIOnly;
678+
case ImportFlags::Exported:
679+
return ImportFlagForDiag::Exported;
680+
default:
681+
llvm_unreachable("Unexpected ImportFlag");
682+
}
683+
};
684+
685+
// Report the conflict, only the first two conflicts should be enough.
686+
auto diag = ctx.Diags.diagnose(import.module.getModulePath().front().Loc,
687+
diag::import_restriction_conflict,
688+
import.module.getModulePath().front().Item,
689+
(uint8_t)flagToDiag(conflicts[0]),
690+
(uint8_t)flagToDiag(conflicts[1]));
691+
692+
auto *ID = getImportDecl().getPtrOrNull();
693+
if (!ID) return;
694+
auto *attr = ID->getAttrs().getAttribute(attrToRemove);
695+
if (!attr) return;
696+
697+
diag.fixItRemove(attr->getRangeWithAt());
698+
attr->setInvalid();
649699
}
650700

651701
void UnboundImport::validateTestable(ModuleDecl *topLevelModule) {
@@ -709,54 +759,66 @@ static bool moduleHasAnyImportsMatchingFlag(ModuleDecl *mod, ImportFlags flag) {
709759
return false;
710760
}
711761

712-
/// Finds all import declarations for a single module that inconsistently match
762+
/// Finds all import declarations for a single file that inconsistently match
713763
/// \c predicate and passes each pair of inconsistent imports to \c diagnose.
714764
template <typename Pred, typename Diag>
715-
static void findInconsistentImports(ModuleDecl *mod, Pred predicate,
716-
Diag diagnose) {
717-
llvm::DenseMap<ModuleDecl *, const ImportDecl *> matchingImports;
718-
llvm::DenseMap<ModuleDecl *, std::vector<const ImportDecl *>> otherImports;
765+
static void findInconsistentImportsAcrossFile(
766+
const SourceFile *SF, Pred predicate, Diag diagnose,
767+
llvm::DenseMap<ModuleDecl *, const ImportDecl *> &matchingImports,
768+
llvm::DenseMap<ModuleDecl *, std::vector<const ImportDecl *>> &otherImports) {
769+
770+
for (auto *topLevelDecl : SF->getTopLevelDecls()) {
771+
auto *nextImport = dyn_cast<ImportDecl>(topLevelDecl);
772+
if (!nextImport)
773+
continue;
719774

720-
for (const FileUnit *file : mod->getFiles()) {
721-
auto *SF = dyn_cast<SourceFile>(file);
722-
if (!SF)
775+
ModuleDecl *module = nextImport->getModule();
776+
if (!module)
723777
continue;
724778

725-
for (auto *topLevelDecl : SF->getTopLevelDecls()) {
726-
auto *nextImport = dyn_cast<ImportDecl>(topLevelDecl);
727-
if (!nextImport)
779+
if (predicate(nextImport)) {
780+
// We found a matching import.
781+
bool isNew = matchingImports.insert({module, nextImport}).second;
782+
if (!isNew)
728783
continue;
729784

730-
ModuleDecl *module = nextImport->getModule();
731-
if (!module)
732-
continue;
785+
auto seenOtherImportPosition = otherImports.find(module);
786+
if (seenOtherImportPosition != otherImports.end()) {
787+
for (auto *seenOtherImport : seenOtherImportPosition->getSecond())
788+
diagnose(seenOtherImport, nextImport);
733789

734-
if (predicate(nextImport)) {
735-
// We found a matching import.
736-
bool isNew = matchingImports.insert({module, nextImport}).second;
737-
if (!isNew)
738-
continue;
790+
// We're done with these; keep the map small if possible.
791+
otherImports.erase(seenOtherImportPosition);
792+
}
793+
continue;
794+
}
739795

740-
auto seenOtherImportPosition = otherImports.find(module);
741-
if (seenOtherImportPosition != otherImports.end()) {
742-
for (auto *seenOtherImport : seenOtherImportPosition->getSecond())
743-
diagnose(seenOtherImport, nextImport);
796+
// We saw a non-matching import. Is that in conflict with what we've seen?
797+
if (auto *seenMatchingImport = matchingImports.lookup(module)) {
798+
diagnose(nextImport, seenMatchingImport);
799+
continue;
800+
}
744801

745-
// We're done with these; keep the map small if possible.
746-
otherImports.erase(seenOtherImportPosition);
747-
}
748-
continue;
749-
}
802+
// Otherwise, record it for later.
803+
otherImports[module].push_back(nextImport);
804+
}
805+
}
750806

751-
// We saw a non-matching import. Is that in conflict with what we've seen?
752-
if (auto *seenMatchingImport = matchingImports.lookup(module)) {
753-
diagnose(nextImport, seenMatchingImport);
754-
continue;
755-
}
807+
/// Finds all import declarations for a single module that inconsistently match
808+
/// \c predicate and passes each pair of inconsistent imports to \c diagnose.
809+
template <typename Pred, typename Diag>
810+
static void findInconsistentImportsAcrossModule(ModuleDecl *mod, Pred predicate,
811+
Diag diagnose) {
812+
llvm::DenseMap<ModuleDecl *, const ImportDecl *> matchingImports;
813+
llvm::DenseMap<ModuleDecl *, std::vector<const ImportDecl *>> otherImports;
756814

757-
// Otherwise, record it for later.
758-
otherImports[module].push_back(nextImport);
759-
}
815+
for (const FileUnit *file : mod->getFiles()) {
816+
auto *SF = dyn_cast<SourceFile>(file);
817+
if (!SF)
818+
continue;
819+
820+
findInconsistentImportsAcrossFile(SF, predicate, diagnose,
821+
matchingImports, otherImports);
760822
}
761823
}
762824

@@ -790,7 +852,34 @@ CheckInconsistentImplementationOnlyImportsRequest::evaluate(
790852
return decl->getAttrs().hasAttribute<ImplementationOnlyAttr>();
791853
};
792854

793-
findInconsistentImports(mod, predicate, diagnose);
855+
findInconsistentImportsAcrossModule(mod, predicate, diagnose);
856+
return {};
857+
}
858+
859+
evaluator::SideEffect
860+
CheckInconsistentSPIOnlyImportsRequest::evaluate(
861+
Evaluator &evaluator, SourceFile *SF) const {
862+
863+
auto mod = SF->getParentModule();
864+
auto diagnose = [mod](const ImportDecl *normalImport,
865+
const ImportDecl *spiOnlyImport) {
866+
auto &diags = mod->getDiags();
867+
{
868+
diags.diagnose(normalImport, diag::spi_only_import_conflict,
869+
normalImport->getModule()->getName());
870+
}
871+
diags.diagnose(spiOnlyImport,
872+
diag::spi_only_import_conflict_here);
873+
};
874+
875+
auto predicate = [](ImportDecl *decl) {
876+
return decl->getAttrs().hasAttribute<SPIOnlyAttr>();
877+
};
878+
879+
llvm::DenseMap<ModuleDecl *, const ImportDecl *> matchingImports;
880+
llvm::DenseMap<ModuleDecl *, std::vector<const ImportDecl *>> otherImports;
881+
findInconsistentImportsAcrossFile(SF, predicate, diagnose,
882+
matchingImports, otherImports);
794883
return {};
795884
}
796885

@@ -815,7 +904,7 @@ CheckInconsistentWeakLinkedImportsRequest::evaluate(Evaluator &evaluator,
815904
return decl->getAttrs().hasAttribute<WeakLinkedAttr>();
816905
};
817906

818-
findInconsistentImports(mod, predicate, diagnose);
907+
findInconsistentImportsAcrossModule(mod, predicate, diagnose);
819908
return {};
820909
}
821910

lib/Sema/TypeChecker.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,11 @@ TypeCheckSourceFileRequest::evaluate(Evaluator &eval, SourceFile *SF) const {
335335
CheckInconsistentImplementationOnlyImportsRequest{SF->getParentModule()},
336336
{});
337337

338+
evaluateOrDefault(
339+
Ctx.evaluator,
340+
CheckInconsistentSPIOnlyImportsRequest{SF},
341+
{});
342+
338343
evaluateOrDefault(
339344
Ctx.evaluator,
340345
CheckInconsistentWeakLinkedImportsRequest{SF->getParentModule()}, {});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/// Test duplicate and conflicting imports from the same file.
2+
3+
// RUN: %empty-directory(%t)
4+
// RUN: split-file %s %t
5+
6+
/// Generate dependencies.
7+
// RUN: %target-swift-frontend -emit-module %t/Lib.swift \
8+
// RUN: -module-name Lib -emit-module-path %t/Lib.swiftmodule \
9+
// RUN: -swift-version 5 -enable-library-evolution
10+
11+
/// Build clients.
12+
// RUN: %target-swift-frontend -typecheck %t/SPIOnly_Default.swift -I %t -verify \
13+
// RUN: -experimental-spi-only-imports -verify
14+
// RUN: %target-swift-frontend -typecheck %t/Default_SPIOnly.swift -I %t -verify \
15+
// RUN: -experimental-spi-only-imports -verify
16+
// RUN: %target-swift-frontend -typecheck %t/SPIOnly_Exported.swift -I %t -verify \
17+
// RUN: -experimental-spi-only-imports -verify
18+
// RUN: %target-swift-frontend -typecheck %t/Exported_SPIOnly.swift -I %t -verify \
19+
// RUN: -experimental-spi-only-imports -verify
20+
// RUN: %target-swift-frontend -typecheck %t/SPIOnly_IOI.swift -I %t -verify \
21+
// RUN: -experimental-spi-only-imports -verify
22+
// RUN: %target-swift-frontend -typecheck %t/SPIOnly_IOI_Exported_Default.swift -I %t -verify \
23+
// RUN: -experimental-spi-only-imports -verify
24+
// RUN: %target-swift-frontend -typecheck -primary-file %t/SPIOnly_Default_FileA.swift \
25+
// RUN: %t/SPIOnly_Default_FileB.swift -I %t -verify \
26+
// RUN: -experimental-spi-only-imports -verify
27+
// RUN: %target-swift-frontend -typecheck -primary-file %t/IOI_Default_FileA.swift \
28+
// RUN: %t/IOI_Default_FileB.swift -I %t -verify \
29+
// RUN: -experimental-spi-only-imports -verify
30+
31+
//--- Lib.swift
32+
// Empty source file for import.
33+
34+
//--- SPIOnly_Default.swift
35+
@_spiOnly import Lib // expected-note {{imported for SPI only here}}
36+
import Lib // expected-error {{'Lib' inconsistently imported for SPI only}}
37+
38+
//--- Default_SPIOnly.swift
39+
import Lib // expected-error {{'Lib' inconsistently imported for SPI only}}
40+
@_spiOnly import Lib // expected-note {{imported for SPI only here}}
41+
42+
//--- SPIOnly_Exported.swift
43+
@_spiOnly import Lib // expected-note {{imported for SPI only here}}
44+
@_exported import Lib // expected-error {{'Lib' inconsistently imported for SPI only}}
45+
46+
//--- Exported_SPIOnly.swift
47+
@_exported import Lib // expected-error {{'Lib' inconsistently imported for SPI only}}
48+
@_spiOnly import Lib // expected-note {{imported for SPI only here}}
49+
50+
//--- SPIOnly_IOI.swift
51+
@_spiOnly import Lib // expected-note {{imported for SPI only here}}
52+
// expected-warning @-1 {{'Lib' inconsistently imported as implementation-only}}
53+
@_implementationOnly import Lib // expected-error {{'Lib' inconsistently imported for SPI only}}
54+
// expected-note @-1 {{imported as implementation-only here}}
55+
56+
/// Many confliciting imports lead to many diagnostics.
57+
//--- SPIOnly_IOI_Exported_Default.swift
58+
@_spiOnly import Lib // expected-note 3 {{imported for SPI only here}}
59+
// expected-warning @-1 {{'Lib' inconsistently imported as implementation-only}}
60+
@_implementationOnly import Lib // expected-error {{'Lib' inconsistently imported for SPI only}}
61+
// expected-note @-1 3 {{imported as implementation-only here}}
62+
@_exported import Lib // expected-error {{'Lib' inconsistently imported for SPI only}}
63+
// expected-warning @-1 {{'Lib' inconsistently imported as implementation-only}}
64+
import Lib // expected-error {{'Lib' inconsistently imported for SPI only}}
65+
// expected-warning @-1 {{'Lib' inconsistently imported as implementation-only}}
66+
67+
/// Different SPI only imports in different files of the same module are accepted.
68+
//--- SPIOnly_Default_FileA.swift
69+
@_spiOnly import Lib
70+
71+
//--- SPIOnly_Default_FileB.swift
72+
import Lib
73+
74+
/// Different IOI in different files of the same module are still rejected.
75+
//--- IOI_Default_FileA.swift
76+
@_implementationOnly import Lib // expected-note {{imported as implementation-only here}}
77+
78+
//--- IOI_Default_FileB.swift
79+
import Lib // expected-warning {{'Lib' inconsistently imported as implementation-only}}

0 commit comments

Comments
 (0)