Skip to content

Commit c2ecb87

Browse files
authored
Warn when using '@_implementationOnly' inconsistently in a module (#24828)
I thought it would be useful to allow some uses of a module to be '@_implementationOnly' and others to not be in case someone wanted to change from one to the other gradually, but it turns out that if you're trying to /make/ an import implementation-only, you want to know everywhere you used it. rdar://problem/50748157 (cherry picked from commit 9647483)
1 parent c4d473f commit c2ecb87

File tree

10 files changed

+132
-4
lines changed

10 files changed

+132
-4
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2438,6 +2438,12 @@ ERROR(assoc_conformance_from_implementation_only_module,none,
24382438
"%4); %2 has been imported as implementation-only",
24392439
(Type, DeclName, Identifier, Type, Type))
24402440

2441+
WARNING(warn_implementation_only_conflict,none,
2442+
"%0 inconsistently imported as implementation-only",
2443+
(Identifier))
2444+
NOTE(implementation_only_conflict_here,none,
2445+
"imported as implementation-only here", ())
2446+
24412447
// Derived conformances
24422448
ERROR(cannot_synthesize_init_in_extension_of_nonfinal,none,
24432449
"implementation of %0 for non-final class cannot be automatically "

include/swift/Subsystems.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,14 @@ namespace swift {
216216
/// emitted.
217217
void performWholeModuleTypeChecking(SourceFile &SF);
218218

219+
/// Checks to see if any of the imports in \p M use `@_implementationOnly` in
220+
/// one file and not in another.
221+
///
222+
/// Like redeclaration checking, but for imports. This isn't part of
223+
/// swift::performWholeModuleTypeChecking because it's linear in the number
224+
/// of declarations in the module.
225+
void checkInconsistentImplementationOnlyImports(ModuleDecl *M);
226+
219227
/// Incrementally type-check only added external definitions.
220228
void typeCheckExternalDefinitions(SourceFile &SF);
221229

lib/Frontend/Frontend.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,8 @@ void CompilerInstance::finishTypeChecking(
990990
performWholeModuleTypeChecking(SF);
991991
});
992992
}
993+
994+
checkInconsistentImplementationOnlyImports(MainModule);
993995
}
994996

