Skip to content

[6.2][stdlib] Allow a default for optional interpolations #81360

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions include/swift/AST/DiagnosticsSema.def
Original file line number Diff line number Diff line change
Expand Up @@ -4973,6 +4973,8 @@ NOTE(iuo_to_any_coercion_note_func_result,none,
(const ValueDecl *))
NOTE(default_optional_to_any,none,
"provide a default value to avoid this warning", ())
NOTE(default_optional_parameter,none,
"use a default value parameter to avoid this warning", ())
NOTE(force_optional_to_any,none,
"force-unwrap the value to avoid this warning", ())
NOTE(silence_optional_to_any,none,
Expand Down
38 changes: 29 additions & 9 deletions lib/Sema/MiscDiagnostics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5604,7 +5604,7 @@ static void diagnoseUnintendedOptionalBehavior(const Expr *E,
segment->getCalledValue(/*skipFunctionConversions=*/true), kind))
if (auto firstArg =
getFirstArgIfUnintendedInterpolation(segment->getArgs(), kind))
diagnoseUnintendedInterpolation(firstArg, kind);
diagnoseUnintendedInterpolation(segment, firstArg, kind);
}

bool interpolationWouldBeUnintended(ConcreteDeclRef appendMethod,
Expand Down Expand Up @@ -5670,13 +5670,40 @@ static void diagnoseUnintendedOptionalBehavior(const Expr *E,
return firstArg;
}

void diagnoseUnintendedInterpolation(Expr * arg, UnintendedInterpolationKind kind) {
std::string baseInterpolationTypeName(CallExpr *segment) {
if (auto selfApplyExpr = dyn_cast<SelfApplyExpr>(segment->getFn())) {
auto baseType = selfApplyExpr->getBase()->getType();
return baseType->getWithoutSpecifierType()->getString();
}
return "unknown";
}

void diagnoseUnintendedInterpolation(CallExpr *segment,
Expr * arg,
UnintendedInterpolationKind kind) {
Ctx.Diags
.diagnose(arg->getStartLoc(),
diag::debug_description_in_string_interpolation_segment,
(bool)kind)
.highlight(arg->getSourceRange());

if (kind == UnintendedInterpolationKind::Optional) {
auto wrappedArgType = arg->getType()->getRValueType()->getOptionalObjectType();
auto baseTypeName = baseInterpolationTypeName(segment);

// Suggest using a default value parameter, but only for non-string values
// when the base interpolation type is the default.
if (!wrappedArgType->isString() && baseTypeName == "DefaultStringInterpolation")
Ctx.Diags.diagnose(arg->getLoc(), diag::default_optional_parameter)
.highlight(arg->getSourceRange())
.fixItInsertAfter(arg->getEndLoc(), ", default: <#default value#>");

// Suggest providing a default value using the nil-coalescing operator.
Ctx.Diags.diagnose(arg->getLoc(), diag::default_optional_to_any)
.highlight(arg->getSourceRange())
.fixItInsertAfter(arg->getEndLoc(), " ?? <#default value#>");
}

// Suggest 'String(describing: <expr>)'.
auto argStart = arg->getStartLoc();
Ctx.Diags
Expand All @@ -5686,13 +5713,6 @@ static void diagnoseUnintendedOptionalBehavior(const Expr *E,
.highlight(arg->getSourceRange())
.fixItInsert(argStart, "String(describing: ")
.fixItInsertAfter(arg->getEndLoc(), ")");

if (kind == UnintendedInterpolationKind::Optional) {
// Suggest inserting a default value.
Ctx.Diags.diagnose(arg->getLoc(), diag::default_optional_to_any)
.highlight(arg->getSourceRange())
.fixItInsertAfter(arg->getEndLoc(), " ?? <#default value#>");
}
}

PreWalkResult<Expr *> walkToExprPre(Expr *E) override {
Expand Down
168 changes: 145 additions & 23 deletions stdlib/public/core/StringInterpolation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
//
//===----------------------------------------------------------------------===//

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

/// Appends a literal segment of a string interpolation.
///
/// Do not call this method directly. It is used by the compiler when
/// interpreting string interpolations.
/// You don't need to call this method directly. It's used by the compiler
/// when interpreting string interpolations.
@inlinable
public mutating func appendLiteral(_ literal: String) {
literal.write(to: &self)
Expand All @@ -90,8 +90,8 @@ public struct DefaultStringInterpolation: StringInterpolationProtocol, Sendable
/// Interpolates the given value's textual representation into the
/// string literal being created.
///
/// Do not call this method directly. It is used by the compiler when
/// interpreting string interpolations. Instead, use string
/// You don't need to call this method directly. It's used by the compiler
/// when interpreting string interpolations. Instead, use string
/// interpolation to create a new string by including values, literals,
/// variables, or expressions enclosed in parentheses, prefixed by a
/// backslash (`\(`...`)`).
Expand All @@ -114,8 +114,8 @@ public struct DefaultStringInterpolation: StringInterpolationProtocol, Sendable
/// Interpolates the given value's textual representation into the
/// string literal being created.
///
/// Do not call this method directly. It is used by the compiler when
/// interpreting string interpolations. Instead, use string
/// You don't need to call this method directly. It's used by the compiler
/// when interpreting string interpolations. Instead, use string
/// interpolation to create a new string by including values, literals,
/// variables, or expressions enclosed in parentheses, prefixed by a
/// backslash (`\(`...`)`).
Expand All @@ -136,8 +136,8 @@ public struct DefaultStringInterpolation: StringInterpolationProtocol, Sendable
/// Interpolates the given value's textual representation into the
/// string literal being created.
///
/// Do not call this method directly. It is used by the compiler when
/// interpreting string interpolations. Instead, use string
/// You don't need to call this method directly. It's used by the compiler
/// when interpreting string interpolations. Instead, use string
/// interpolation to create a new string by including values, literals,
/// variables, or expressions enclosed in parentheses, prefixed by a
/// backslash (`\(`...`)`).
Expand All @@ -160,8 +160,8 @@ public struct DefaultStringInterpolation: StringInterpolationProtocol, Sendable
/// Interpolates the given value's textual representation into the
/// string literal being created.
///
/// Do not call this method directly. It is used by the compiler when
/// interpreting string interpolations. Instead, use string
/// You don't need to call this method directly. It's used by the compiler
/// when interpreting string interpolations. Instead, use string
/// interpolation to create a new string by including values, literals,
/// variables, or expressions enclosed in parentheses, prefixed by a
/// backslash (`\(`...`)`).
Expand Down Expand Up @@ -197,6 +197,128 @@ public struct DefaultStringInterpolation: StringInterpolationProtocol, Sendable
}
}

extension DefaultStringInterpolation {
/// Interpolates the given optional value's textual representation, or the
/// specified default string, into the string literal being created.
///
/// You don't need to call this method directly. It's used by the compiler
/// when interpreting string interpolations where you provide a `default`
/// parameter. For example, the following code implicitly calls this method,
/// using the value of the `default` parameter when `value` is `nil`:
///
/// var age: Int? = 48
/// print("Your age is \(age, default: "unknown")")
/// // Prints: Your age is 48
/// age = nil
/// print("Your age is \(age, default: "unknown")")
/// // Prints: Your age is unknown
///
/// - Parameters:
/// - value: The value to include in a string interpolation, if non-`nil`.
/// - default: The string to include if `value` is `nil`.
@_alwaysEmitIntoClient
public mutating func appendInterpolation<T>(
_ value: T?,
default: @autoclosure () -> some StringProtocol
) where T: TextOutputStreamable, T: CustomStringConvertible {
if let value {
self.appendInterpolation(value)
} else {
self.appendInterpolation(`default`())
}
}

/// Interpolates the given optional value's textual representation, or the
/// specified default string, into the string literal being created.
///
/// You don't need to call this method directly. It's used by the compiler
/// when interpreting string interpolations where you provide a `default`
/// parameter. For example, the following code implicitly calls this method,
/// using the value of the `default` parameter when `value` is `nil`:
///
/// var age: Int? = 48
/// print("Your age is \(age, default: "unknown")")
/// // Prints: Your age is 48
/// age = nil
/// print("Your age is \(age, default: "unknown")")
/// // Prints: Your age is unknown
///
/// - Parameters:
/// - value: The value to include in a string interpolation, if non-`nil`.
/// - default: The string to include if `value` is `nil`.
@_alwaysEmitIntoClient
public mutating func appendInterpolation<T>(
_ value: T?,
default: @autoclosure () -> some StringProtocol
) where T: TextOutputStreamable {
if let value {
self.appendInterpolation(value)
} else {
self.appendInterpolation(`default`())
}
}

/// Interpolates the given optional value's textual representation, or the
/// specified default string, into the string literal being created.
///
/// You don't need to call this method directly. It's used by the compiler
/// when interpreting string interpolations where you provide a `default`
/// parameter. For example, the following code implicitly calls this method,
/// using the value of the `default` parameter when `value` is `nil`:
///
/// var age: Int? = 48
/// print("Your age is \(age, default: "unknown")")
/// // Prints: Your age is 48
/// age = nil
/// print("Your age is \(age, default: "unknown")")
/// // Prints: Your age is unknown
///
/// - Parameters:
/// - value: The value to include in a string interpolation, if non-`nil`.
/// - default: The string to include if `value` is `nil`.
@_alwaysEmitIntoClient
public mutating func appendInterpolation<T>(
_ value: T?,
default: @autoclosure () -> some StringProtocol
) where T: CustomStringConvertible {
if let value {
self.appendInterpolation(value)
} else {
self.appendInterpolation(`default`())
}
}

/// Interpolates the given optional value's textual representation, or the
/// specified default string, into the string literal being created.
///
/// You don't need to call this method directly. It's used by the compiler
/// when interpreting string interpolations where you provide a `default`
/// parameter. For example, the following code implicitly calls this method,
/// using the value of the `default` parameter when `value` is `nil`:
///
/// var age: Int? = 48
/// print("Your age is \(age, default: "unknown")")
/// // Prints: Your age is 48
/// age = nil
/// print("Your age is \(age, default: "unknown")")
/// // Prints: Your age is unknown
///
/// - Parameters:
/// - value: The value to include in a string interpolation, if non-`nil`.
/// - default: The string to include if `value` is `nil`.
@_alwaysEmitIntoClient
public mutating func appendInterpolation<T>(
_ value: T?,
default: @autoclosure () -> some StringProtocol
) {
if let value {
self.appendInterpolation(value)
} else {
self.appendInterpolation(`default`())
}
}
}

extension DefaultStringInterpolation: CustomStringConvertible {
@inlinable
public var description: String {
Expand All @@ -220,9 +342,9 @@ extension DefaultStringInterpolation: TextOutputStream {
extension String {
/// Creates a new instance from an interpolated string literal.
///
/// Do not call this initializer directly. It is used by the compiler when
/// you create a string using string interpolation. Instead, use string
/// interpolation to create a new string by including values, literals,
/// You don't need to call this initializer directly. It's used by the
/// compiler when you create a string using string interpolation. Instead, use
/// string interpolation to create a new string by including values, literals,
/// variables, or expressions enclosed in parentheses, prefixed by a
/// backslash (`\(`...`)`).
///
Expand All @@ -244,9 +366,9 @@ extension String {
extension Substring {
/// Creates a new instance from an interpolated string literal.
///
/// Do not call this initializer directly. It is used by the compiler when
/// you create a string using string interpolation. Instead, use string
/// interpolation to create a new string by including values, literals,
/// You don't need to call this initializer directly. It's used by the
/// compiler when you create a string using string interpolation. Instead, use
/// string interpolation to create a new string by including values, literals,
/// variables, or expressions enclosed in parentheses, prefixed by a
/// backslash (`\(`...`)`).
///
Expand Down
3 changes: 2 additions & 1 deletion test/Constraints/diag_ambiguities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ func rdar29691909(o: AnyObject) -> Any? {

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

// https://github.com/apple/swift/issues/46300
Expand Down
8 changes: 4 additions & 4 deletions test/IDE/complete_at_top_level.swift
Original file line number Diff line number Diff line change
Expand Up @@ -295,10 +295,10 @@ func resyncParserB14() {}
var stringInterp = "\(#^STRING_INTERP_3?check=STRING_INTERP^#)"
_ = "" + "\(#^STRING_INTERP_4?check=STRING_INTERP^#)" + ""
// STRING_INTERP-DAG: Decl[InstanceMethod]/CurrNominal/Flair[ArgLabels]/IsSystem: ['(']{#(value): any Any.Type#}[')'][#Void#];
// STRING_INTERP-DAG: Decl[Struct]/CurrModule: FooStruct[#FooStruct#]; name=FooStruct
// STRING_INTERP-DAG: Decl[FreeFunction]/CurrModule/TypeRelation[Invalid]: fooFunc1()[#Void#];
// STRING_INTERP-DAG: Decl[FreeFunction]/CurrModule: optStr()[#String?#];
// STRING_INTERP-DAG: Decl[GlobalVar]/Local: fooObject[#FooStruct#];
// STRING_INTERP-DAG: Decl[Struct]/CurrModule/TypeRelation[Convertible]: FooStruct[#FooStruct#]; name=FooStruct
// STRING_INTERP-DAG: Decl[FreeFunction]/CurrModule/TypeRelation[Convertible]: fooFunc1[#() -> ()#]; name=fooFunc1
// STRING_INTERP-DAG: Decl[FreeFunction]/CurrModule/TypeRelation[Convertible]: optStr()[#String?#]; name=optStr()
// STRING_INTERP-DAG: Decl[GlobalVar]/Local/TypeRelation[Convertible]: fooObject[#FooStruct#]; name=fooObject
func resyncParserC1() {}

// FOR_COLLECTION-NOT: forIndex
Expand Down
14 changes: 7 additions & 7 deletions test/IDE/complete_expr_postfix_begin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,18 @@ protocol FooProtocol {
typealias FooTypealias = Int

// Function parameter
// COMMON-DAG: Decl[LocalVar]/Local: fooParam[#FooStruct#]{{; name=.+$}}
// COMMON-DAG: Decl[LocalVar]/Local{{(/TypeRelation\[Convertible\])?}}: fooParam[#FooStruct#]; name=fooParam
// Global completions
// COMMON-DAG: Decl[Struct]/CurrModule: FooStruct[#FooStruct#]{{; name=.+$}}
// COMMON-DAG: Decl[Enum]/CurrModule: FooEnum[#FooEnum#]{{; name=.+$}}
// COMMON-DAG: Decl[Class]/CurrModule: FooClass[#FooClass#]{{; name=.+$}}
// COMMON-DAG: Decl[Protocol]/CurrModule/Flair[RareType]: FooProtocol[#FooProtocol#]{{; name=.+$}}
// COMMON-DAG: Decl[Struct]/CurrModule{{(/TypeRelation\[Convertible\])?}}: FooStruct[#FooStruct#]{{; name=.+$}}
// COMMON-DAG: Decl[Enum]/CurrModule{{(/TypeRelation\[Convertible\])?}}: FooEnum[#FooEnum#]{{; name=.+$}}
// COMMON-DAG: Decl[Class]/CurrModule{{(/TypeRelation\[Convertible\])?}}: FooClass[#FooClass#]{{; name=.+$}}
// COMMON-DAG: Decl[Protocol]/CurrModule/Flair[RareType]{{(/TypeRelation\[Convertible\])?}}: FooProtocol[#FooProtocol#]{{; name=.+$}}
// COMMON-DAG: Decl[TypeAlias]/CurrModule{{(/TypeRelation\[Convertible\])?}}: FooTypealias[#Int#]{{; name=.+$}}
// COMMON-DAG: Decl[GlobalVar]/CurrModule: fooObject[#FooStruct#]{{; name=.+$}}
// COMMON-DAG: Decl[GlobalVar]/CurrModule{{(/TypeRelation\[Convertible\])?}}: fooObject[#FooStruct#]{{; name=.+$}}
// COMMON-DAG: Keyword[try]/None: try{{; name=.+$}}
// COMMON-DAG: Literal[Boolean]/None{{(/TypeRelation\[Convertible\])?}}: true[#Bool#]{{; name=.+$}}
// COMMON-DAG: Literal[Boolean]/None{{(/TypeRelation\[Convertible\])?}}: false[#Bool#]{{; name=.+$}}
// COMMON-DAG: Literal[Nil]/None: nil{{; name=.+$}}
// COMMON-DAG: Literal[Nil]/None{{(/TypeRelation\[Convertible\])?}}: nil{{.*; name=.+$}}
// COMMON-DAG: Decl[Struct]/OtherModule[Swift]/IsSystem{{(/TypeRelation\[Convertible\])?}}: Int8[#Int8#]{{; name=.+$}}
// COMMON-DAG: Decl[Struct]/OtherModule[Swift]/IsSystem{{(/TypeRelation\[Convertible\])?}}: Int16[#Int16#]{{; name=.+$}}
// COMMON-DAG: Decl[Struct]/OtherModule[Swift]/IsSystem{{(/TypeRelation\[Convertible\])?}}: Int32[#Int32#]{{; name=.+$}}
Expand Down
4 changes: 2 additions & 2 deletions test/IDE/complete_sself.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

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

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

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

func freeFunc() {
#^GLOBAL_BODY_EXPR?check=NOSELF^#
Expand Down
Loading