Skip to content

Commit 29f23bb

Browse files
committed
Improve diagnostics for uses of unsafe declarations in functions
Instead of producing a warning for each use of an unsafe entity, collect all of the uses of unsafe constructs within a given function and batch them together in a single diagnostic at the function level that tells you what you can do (add `@unsafe` or `@safe(unchecked)`, depending on whether all unsafe uses were in the definition), plus notes identifying every unsafe use within that declaration. The new diagnostic renderer nicely collects together in a single snippet, so it's easier to reason about. Here's an example from the embedded runtime that previously would have been 6 separate warnings, each with 1-2 notes: ``` swift/stdlib/public/core/EmbeddedRuntime.swift:397:13: warning: global function 'swift_retainCount' involves unsafe code; use '@safe(unchecked)' to assert that the code is memory-safe 395 | 396 | @_cdecl("swift_retainCount") 397 | public func swift_retainCount(object: Builtin.RawPointer) -> Int { | `- warning: global function 'swift_retainCount' involves unsafe code; use '@safe(unchecked)' to assert that the code is memory-safe 398 | if !isValidPointerForNativeRetain(object: object) { return 0 } 399 | let o = UnsafeMutablePointer<HeapObject>(object) | | `- note: call to unsafe initializer 'init(_:)' | `- note: reference to unsafe generic struct 'UnsafeMutablePointer' 400 | let refcount = refcountPointer(for: o) | | `- note: reference to let 'o' involves unsafe type 'UnsafeMutablePointer<HeapObject>' | `- note: call to global function 'refcountPointer(for:)' involves unsafe type 'UnsafeMutablePointer<Int>' 401 | return loadAcquire(refcount) & HeapObject.refcountMask | | `- note: reference to let 'refcount' involves unsafe type 'UnsafeMutablePointer<Int>' | `- note: call to global function 'loadAcquire' involves unsafe type 'UnsafeMutablePointer<Int>' 402 | } 403 | ``` Note that we have lost a little bit of information, because we no longer produce "unsafe declaration was here" notes pointing back at things like `UnsafeMutablePointer` or `recountPointer(for:)`. However, strict memory safety tends to be noisy to turn on, so it's worth losing a little bit of easily-recovered information to gain some brevity.
1 parent b3763ee commit 29f23bb

15 files changed

+231
-55
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8072,6 +8072,19 @@ NOTE(sending_function_result_with_sending_param_note, none,
80728072
//------------------------------------------------------------------------------
80738073
ERROR(unsafe_attr_disabled,none,
80748074
"attribute requires '-enable-experimental-feature AllowUnsafeAttribute'", ())
8075+
8076+
GROUPED_WARNING(decl_involves_unsafe,Unsafe,none,
8077+
"%kindbase0 involves unsafe code; "
8078+
"use %select{'@unsafe' to indicate that its use is not memory-safe|"
8079+
"'@safe(unchecked)' to assert that the code is memory-safe}1",
8080+
(const Decl *, bool))
8081+
NOTE(note_reference_to_unsafe_decl,none,
8082+
"%select{reference|call}0 to unsafe %kind1",
8083+
(bool, const ValueDecl *))
8084+
NOTE(note_reference_to_unsafe_typed_decl,none,
8085+
"%select{reference|call}0 to %kind1 involves unsafe type %2",
8086+
(bool, const ValueDecl *, Type))
8087+
80758088
GROUPED_WARNING(override_safe_withunsafe,Unsafe,none,
80768089
"override of safe %0 with unsafe %0", (DescriptiveDeclKind))
80778090
GROUPED_WARNING(witness_unsafe,Unsafe,none,

include/swift/AST/SourceFile.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
namespace swift {
2828

2929
class PersistentParserState;
30+
struct SourceFileExtras;
3031

3132
/// Kind of import affecting how a decl can be reexported.
3233
///
@@ -243,6 +244,9 @@ class SourceFile final : public FileUnit {
243244
/// Storage for \c HasImportsMatchingFlagRequest.
244245
ImportOptions cachedImportOptions;
245246

247+
/// Extra information for the source file, allocated as needed.
248+
SourceFileExtras *extras = nullptr;
249+
246250
friend ASTContext;
247251

248252
public:
@@ -368,6 +372,11 @@ class SourceFile final : public FileUnit {
368372
/// \c #sourceLocation(file:) declarations.
369373
llvm::StringMap<SourceFilePathInfo> getInfoForUsedFilePaths() const;
370374

375+
/// Retrieve "extra" information associated with this source file, which is
376+
/// lazily and separately constructed. Use this for scratch information
377+
/// that isn't needed for all source files.
378+
SourceFileExtras &getExtras() const;
379+
371380
SourceFile(ModuleDecl &M, SourceFileKind K, unsigned bufferID,
372381
ParsingOptions parsingOpts = {}, bool isPrimary = false);
373382

include/swift/AST/SourceFileExtras.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//===--- SourceFileExtras.h - Extra data for a source file ------*- C++ -*-===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#ifndef SWIFT_AST_SOURCEFILEEXTRAS_H
14+
#define SWIFT_AST_SOURCEFILEEXTRAS_H
15+
16+
#include "swift/AST/UnsafeUse.h"
17+
#include "llvm/ADT/DenseMap.h"
18+
#include <vector>
19+
20+
namespace swift {
21+
22+
class Decl;
23+
24+
/// Extra information associated with a source file that is lazily created and
25+
/// stored in a separately-allocated side structure.
26+
struct SourceFileExtras {
27+
/// Captures all of the unsafe uses associated with a given declaration.
28+
///
29+
/// The declaration is the entity that can be annotated (e.g., with @unsafe)
30+
/// to suppress all of the unsafe-related diagnostics listed here.
31+
llvm::DenseMap<const Decl *, std::vector<UnsafeUse>> unsafeUses;
32+
};
33+
34+
}
35+
36+
#endif // SWIFT_AST_SOURCEFILEEXTRAS_H

include/swift/AST/UnsafeUse.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class UnsafeUse {
6767

6868
struct {
6969
NormalProtocolConformance *conformance;
70+
DeclContext *declContext;
7071
const void *location;
7172
} conformance;
7273

@@ -139,9 +140,11 @@ class UnsafeUse {
139140
}
140141

141142
static UnsafeUse forConformance(NormalProtocolConformance *conformance,
142-
SourceLoc location) {
143+
SourceLoc location,
144+
DeclContext *dc) {
143145
UnsafeUse result(UnsafeConformance);
144146
result.storage.conformance.conformance = conformance;
147+
result.storage.conformance.declContext = dc;
145148
result.storage.conformance.location = location.getOpaquePointerValue();
146149
return result;
147150
}
@@ -230,10 +233,14 @@ class UnsafeUse {
230233
return storage.entity.declContext;
231234

232235
case Override:
236+
return getDecl()->getDeclContext();
237+
233238
case Witness:
234239
case TypeWitness:
240+
return getConformance()->getDeclContext();
241+
235242
case UnsafeConformance:
236-
return nullptr;
243+
return storage.conformance.declContext;
237244
}
238245
}
239246

lib/AST/Module.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
#include "swift/AST/PrintOptions.h"
4242
#include "swift/AST/ProtocolConformance.h"
4343
#include "swift/AST/SourceFile.h"
44+
#include "swift/AST/SourceFileExtras.h"
4445
#include "swift/AST/SynthesizedFileUnit.h"
4546
#include "swift/AST/Type.h"
4647
#include "swift/AST/TypeCheckRequests.h"
@@ -1175,6 +1176,17 @@ ASTNode SourceFile::getNodeInEnclosingSourceFile() const {
11751176
return ASTNode::getFromOpaqueValue(getGeneratedSourceFileInfo()->astNode);
11761177
}
11771178

1179+
SourceFileExtras &SourceFile::getExtras() const {
1180+
if (!extras) {
1181+
const_cast<SourceFile *>(this)->extras = new SourceFileExtras();
1182+
getASTContext().addCleanup([extras=this->extras] {
1183+
delete extras;
1184+
});
1185+
}
1186+
1187+
return *extras;
1188+
}
1189+
11781190
void ModuleDecl::lookupClassMember(ImportPath::Access accessPath,
11791191
DeclName name,
11801192
SmallVectorImpl<ValueDecl*> &results) const {

lib/Sema/TypeCheckAvailability.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,12 @@ std::pair<const Decl *, bool /*inDefinition*/>
274274
enclosingContextForUnsafe(SourceLoc referenceLoc, const DeclContext *referenceDC);
275275

276276
/// Diagnose the given unsafe use right now.
277-
void diagnoseUnsafeUse(const UnsafeUse &use);
277+
void diagnoseUnsafeUse(const UnsafeUse &use, bool asNote = false);
278+
279+
/// Diagnose any unsafe uses within the signature or definition of the given
280+
/// declaration, if there are any.
281+
void diagnoseUnsafeUsesIn(const Decl *decl);
282+
278283

279284
} // namespace swift
280285

lib/Sema/TypeCheckProtocol.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2469,7 +2469,8 @@ checkIndividualConformance(NormalProtocolConformance *conformance) {
24692469

24702470
if (!conformance->getDeclContext()->allowsUnsafe()) {
24712471
diagnoseUnsafeUse(
2472-
UnsafeUse::forConformance(conformance, ComplainLoc));
2472+
UnsafeUse::forConformance(conformance, ComplainLoc,
2473+
conformance->getDeclContext()));
24732474
}
24742475
}
24752476

lib/Sema/TypeCheckStmt.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2949,6 +2949,8 @@ TypeCheckFunctionBodyRequest::evaluate(Evaluator &eval,
29492949
TypeChecker::checkFunctionEffects(AFD);
29502950
}
29512951

2952+
diagnoseUnsafeUsesIn(AFD);
2953+
29522954
return hadError ? errorBody() : body;
29532955
}
29542956

lib/Sema/TypeCheckUnsafe.cpp

Lines changed: 102 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,40 @@
1717
#include "swift/AST/ASTContext.h"
1818
#include "swift/AST/UnsafeUse.h"
1919
#include "swift/AST/DiagnosticsSema.h"
20+
#include "swift/AST/SourceFile.h"
21+
#include "swift/AST/SourceFileExtras.h"
2022
#include "TypeCheckAvailability.h"
2123
#include "TypeCheckType.h"
2224

2325
using namespace swift;
2426

27+
static std::pair<const Decl *, bool /*inDefinition*/>
28+
enclosingContextForUnsafe(const UnsafeUse &use) {
29+
return swift::enclosingContextForUnsafe(use.getLocation(),
30+
use.getDeclContext());
31+
}
32+
33+
/// Whether this particular unsafe use occurs within the definition of an
34+
/// entity, but not in its signature, meaning that the unsafety could be
35+
/// encapsulated.
36+
static bool isUnsafeUseInDefinition(const UnsafeUse &use) {
37+
switch (use.getKind()) {
38+
case UnsafeUse::Override:
39+
case UnsafeUse::Witness:
40+
case UnsafeUse::TypeWitness:
41+
case UnsafeUse::UnsafeConformance:
42+
case UnsafeUse::UnownedUnsafe:
43+
case UnsafeUse::NonisolatedUnsafe:
44+
// Never part of the definition. These are always part of the interface.
45+
return false;
46+
47+
case UnsafeUse::ReferenceToUnsafe:
48+
case UnsafeUse::CallToUnsafe:
49+
return enclosingContextForUnsafe(use).second;
50+
}
51+
}
52+
53+
2554
static void suggestUnsafeOnEnclosingDecl(
2655
SourceLoc referenceLoc, const DeclContext *referenceDC) {
2756
const Decl *decl = nullptr;
@@ -58,7 +87,39 @@ static void suggestUnsafeMarkerOnConformance(
5887
).fixItInsert(decl->getAttributeInsertionLoc(false), "@unsafe ");
5988
}
6089

61-
void swift::diagnoseUnsafeUse(const UnsafeUse &use) {
90+
static bool shouldDeferUnsafeUseIn(const Decl *decl) {
91+
if (auto func = dyn_cast_or_null<AbstractFunctionDecl>(decl)) {
92+
if (func->hasBody()) {
93+
return true;
94+
}
95+
}
96+
97+
return false;
98+
}
99+
100+
/// Retrieve the extra information
101+
static SourceFileExtras &getSourceFileExtrasFor(const Decl *decl) {
102+
auto dc = decl->getDeclContext();
103+
return dc->getOutermostParentSourceFile()->getExtras();
104+
}
105+
106+
void swift::diagnoseUnsafeUse(const UnsafeUse &use, bool asNote) {
107+
if (!asNote) {
108+
// If we can associate this unsafe use within a particular declaration, do so.
109+
// It will be diagnosed later, along with all other unsafe uses within this
110+
// same declaration.
111+
if (use.getKind() == UnsafeUse::ReferenceToUnsafe ||
112+
use.getKind() == UnsafeUse::CallToUnsafe) {
113+
auto [enclosingDecl, _] = enclosingContextForUnsafe(
114+
use.getLocation(), use.getDeclContext());
115+
if (enclosingDecl && shouldDeferUnsafeUseIn(enclosingDecl)) {
116+
getSourceFileExtrasFor(enclosingDecl).unsafeUses[enclosingDecl]
117+
.push_back(use);
118+
return;
119+
}
120+
}
121+
}
122+
62123
switch (use.getKind()) {
63124
case UnsafeUse::Override: {
64125
auto override = use.getDecl();
@@ -142,18 +203,52 @@ void swift::diagnoseUnsafeUse(const UnsafeUse &use) {
142203
use.getDeclContext(),
143204
[&](Type specificType) {
144205
ctx.Diags.diagnose(
145-
loc, diag::reference_to_unsafe_typed_decl, isCall, decl,
146-
specificType);
206+
loc,
207+
asNote ? diag::note_reference_to_unsafe_typed_decl
208+
: diag::reference_to_unsafe_typed_decl,
209+
isCall, decl, specificType);
147210
});
148-
suggestUnsafeOnEnclosingDecl(loc, use.getDeclContext());
149-
decl->diagnose(diag::unsafe_decl_here, decl);
150211
} else {
151-
ctx.Diags
152-
.diagnose(loc, diag::reference_to_unsafe_decl, isCall, decl);
212+
ctx.Diags.diagnose(
213+
loc,
214+
asNote ? diag::note_reference_to_unsafe_decl
215+
: diag::reference_to_unsafe_decl,
216+
isCall, decl);
217+
}
218+
219+
if (!asNote) {
153220
suggestUnsafeOnEnclosingDecl(loc, use.getDeclContext());
154221
decl->diagnose(diag::unsafe_decl_here, decl);
155222
}
223+
156224
return;
157225
}
158226
}
159227
}
228+
229+
void swift::diagnoseUnsafeUsesIn(const Decl *decl) {
230+
auto &extras = getSourceFileExtrasFor(decl);
231+
auto known = extras.unsafeUses.find(decl);
232+
if (known == extras.unsafeUses.end())
233+
return;
234+
235+
// Take the unsafe uses.
236+
auto unsafeUses = std::move(known->second);
237+
extras.unsafeUses.erase(known);
238+
if (unsafeUses.empty())
239+
return;
240+
241+
// Determine whether all of the uses are in the definition.
242+
bool canEncapsulateUnsafety = std::all_of(
243+
unsafeUses.begin(), unsafeUses.end(), isUnsafeUseInDefinition);
244+
StringRef fixItStr = canEncapsulateUnsafety
245+
? "@safe(unchecked) "
246+
: "@unsafe ";
247+
decl->diagnose(diag::decl_involves_unsafe, decl, canEncapsulateUnsafety)
248+
.fixItInsert(decl->getAttributeInsertionLoc(/*forModifier=*/false),
249+
fixItStr);
250+
251+
for (const auto &unsafeUse : unsafeUses) {
252+
diagnoseUnsafeUse(unsafeUse, /*asNote=*/true);
253+
}
254+
}

test/Interop/Cxx/class/safe-interop-mode.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,22 @@ using SpanOfIntAlias = SpanOfInt;
6060
import Test
6161
import CoreFoundation
6262

63-
// expected-note@+1{{make global function 'useUnsafeParam' @unsafe to indicate that its use is not memory-safe}}{{1-1=@unsafe }}
64-
func useUnsafeParam(x: Unannotated) { // expected-warning{{reference to unsafe struct 'Unannotated'}}
63+
// expected-warning@+1{{global function 'useUnsafeParam' involves unsafe code; use '@unsafe' to indicate that its use is not memory-safe}}{{1-1=@unsafe }}
64+
func useUnsafeParam(x: Unannotated) { // expected-note{{reference to unsafe struct 'Unannotated'}}
6565
}
6666

67-
// expected-note@+2{{make global function 'useUnsafeParam2' @unsafe to indicate that its use is not memory-safe}}{{10:1-1=@unsafe }}
67+
// expected-warning@+2{{global function 'useUnsafeParam2' involves unsafe code; use '@unsafe' to indicate that its use is not memory-safe}}{{10:1-1=@unsafe }}
6868
@available(SwiftStdlib 5.8, *)
69-
func useUnsafeParam2(x: UnsafeReference) { // expected-warning{{reference to unsafe class 'UnsafeReference'}}
69+
func useUnsafeParam2(x: UnsafeReference) { // expected-note{{reference to unsafe class 'UnsafeReference'}}
7070
}
7171

72-
// expected-note@+1{{make global function 'useUnsafeParam3' @unsafe to indicate that its use is not memory-safe}}{{1-1=@unsafe }}
73-
func useUnsafeParam3(x: UnknownEscapabilityAggregate) { // expected-warning{{reference to unsafe struct 'UnknownEscapabilityAggregate'}}
72+
// expected-warning@+1{{global function 'useUnsafeParam3' involves unsafe code; use '@unsafe' to indicate that its use is not memory-safe}}{{1-1=@unsafe }}
73+
func useUnsafeParam3(x: UnknownEscapabilityAggregate) { // expected-note{{reference to unsafe struct 'UnknownEscapabilityAggregate'}}
7474
}
7575

76-
// expected-note@+1{{make global function 'useSafeParams' @safe(unchecked) to allow it to use unsafe constructs in its definition}}{{1-1=@safe(unchecked) }}
76+
// expected-warning@+1{{global function 'useSafeParams' involves unsafe code; use '@safe(unchecked)' to assert that the code is memory-safe}}{{1-1=@safe(unchecked) }}
7777
func useSafeParams(x: Owner, y: View, z: SafeEscapableAggregate, c: MyContainer) {
78-
let _ = c.__beginUnsafe() // expected-warning{{call to unsafe instance method '__beginUnsafe'}}
78+
let _ = c.__beginUnsafe() // expected-note{{call to unsafe instance method '__beginUnsafe()'}}
7979
}
8080

8181
func useCfType(x: CFArray) {

0 commit comments

Comments
 (0)