995997
SourceFile *CompilerInstance::createSourceFileForMainModule(

lib/Sema/TypeChecker.cpp

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,81 @@ void swift::performWholeModuleTypeChecking(SourceFile &SF) {
592592
#endif
593593
}
594594

595+
void swift::checkInconsistentImplementationOnlyImports(ModuleDecl *MainModule) {
596+
bool hasAnyImplementationOnlyImports =
597+
llvm::any_of(MainModule->getFiles(), [](const FileUnit *F) -> bool {
598+
auto *SF = dyn_cast<SourceFile>(F);
599+
return SF && SF->hasImplementationOnlyImports();
600+
});
601+
if (!hasAnyImplementationOnlyImports)
602+
return;
603+
604+
auto diagnose = [MainModule](const ImportDecl *normalImport,
605+
const ImportDecl *implementationOnlyImport) {
606+
auto &diags = MainModule->getDiags();
607+
{
608+
InFlightDiagnostic warning =
609+
diags.diagnose(normalImport, diag::warn_implementation_only_conflict,
610+
normalImport->getModule()->getName());
611+
if (normalImport->getAttrs().isEmpty()) {
612+
// Only try to add a fix-it if there's no other annotations on the
613+
// import to avoid creating things like
614+
// `@_implementationOnly @_exported import Foo`. The developer can
615+
// resolve those manually.
616+
warning.fixItInsert(normalImport->getStartLoc(),
617+
"@_implementationOnly ");
618+
}
619+
}
620+
diags.diagnose(implementationOnlyImport,
621+
diag::implementation_only_conflict_here);
622+
};
623+
624+
llvm::DenseMap<ModuleDecl *, std::vector<const ImportDecl *>> normalImports;
625+
llvm::DenseMap<ModuleDecl *, const ImportDecl *> implementationOnlyImports;
626+
627+
for (const FileUnit *file : MainModule->getFiles()) {
628+
auto *SF = dyn_cast<SourceFile>(file);
629+
if (!SF)
630+
continue;
631+
632+
for (auto *topLevelDecl : SF->Decls) {
633+
auto *nextImport = dyn_cast<ImportDecl>(topLevelDecl);
634+
if (!nextImport)
635+
continue;
636+
637+
ModuleDecl *module = nextImport->getModule();
638+
if (nextImport->getAttrs().hasAttribute<ImplementationOnlyAttr>()) {
639+
// We saw an implementation-only import.
640+
bool isNew =
641+
implementationOnlyImports.insert({module, nextImport}).second;
642+
if (!isNew)
643+
continue;
644+
645+
auto seenNormalImportPosition = normalImports.find(module);
646+
if (seenNormalImportPosition != normalImports.end()) {
647+
for (auto *seenNormalImport : seenNormalImportPosition->getSecond())
648+
diagnose(seenNormalImport, nextImport);
649+
650+
// We're done with these; keep the map small if possible.
651+
normalImports.erase(seenNormalImportPosition);
652+
}
653+
continue;
654+
}
655+
656+
// We saw a non-implementation-only import. Is that in conflict with what
657+
// we've seen?
658+
if (auto *seenImplementationOnlyImport =
659+
implementationOnlyImports.lookup(module)) {
660+
diagnose(nextImport, seenImplementationOnlyImport);
661+
continue;
662+
}
663+
664+
// Otherwise, record it for later.
665+
normalImports[module].push_back(nextImport);
666+
}
667+
}
668+
}
669+
595670
bool swift::performTypeLocChecking(ASTContext &Ctx, TypeLoc &T,
596671
DeclContext *DC,
597672
bool ProduceDiagnostics) {

test/ParseableInterface/Inputs/imports-other.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ import A
22
import B.B3
33
import D
44

5-
import NotSoSecret
6-
@_implementationOnly import NotSoSecret2
5+
import NotSoSecret // expected-warning {{'NotSoSecret' inconsistently imported as implementation-only}}
6+
@_implementationOnly import NotSoSecret2 // expected-note {{imported as implementation-only here}}

test/ParseableInterface/imports.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import func C.c // expected-warning {{scoped imports are not yet supported in mo
99
import D
1010
@_implementationOnly import Secret_BAD
1111

12-
@_implementationOnly import NotSoSecret
13-
import NotSoSecret2
12+
@_implementationOnly import NotSoSecret // expected-note {{imported as implementation-only here}}
13+
import NotSoSecret2 // expected-warning {{'NotSoSecret2' inconsistently imported as implementation-only}}
1414

1515
// CHECK-NOT: import
1616
// CHECK: {{^}}import A{{$}}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import NotSoSecret // expected-warning {{'NotSoSecret' inconsistently imported as implementation-only}}
2+
@_implementationOnly import NotSoSecret2 // expected-note 2 {{imported as implementation-only here}}
3+
import NotSoSecret3 // expected-warning {{'NotSoSecret3' inconsistently imported as implementation-only}}
4+
5+
@_implementationOnly import ActuallySecret // no-warning
6+
import ActuallyOkay // no-warning
7+
8+
@_implementationOnly import Contradictory // expected-note {{imported as implementation-only here}}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@_implementationOnly import NotSoSecret
2+
import NotSoSecret2 // expected-warning {{'NotSoSecret2' inconsistently imported as implementation-only}}
3+
@_implementationOnly import NotSoSecret3 // expected-note 2 {{imported as implementation-only here}}
4+
5+
@_implementationOnly import ActuallySecret // no-warning
6+
import ActuallyOkay // no-warning
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module NotSoSecret {}
2+
module NotSoSecret2 {}
3+
module NotSoSecret3 {}
4+
5+
module ActuallySecret {}
6+
module ActuallyOkay {}
7+
8+
module Contradictory {}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Check that the diagnostics are produced regardless of what primary file we're using.
2+
// RUN: %target-swift-frontend -typecheck -primary-file %s %S/Inputs/inconsistent-implementation-only/2.swift %S/Inputs/inconsistent-implementation-only/3.swift -I %S/Inputs/inconsistent-implementation-only/ -verify
3+
// RUN: %target-swift-frontend -typecheck %s -primary-file %S/Inputs/inconsistent-implementation-only/2.swift %S/Inputs/inconsistent-implementation-only/3.swift -I %S/Inputs/inconsistent-implementation-only/ -verify
4+
// RUN: %target-swift-frontend -typecheck %s %S/Inputs/inconsistent-implementation-only/2.swift -primary-file %S/Inputs/inconsistent-implementation-only/3.swift -I %S/Inputs/inconsistent-implementation-only/ -verify
5+
// RUN: %target-swift-frontend -typecheck -primary-file %s -primary-file %S/Inputs/inconsistent-implementation-only/2.swift -primary-file %S/Inputs/inconsistent-implementation-only/3.swift -I %S/Inputs/inconsistent-implementation-only/ -verify
6+
// RUN: %target-swift-frontend -typecheck %s %S/Inputs/inconsistent-implementation-only/2.swift %S/Inputs/inconsistent-implementation-only/3.swift -I %S/Inputs/inconsistent-implementation-only/ -verify
7+
8+
@_implementationOnly import NotSoSecret // expected-note {{imported as implementation-only here}}
9+
import NotSoSecret2 // expected-warning {{'NotSoSecret2' inconsistently imported as implementation-only}} {{1-1=@_implementationOnly }}
10+
import NotSoSecret3 // expected-warning {{'NotSoSecret3' inconsistently imported as implementation-only}} {{1-1=@_implementationOnly }}
11+
12+
@_implementationOnly import ActuallySecret // no-warning
13+
import ActuallyOkay // no-warning
14+
15+
@_exported import Contradictory // expected-warning {{'Contradictory' inconsistently imported as implementation-only}} {{none}}

0 commit comments

Comments
 (0)