Skip to content

Commit 9647483

Browse files
authored
Warn when using '@_implementationOnly' inconsistently in a module (swiftlang#24800)
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
1 parent 5191b03 commit 9647483

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
@@ -2447,6 +2447,12 @@ ERROR(assoc_conformance_from_implementation_only_module,none,
24472447
"%4); %2 has been imported as implementation-only",
24482448
(Type, DeclName, Identifier, Type, Type))
24492449

2450+
WARNING(warn_implementation_only_conflict,none,
2451+
"%0 inconsistently imported as implementation-only",
2452+
(Identifier))
2453+
NOTE(implementation_only_conflict_here,none,
2454+
"imported as implementation-only here", ())
2455+
24502456
// Derived conformances
24512457
ERROR(cannot_synthesize_init_in_extension_of_nonfinal,none,
24522458
"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
@@ -573,6 +573,81 @@ void swift::performWholeModuleTypeChecking(SourceFile &SF) {
573573
#endif
574574
}
575575

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