Skip to content

Commit e68069f

Browse files
authored
[stdlib] Allow a default for optional interpolations (#80547)
This adds an `appendInterpolation` overload to `DefaultStringInterpolation` that includes a parameter for providing a default string when the value to interpolate is `nil`. This allows this kind of usage: ```swift let age: Int? = nil print("Your age is \(age, default: "timeless")") // Prints "Your age is timeless" ``` The change includes an additional fixit when optional values are interpolated, with a suggestion to use this `default:` parameter.
1 parent 62b7a6f commit e68069f

File tree

10 files changed

+248
-58
lines changed

10 files changed

+248
-58
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4993,6 +4993,8 @@ NOTE(iuo_to_any_coercion_note_func_result,none,
49934993
(const ValueDecl *))
49944994
NOTE(default_optional_to_any,none,
49954995
"provide a default value to avoid this warning", ())
4996+
NOTE(default_optional_parameter,none,
4997+
"use a default value parameter to avoid this warning", ())
49964998
NOTE(force_optional_to_any,none,
49974999
"force-unwrap the value to avoid this warning", ())
49985000
NOTE(silence_optional_to_any,none,

lib/Sema/MiscDiagnostics.cpp

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5600,7 +5600,7 @@ static void diagnoseUnintendedOptionalBehavior(const Expr *E,
56005600
segment->getCalledValue(/*skipFunctionConversions=*/true), kind))
56015601
if (auto firstArg =
56025602
getFirstArgIfUnintendedInterpolation(segment->getArgs(), kind))
5603-
diagnoseUnintendedInterpolation(firstArg, kind);
5603+
diagnoseUnintendedInterpolation(segment, firstArg, kind);
56045604
}
56055605

