Skip to content

Commit 18008f2

Browse files
committed
Provide more Fix-It guidance for concurrency-unsafe global variables (SE-0412)
When diagnosing a concurrency-unsafe global or static variable, provide Fix-Its with specific guidance and advice. This is intended to aid the workflow for folks enabling strict concurrency checking or Swift 6. There are up to three Fix-Its attached to a diagnostic about concurrency-unsafe global/static variables: * convert 'global' to a 'let' constant to make the shared state immutable, which replaces `var` with `let` * restrict 'global' to the main actor if it will only be accessed from the main thread, which adds `@MainActor` * unsafely mark %0 as concurrency-safe if all accesses are protected by an external synchronization mechanism, which adds `nonisolated(unsafe)` I fretted over two things before deciding on this path: 1. For the second note, the reality is that any global actor will suffice, but `@MainActor` is orders of magnitude more common than any other global actor, so "common case convenience" wins over "precise but less useful. 2. For the third note, `nonisolated(unsafe)` should only be used sparingly, and surfacing it via Fix-It could cause overuse. However, developers need to know about it, and this is how we do that. It comes last in the list of notes (after the better options) and says "unsafe" in not one but two places. (cherry picked from commit d4ce618)
1 parent 52d6435 commit 18008f2

13 files changed

+102
-52
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5571,16 +5571,17 @@ ERROR(shared_mutable_state_access,none,
55715571
ERROR(shared_mutable_state_decl,none,
55725572
"%kind0 is not concurrency-safe because it is non-isolated global shared "
55735573
"mutable state", (const ValueDecl *))
5574-
NOTE(shared_mutable_state_decl_note,none,
5575-
"isolate %0 to a global actor, or convert it to a 'let' constant and "
5576-
"conform it to 'Sendable'", (const ValueDecl *))
55775574
ERROR(shared_immutable_state_decl,none,
55785575
"%kind1 is not concurrency-safe because non-'Sendable' type %0 may have "
55795576
"shared mutable state",
55805577
(Type, const ValueDecl *))
5581-
NOTE(shared_immutable_state_decl_note,none,
5582-
"isolate %0 to a global actor, or conform %1 to 'Sendable'",
5583-
(const ValueDecl *, Type))
5578+
NOTE(shared_state_make_immutable,none,
5579+
"convert %0 to a 'let' constant to make the shared state immutable",
5580+
(const ValueDecl *))
5581+
NOTE(shared_state_main_actor_node,none,
5582+
"restrict %0 to the main actor if it will only be accessed from the main thread", (const ValueDecl *))
5583+
NOTE(shared_state_nonisolated_unsafe,none,
5584+
"unsafely mark %0 as concurrency-safe if all accesses are protected by an external synchronization mechanism", (const ValueDecl *))
55845585
ERROR(actor_isolated_witness,none,
55855586
"%select{|distributed }0%1 %kind2 cannot be used to satisfy %3 protocol "
55865587
"requirement",

lib/Sema/MiscDiagnostics.cpp

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3374,6 +3374,28 @@ class ReturnTypePlaceholderReplacer : public ASTWalker {
33743374

33753375
} // end anonymous namespace
33763376

3377+
SourceLoc swift::getFixItLocForVarToLet(VarDecl *var) {
3378+
// Try to find the location of the 'var' so we can produce a fixit. If
3379+
// this is a simple PatternBinding, use its location.
3380+
if (auto *PBD = var->getParentPatternBinding()) {
3381+
if (PBD->getSingleVar() == var)
3382+
return PBD->getLoc();
3383+
} else if (auto *pattern = var->getParentPattern()) {
3384+
BindingPattern *foundVP = nullptr;
3385+
pattern->forEachNode([&](Pattern *P) {
3386+
if (auto *VP = dyn_cast<BindingPattern>(P))
3387+
if (VP->getSingleVar() == var)
3388+
foundVP = VP;
3389+
});
3390+
3391+
if (foundVP && foundVP->getIntroducer() != VarDecl::Introducer::Let) {
3392+
return foundVP->getLoc();
3393+
}
3394+
}
3395+
3396+
return SourceLoc();
3397+
}
3398+
33773399
// After we have scanned the entire region, diagnose variables that could be
33783400
// declared with a narrower usage kind.
33793401
VarDeclUsageChecker::~VarDeclUsageChecker() {
@@ -3593,25 +3615,7 @@ VarDeclUsageChecker::~VarDeclUsageChecker() {
35933615
// Don't warn if we have something like "let (x,y) = ..." and 'y' was
35943616
// never mutated, but 'x' was.
35953617
!isVarDeclPartOfPBDThatHadSomeMutation(var)) {
3596-
SourceLoc FixItLoc;
3597-
3598-
// Try to find the location of the 'var' so we can produce a fixit. If
3599-
// this is a simple PatternBinding, use its location.
3600-
if (auto *PBD = var->getParentPatternBinding()) {
3601-
if (PBD->getSingleVar() == var)
3602-
FixItLoc = PBD->getLoc();
3603-
} else if (auto *pattern = var->getParentPattern()) {
3604-
BindingPattern *foundVP = nullptr;
3605-
pattern->forEachNode([&](Pattern *P) {
3606-
if (auto *VP = dyn_cast<BindingPattern>(P))
3607-
if (VP->getSingleVar() == var)
3608-
foundVP = VP;
3609-
});
3610-
3611-
if (foundVP && foundVP->getIntroducer() != VarDecl::Introducer::Let) {
3612-
FixItLoc = foundVP->getLoc();
3613-
}
3614-
}
3618+
SourceLoc FixItLoc = getFixItLocForVarToLet(var);
36153619

36163620
// If this is a parameter explicitly marked 'var', remove it.
36173621
if (FixItLoc.isInvalid()) {

lib/Sema/MiscDiagnostics.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ namespace swift {
7070
AccessLevel desiredAccess, bool isForSetter = false,
7171
bool shouldUseDefaultAccess = false);
7272

73+
/// Compute the location of the 'var' keyword for a 'var'-to-'let' Fix-It.
74+
SourceLoc getFixItLocForVarToLet(VarDecl *var);
75+
7376
/// Describes the context of a parameter, for use in diagnosing argument
7477
/// label problems.
7578
enum class ParameterContext : unsigned {

lib/Sema/TypeCheckConcurrency.cpp

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// This file implements type checking support for Swift's concurrency model.
1414
//
1515
//===----------------------------------------------------------------------===//
16+
#include "MiscDiagnostics.h"
1617
#include "TypeCheckConcurrency.h"
1718
#include "TypeCheckDistributed.h"
1819
#include "TypeCheckInvertible.h"
@@ -5028,23 +5029,41 @@ ActorIsolation ActorIsolationRequest::evaluate(
50285029
if (auto *originalVar = var->getOriginalWrappedProperty()) {
50295030
diagVar = originalVar;
50305031
}
5032+
5033+
bool diagnosed = false;
50315034
if (var->isLet()) {
50325035
auto type = var->getInterfaceType();
5033-
bool diagnosed = diagnoseIfAnyNonSendableTypes(
5036+
diagnosed = diagnoseIfAnyNonSendableTypes(
50345037
type, SendableCheckContext(var->getDeclContext()),
50355038
/*inDerivedConformance=*/Type(), /*typeLoc=*/SourceLoc(),
50365039
/*diagnoseLoc=*/var->getLoc(),
50375040
diag::shared_immutable_state_decl, diagVar);
5038-
5039-
// If we diagnosed this 'let' as non-Sendable, tack on a note
5040-
// to suggest a course of action.
5041-
if (diagnosed)
5042-
diagVar->diagnose(diag::shared_immutable_state_decl_note,
5043-
diagVar, type);
50445041
} else {
50455042
diagVar->diagnose(diag::shared_mutable_state_decl, diagVar)
50465043
.warnUntilSwiftVersion(6);
5047-
diagVar->diagnose(diag::shared_mutable_state_decl_note, diagVar);
5044+
diagnosed = true;
5045+
}
5046+
5047+
// If we diagnosed this global, tack on notes to suggest potential
5048+
// courses of action.
5049+
if (diagnosed) {
5050+
if (!var->isLet()) {
5051+
auto diag = diagVar->diagnose(diag::shared_state_make_immutable,
5052+
diagVar);
5053+
SourceLoc fixItLoc = getFixItLocForVarToLet(diagVar);
5054+
if (fixItLoc.isValid()) {
5055+
diag.fixItReplace(fixItLoc, "let");
5056+
}
5057+
}
5058+
5059+
diagVar->diagnose(diag::shared_state_main_actor_node,
5060+
diagVar)
5061+
.fixItInsert(diagVar->getAttributeInsertionLoc(false),
5062+
"@MainActor ");
5063+
diagVar->diagnose(diag::shared_state_nonisolated_unsafe,
5064+
diagVar)
5065+
.fixItInsert(diagVar->getAttributeInsertionLoc(true),
5066+
"nonisolated(unsafe) ");
50485067
}
50495068
}
50505069
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
struct Foo {
22
static let member = Bar() // expected-complete-warning {{static property 'member' is not concurrency-safe because non-'Sendable' type 'Bar' may have shared mutable state; this is an error in the Swift 6 language mode}}
3-
// expected-complete-note@-1 {{isolate 'member' to a global actor, or conform 'Bar' to 'Sendable'}}
3+
// expected-complete-note@-1 {{restrict 'member' to the main actor if it will only be accessed from the main thread}}
4+
// expected-complete-note@-2{{unsafely mark 'member' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
45
}

test/Concurrency/actor_isolation.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import OtherActors // expected-warning{{add '@preconcurrency' to suppress 'Senda
1212

1313
let immutableGlobal: String = "hello"
1414

15-
// expected-warning@+2 {{var 'mutableGlobal' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode}}
16-
// expected-note@+1 {{isolate 'mutableGlobal' to a global actor, or convert it to a 'let' constant and conform it to 'Sendable'}}
15+
// expected-warning@+4 {{var 'mutableGlobal' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode}}
16+
// expected-note@+3 {{convert 'mutableGlobal' to a 'let' constant to make the shared state immutable}}
17+
// expected-note@+2 {{restrict 'mutableGlobal' to the main actor if it will only be accessed from the main thread}}
18+
// expected-note@+1 {{unsafely mark 'mutableGlobal' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
1719
var mutableGlobal: String = "can't touch this" // expected-note 5{{var declared here}}
1820

1921
@available(SwiftStdlib 5.1, *)

test/Concurrency/concurrency_warnings.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ class GlobalCounter { // expected-note{{class 'GlobalCounter' does not conform t
99
}
1010

1111
let rs = GlobalCounter() // expected-warning {{let 'rs' is not concurrency-safe because non-'Sendable' type 'GlobalCounter' may have shared mutable state; this is an error in the Swift 6 language mode}}
12-
// expected-note@-1 {{isolate 'rs' to a global actor, or conform 'GlobalCounter' to 'Sendable'}}
12+
// expected-note@-1 {{restrict 'rs' to the main actor if it will only be accessed from the main thread}}
13+
// expected-note@-2 {{unsafely mark 'rs' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
14+
1315

1416
var globalInt = 17 // expected-warning {{var 'globalInt' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode}}
15-
// expected-note@-1 {{isolate 'globalInt' to a global actor, or convert it to a 'let' constant and conform it to 'Sendable'}}
17+
// expected-note@-1 {{restrict 'globalInt' to the main actor if it will only be accessed from the main thread}}
1618
// expected-note@-2 2{{var declared here}}
17-
19+
// expected-note@-3 {{unsafely mark 'globalInt' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
20+
// expected-note@-4 {{convert 'globalInt' to a 'let' constant to make the shared state immutable}}
1821

1922
class MyError: Error { // expected-warning{{non-final class 'MyError' cannot conform to 'Sendable'; use '@unchecked Sendable'}}
2023
var storage = 0 // expected-warning{{stored property 'storage' of 'Sendable'-conforming class 'MyError' is mutable}}

test/Concurrency/concurrent_value_checking.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,9 @@ typealias BadGenericCF<T> = @Sendable () -> T?
274274
typealias GoodGenericCF<T: Sendable> = @Sendable () -> T? // okay
275275

276276
var concurrentFuncVar: (@Sendable (NotConcurrent) -> Void)? = nil // expected-warning{{var 'concurrentFuncVar' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode}}
277-
// expected-note@-1 {{isolate 'concurrentFuncVar' to a global actor, or convert it to a 'let' constant and conform it to 'Sendable'}}
277+
// expected-note@-1 {{restrict 'concurrentFuncVar' to the main actor if it will only be accessed from the main thread}}
278+
// expected-note@-2 {{unsafely mark 'concurrentFuncVar' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
279+
// expected-note@-3 {{convert 'concurrentFuncVar' to a 'let' constant to make the shared state immutable}}
278280

279281
// ----------------------------------------------------------------------
280282
// Sendable restriction on @Sendable closures.

test/Concurrency/flow_isolation.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,8 +520,10 @@ struct CardboardBox<T> {
520520

521521
@available(SwiftStdlib 5.1, *)
522522
var globalVar: EscapeArtist? // expected-warning {{var 'globalVar' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode}}
523-
// expected-note@-1 {{isolate 'globalVar' to a global actor, or convert it to a 'let' constant and conform it to 'Sendable'}}
523+
// expected-note@-1 {{restrict 'globalVar' to the main actor if it will only be accessed from the main thread}}
524524
// expected-note@-2 2 {{var declared here}}
525+
// expected-note@-3 {{unsafely mark 'globalVar' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
526+
// expected-note@-4 {{convert 'globalVar' to a 'let' constant to make the shared state immutable}}
525527

526528
@available(SwiftStdlib 5.1, *)
527529
actor EscapeArtist {

test/Concurrency/freestanding_top_level.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// RUN: %target-swift-frontend -concurrency-model=task-to-thread -typecheck -verify %s
22
// RUN: %target-swift-frontend -concurrency-model=task-to-thread -typecheck -verify -verify-additional-prefix complete- -strict-concurrency=complete %s
33

4-
// expected-complete-warning@+3 {{var 'global' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode}}
5-
// expected-complete-note@+2 {{isolate 'global' to a global actor, or convert it to a 'let' constant and conform it to 'Sendable'}}
6-
// expected-complete-note@+1 {{var declared here}}
4+
// expected-complete-warning@+5 {{var 'global' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode}}
5+
// expected-complete-note@+4 {{restrict 'global' to the main actor if it will only be accessed from the main thread}}{{1-1=@MainActor }}
6+
// expected-complete-note@+3 {{var declared here}}
7+
// expected-complete-note@+2 {{unsafely mark 'global' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}{{1-1=nonisolated(unsafe) }}
8+
// expected-complete-note@+1 {{convert 'global' to a 'let' constant to make the shared state immutable}}{{1-4=let}}
79
var global = 10
810

911
// expected-complete-warning@+1 {{reference to var 'global' is not concurrency-safe because it involves shared mutable state; this is an error in the Swift 6 language mode}}

0 commit comments

Comments
 (0)