56065606
bool interpolationWouldBeUnintended(ConcreteDeclRef appendMethod,
@@ -5666,13 +5666,40 @@ static void diagnoseUnintendedOptionalBehavior(const Expr *E,
56665666
return firstArg;
56675667
}
56685668

5669-
void diagnoseUnintendedInterpolation(Expr * arg, UnintendedInterpolationKind kind) {
5669+
std::string baseInterpolationTypeName(CallExpr *segment) {
5670+
if (auto selfApplyExpr = dyn_cast<SelfApplyExpr>(segment->getFn())) {
5671+
auto baseType = selfApplyExpr->getBase()->getType();
5672+
return baseType->getWithoutSpecifierType()->getString();
5673+
}
5674+
return "unknown";
5675+
}
5676+
5677+
void diagnoseUnintendedInterpolation(CallExpr *segment,
5678+
Expr * arg,
5679+
UnintendedInterpolationKind kind) {
56705680
Ctx.Diags
56715681
.diagnose(arg->getStartLoc(),
56725682
diag::debug_description_in_string_interpolation_segment,
56735683
(bool)kind)
56745684
.highlight(arg->getSourceRange());
56755685

5686+
if (kind == UnintendedInterpolationKind::Optional) {
5687+
auto wrappedArgType = arg->getType()->getRValueType()->getOptionalObjectType();
5688+
auto baseTypeName = baseInterpolationTypeName(segment);
5689+
5690+
// Suggest using a default value parameter, but only for non-string values
5691+
// when the base interpolation type is the default.
5692+
if (!wrappedArgType->isString() && baseTypeName == "DefaultStringInterpolation")
5693+
Ctx.Diags.diagnose(arg->getLoc(), diag::default_optional_parameter)
5694+
.highlight(arg->getSourceRange())
5695+
.fixItInsertAfter(arg->getEndLoc(), ", default: <#default value#>");
5696+
5697+
// Suggest providing a default value using the nil-coalescing operator.
5698+
Ctx.Diags.diagnose(arg->getLoc(), diag::default_optional_to_any)
5699+
.highlight(arg->getSourceRange())
5700+
.fixItInsertAfter(arg->getEndLoc(), " ?? <#default value#>");
5701+
}
5702+
56765703
// Suggest 'String(describing: <expr>)'.
56775704
auto argStart = arg->getStartLoc();
56785705
Ctx.Diags
@@ -5682,13 +5709,6 @@ static void diagnoseUnintendedOptionalBehavior(const Expr *E,
56825709
.highlight(arg->getSourceRange())
56835710
.fixItInsert(argStart, "String(describing: ")
56845711
.fixItInsertAfter(arg->getEndLoc(), ")");
5685-
5686-
if (kind == UnintendedInterpolationKind::Optional) {
5687-
// Suggest inserting a default value.
5688-
Ctx.Diags.diagnose(arg->getLoc(), diag::default_optional_to_any)
5689-
.highlight(arg->getSourceRange())
5690-
.fixItInsertAfter(arg->getEndLoc(), " ?? <#default value#>");
5691-
}
56925712
}
56935713

56945714
PreWalkResult<Expr *> walkToExprPre(Expr *E) override {

stdlib/public/core/StringInterpolation.swift

Lines changed: 145 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
/// Represents a string literal with interpolations while it is being built up.
14-
///
15-
/// Do not create an instance of this type directly. It is used by the compiler
16-
/// when you create a string using string interpolation. Instead, use string
17-
/// interpolation to create a new string by including values, literals,
13+
/// Represents a string literal with interpolations while it's being built up.
14+
///
15+
/// You don't need to create an instance of this type directly. It's used by the
16+
/// compiler when you create a string using string interpolation. Instead, use
17+
/// string interpolation to create a new string by including values, literals,
1818
/// variables, or expressions enclosed in parentheses, prefixed by a
1919
/// backslash (`\(`...`)`).
2020
///
@@ -68,8 +68,8 @@ public struct DefaultStringInterpolation: StringInterpolationProtocol, Sendable
6868
/// Creates a string interpolation with storage pre-sized for a literal
6969
/// with the indicated attributes.
7070
///
71-
/// Do not call this initializer directly. It is used by the compiler when
72-
/// interpreting string interpolations.
71+
/// You don't need to call this initializer directly. It's used by the
72+
/// compiler when interpreting string interpolations.
7373
@inlinable
7474
public init(literalCapacity: Int, interpolationCount: Int) {
7575
let capacityPerInterpolation = 2
@@ -80,8 +80,8 @@ public struct DefaultStringInterpolation: StringInterpolationProtocol, Sendable
8080

8181
/// Appends a literal segment of a string interpolation.
8282
///
83-
/// Do not call this method directly. It is used by the compiler when
84-
/// interpreting string interpolations.
83+
/// You don't need to call this method directly. It's used by the compiler
84+
/// when interpreting string interpolations.
8585
@inlinable
8686
public mutating func appendLiteral(_ literal: String) {
8787
literal.write(to: &self)
@@ -90,8 +90,8 @@ public struct DefaultStringInterpolation: StringInterpolationProtocol, Sendable
9090
/// Interpolates the given value's textual representation into the
9191
/// string literal being created.
9292
///
93-
/// Do not call this method directly. It is used by the compiler when
94-
/// interpreting string interpolations. Instead, use string
93+
/// You don't need to call this method directly. It's used by the compiler
94+
/// when interpreting string interpolations. Instead, use string
9595
/// interpolation to create a new string by including values, literals,
9696
/// variables, or expressions enclosed in parentheses, prefixed by a
9797
/// backslash (`\(`...`)`).
@@ -114,8 +114,8 @@ public struct DefaultStringInterpolation: StringInterpolationProtocol, Sendable
114114
/// Interpolates the given value's textual representation into the
115115
/// string literal being created.
116116
///
117-
/// Do not call this method directly. It is used by the compiler when
118-
/// interpreting string interpolations. Instead, use string
117+
/// You don't need to call this method directly. It's used by the compiler
118+
/// when interpreting string interpolations. Instead, use string
119119
/// interpolation to create a new string by including values, literals,
120120
/// variables, or expressions enclosed in parentheses, prefixed by a
121121
/// backslash (`\(`...`)`).
@@ -136,8 +136,8 @@ public struct DefaultStringInterpolation: StringInterpolationProtocol, Sendable
136136
/// Interpolates the given value's textual representation into the
137137
/// string literal being created.
138138
///
139-
/// Do not call this method directly. It is used by the compiler when
140-
/// interpreting string interpolations. Instead, use string
139+
/// You don't need to call this method directly. It's used by the compiler
140+
/// when interpreting string interpolations. Instead, use string
141141
/// interpolation to create a new string by including values, literals,
142142
/// variables, or expressions enclosed in parentheses, prefixed by a
143143
/// backslash (`\(`...`)`).
@@ -160,8 +160,8 @@ public struct DefaultStringInterpolation: StringInterpolationProtocol, Sendable
160160
/// Interpolates the given value's textual representation into the
161161
/// string literal being created.
162162
///
163-
/// Do not call this method directly. It is used by the compiler when
164-
/// interpreting string interpolations. Instead, use string
163+
/// You don't need to call this method directly. It's used by the compiler
164+
/// when interpreting string interpolations. Instead, use string
165165
/// interpolation to create a new string by including values, literals,
166166
/// variables, or expressions enclosed in parentheses, prefixed by a
167167
/// backslash (`\(`...`)`).
@@ -197,6 +197,128 @@ public struct DefaultStringInterpolation: StringInterpolationProtocol, Sendable
197197
}
198198
}
199199

200+
extension DefaultStringInterpolation {
201+
/// Interpolates the given optional value's textual representation, or the
202+
/// specified default string, into the string literal being created.
203+
///
204+
/// You don't need to call this method directly. It's used by the compiler
205+
/// when interpreting string interpolations where you provide a `default`
206+
/// parameter. For example, the following code implicitly calls this method,
207+
/// using the value of the `default` parameter when `value` is `nil`:
208+
///
209+
/// var age: Int? = 48
210+
/// print("Your age is \(age, default: "unknown")")
211+
/// // Prints: Your age is 48
212+
/// age = nil
213+
/// print("Your age is \(age, default: "unknown")")
214+
/// // Prints: Your age is unknown
215+
///
216+
/// - Parameters:
217+
/// - value: The value to include in a string interpolation, if non-`nil`.
218+
/// - default: The string to include if `value` is `nil`.
219+
@_alwaysEmitIntoClient
220+
public mutating func appendInterpolation<T>(
221+
_ value: T?,
222+
default: @autoclosure () -> some StringProtocol
223+
) where T: TextOutputStreamable, T: CustomStringConvertible {
224+
if let value {
225+
self.appendInterpolation(value)
226+
} else {
227+
self.appendInterpolation(`default`())
228+
}
229+
}
230+
231+
/// Interpolates the given optional value's textual representation, or the
232+
/// specified default string, into the string literal being created.
233+
///
234+
/// You don't need to call this method directly. It's used by the compiler
235+
/// when interpreting string interpolations where you provide a `default`
236+
/// parameter. For example, the following code implicitly calls this method,
237+
/// using the value of the `default` parameter when `value` is `nil`:
238+
///
239+
/// var age: Int? = 48
240+
/// print("Your age is \(age, default: "unknown")")
241+
/// // Prints: Your age is 48
242+
/// age = nil
243+
/// print("Your age is \(age, default: "unknown")")
244+
/// // Prints: Your age is unknown
245+
///
246+
/// - Parameters:
247+
/// - value: The value to include in a string interpolation, if non-`nil`.
248+
/// - default: The string to include if `value` is `nil`.
249+
@_alwaysEmitIntoClient
250+
public mutating func appendInterpolation<T>(
251+
_ value: T?,
252+
default: @autoclosure () -> some StringProtocol
253+
) where T: TextOutputStreamable {
254+
if let value {
255+
self.appendInterpolation(value)
256+
} else {
257+
self.appendInterpolation(`default`())
258+
}
259+
}
260+
261+
/// Interpolates the given optional value's textual representation, or the
262+
/// specified default string, into the string literal being created.
263+
///
264+
/// You don't need to call this method directly. It's used by the compiler
265+
/// when interpreting string interpolations where you provide a `default`
266+
/// parameter. For example, the following code implicitly calls this method,
267+
/// using the value of the `default` parameter when `value` is `nil`:
268+
///
269+
/// var age: Int? = 48
270+
/// print("Your age is \(age, default: "unknown")")
271+
/// // Prints: Your age is 48
272+
/// age = nil
273+
/// print("Your age is \(age, default: "unknown")")
274+
/// // Prints: Your age is unknown
275+
///
276+
/// - Parameters:
277+
/// - value: The value to include in a string interpolation, if non-`nil`.
278+
/// - default: The string to include if `value` is `nil`.
279+
@_alwaysEmitIntoClient
280+
public mutating func appendInterpolation<T>(
281+
_ value: T?,
282+
default: @autoclosure () -> some StringProtocol
283+
) where T: CustomStringConvertible {
284+
if let value {
285+
self.appendInterpolation(value)
286+
} else {
287+
self.appendInterpolation(`default`())
288+
}
289+
}
290+
291+
/// Interpolates the given optional value's textual representation, or the
292+
/// specified default string, into the string literal being created.
293+
///
294+
/// You don't need to call this method directly. It's used by the compiler
295+
/// when interpreting string interpolations where you provide a `default`
296+
/// parameter. For example, the following code implicitly calls this method,
297+
/// using the value of the `default` parameter when `value` is `nil`:
298+
///
299+
/// var age: Int? = 48
300+
/// print("Your age is \(age, default: "unknown")")
301+
/// // Prints: Your age is 48
302+
/// age = nil
303+
/// print("Your age is \(age, default: "unknown")")
304+
/// // Prints: Your age is unknown
305+
///
306+
/// - Parameters:
307+
/// - value: The value to include in a string interpolation, if non-`nil`.
308+
/// - default: The string to include if `value` is `nil`.
309+
@_alwaysEmitIntoClient
310+
public mutating func appendInterpolation<T>(
311+
_ value: T?,
312+
default: @autoclosure () -> some StringProtocol
313+
) {
314+
if let value {
315+
self.appendInterpolation(value)
316+
} else {
317+
self.appendInterpolation(`default`())
318+
}
319+
}
320+
}
321+
200322
extension DefaultStringInterpolation: CustomStringConvertible {
201323
@inlinable
202324
public var description: String {
@@ -220,9 +342,9 @@ extension DefaultStringInterpolation: TextOutputStream {
220342
extension String {
221343
/// Creates a new instance from an interpolated string literal.
222344
///
223-
/// Do not call this initializer directly. It is used by the compiler when
224-
/// you create a string using string interpolation. Instead, use string
225-
/// interpolation to create a new string by including values, literals,
345+
/// You don't need to call this initializer directly. It's used by the
346+
/// compiler when you create a string using string interpolation. Instead, use
347+
/// string interpolation to create a new string by including values, literals,
226348
/// variables, or expressions enclosed in parentheses, prefixed by a
227349
/// backslash (`\(`...`)`).
228350
///
@@ -244,9 +366,9 @@ extension String {
244366
extension Substring {
245367
/// Creates a new instance from an interpolated string literal.
246368
///
247-
/// Do not call this initializer directly. It is used by the compiler when
248-
/// you create a string using string interpolation. Instead, use string
249-
/// interpolation to create a new string by including values, literals,
369+
/// You don't need to call this initializer directly. It's used by the
370+
/// compiler when you create a string using string interpolation. Instead, use
371+
/// string interpolation to create a new string by including values, literals,
250372
/// variables, or expressions enclosed in parentheses, prefixed by a
251373
/// backslash (`\(`...`)`).
252374
///

test/Constraints/diag_ambiguities.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ func rdar29691909(o: AnyObject) -> Any? {
3838

3939
func rdar29907555(_ value: Any!) -> String {
4040
return "\(value)" // expected-warning {{string interpolation produces a debug description for an optional value; did you mean to make this explicit?}}
41-
// expected-note@-1 {{use 'String(describing:)' to silence this warning}}
41+
// expected-note@-1 {{use a default value parameter to avoid this warning}}
4242
// expected-note@-2 {{provide a default value to avoid this warning}}
43+
// expected-note@-3 {{use 'String(describing:)' to silence this warning}}
4344
}
4445

4546
// https://github.com/apple/swift/issues/46300

test/IDE/complete_at_top_level.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,10 +295,10 @@ func resyncParserB14() {}
295295
var stringInterp = "\(#^STRING_INTERP_3?check=STRING_INTERP^#)"
296296
_ = "" + "\(#^STRING_INTERP_4?check=STRING_INTERP^#)" + ""
297297
// STRING_INTERP-DAG: Decl[InstanceMethod]/CurrNominal/Flair[ArgLabels]/IsSystem: ['(']{#(value): any Any.Type#}[')'][#Void#];
298-
// STRING_INTERP-DAG: Decl[Struct]/CurrModule: FooStruct[#FooStruct#]; name=FooStruct
299-
// STRING_INTERP-DAG: Decl[FreeFunction]/CurrModule/TypeRelation[Invalid]: fooFunc1()[#Void#];
300-
// STRING_INTERP-DAG: Decl[FreeFunction]/CurrModule: optStr()[#String?#];
301-
// STRING_INTERP-DAG: Decl[GlobalVar]/Local: fooObject[#FooStruct#];
298+
// STRING_INTERP-DAG: Decl[Struct]/CurrModule/TypeRelation[Convertible]: FooStruct[#FooStruct#]; name=FooStruct
299+
// STRING_INTERP-DAG: Decl[FreeFunction]/CurrModule/TypeRelation[Convertible]: fooFunc1[#() -> ()#]; name=fooFunc1
300+
// STRING_INTERP-DAG: Decl[FreeFunction]/CurrModule/TypeRelation[Convertible]: optStr()[#String?#]; name=optStr()
301+
// STRING_INTERP-DAG: Decl[GlobalVar]/Local/TypeRelation[Convertible]: fooObject[#FooStruct#]; name=fooObject
302302
func resyncParserC1() {}
303303

304304
// FOR_COLLECTION-NOT: forIndex

test/IDE/complete_expr_postfix_begin.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,18 @@ protocol FooProtocol {
2727
typealias FooTypealias = Int
2828

2929
// Function parameter
30-
// COMMON-DAG: Decl[LocalVar]/Local: fooParam[#FooStruct#]{{; name=.+$}}
30+
// COMMON-DAG: Decl[LocalVar]/Local{{(/TypeRelation\[Convertible\])?}}: fooParam[#FooStruct#]; name=fooParam
3131
// Global completions
32-
// COMMON-DAG: Decl[Struct]/CurrModule: FooStruct[#FooStruct#]{{; name=.+$}}
33-
// COMMON-DAG: Decl[Enum]/CurrModule: FooEnum[#FooEnum#]{{; name=.+$}}
34-
// COMMON-DAG: Decl[Class]/CurrModule: FooClass[#FooClass#]{{; name=.+$}}
35-
// COMMON-DAG: Decl[Protocol]/CurrModule/Flair[RareType]: FooProtocol[#FooProtocol#]{{; name=.+$}}
32+
// COMMON-DAG: Decl[Struct]/CurrModule{{(/TypeRelation\[Convertible\])?}}: FooStruct[#FooStruct#]{{; name=.+$}}
33+
// COMMON-DAG: Decl[Enum]/CurrModule{{(/TypeRelation\[Convertible\])?}}: FooEnum[#FooEnum#]{{; name=.+$}}
34+
// COMMON-DAG: Decl[Class]/CurrModule{{(/TypeRelation\[Convertible\])?}}: FooClass[#FooClass#]{{; name=.+$}}
35+
// COMMON-DAG: Decl[Protocol]/CurrModule/Flair[RareType]{{(/TypeRelation\[Convertible\])?}}: FooProtocol[#FooProtocol#]{{; name=.+$}}
3636
// COMMON-DAG: Decl[TypeAlias]/CurrModule{{(/TypeRelation\[Convertible\])?}}: FooTypealias[#Int#]{{; name=.+$}}
37-
// COMMON-DAG: Decl[GlobalVar]/CurrModule: fooObject[#FooStruct#]{{; name=.+$}}
37+
// COMMON-DAG: Decl[GlobalVar]/CurrModule{{(/TypeRelation\[Convertible\])?}}: fooObject[#FooStruct#]{{; name=.+$}}
3838
// COMMON-DAG: Keyword[try]/None: try{{; name=.+$}}
3939
// COMMON-DAG: Literal[Boolean]/None{{(/TypeRelation\[Convertible\])?}}: true[#Bool#]{{; name=.+$}}
4040
// COMMON-DAG: Literal[Boolean]/None{{(/TypeRelation\[Convertible\])?}}: false[#Bool#]{{; name=.+$}}
41-
// COMMON-DAG: Literal[Nil]/None: nil{{; name=.+$}}
41+
// COMMON-DAG: Literal[Nil]/None{{(/TypeRelation\[Convertible\])?}}: nil{{.*; name=.+$}}
4242
// COMMON-DAG: Decl[Struct]/OtherModule[Swift]/IsSystem{{(/TypeRelation\[Convertible\])?}}: Int8[#Int8#]{{; name=.+$}}
4343
// COMMON-DAG: Decl[Struct]/OtherModule[Swift]/IsSystem{{(/TypeRelation\[Convertible\])?}}: Int16[#Int16#]{{; name=.+$}}
4444
// COMMON-DAG: Decl[Struct]/OtherModule[Swift]/IsSystem{{(/TypeRelation\[Convertible\])?}}: Int32[#Int32#]{{; name=.+$}}

test/IDE/complete_sself.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
// GENERICPARAM: Decl[GenericTypeParam]/Local: Self[#Self#];
66

7-
// STATICSELF: Keyword[Self]/CurrNominal: Self[#S#];
7+
// STATICSELF: Keyword[Self]/CurrNominal{{(/TypeRelation\[Convertible\])?}}: Self[#S#];
88

9-
// DYNAMICSELF: Keyword[Self]/CurrNominal: Self[#Self#];
9+
// DYNAMICSELF: Keyword[Self]/CurrNominal{{(/TypeRelation\[Convertible\])?}}: Self[#Self#];
1010

1111
func freeFunc() {
1212
#^GLOBAL_BODY_EXPR?check=NOSELF^#

0 commit comments

Comments
 (0)