Skip to content

[4.2] [Type checker] Improve diagnostics when an optional value is not unwrapped #17955

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
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
19 changes: 15 additions & 4 deletions include/swift/AST/DiagnosticsSema.def
Original file line number Diff line number Diff line change
Expand Up @@ -885,10 +885,21 @@ NOTE(note_init_parameter,none,
ERROR(missing_nullary_call,none,
"function produces expected type %0; did you mean to call it with '()'?",
(Type))
ERROR(missing_unwrap_optional,none,
"value of optional type %0 not unwrapped; did you mean to use '!' "
"or '?'?",
(Type))
ERROR(optional_not_unwrapped,none,
"value of optional type %0 must be unwrapped to a value of type %1",
(Type, Type))
NOTE(unwrap_with_default_value,none,
"coalesce using '?" "?' to provide a default when the optional value "
"contains 'nil'", ())
NOTE(unwrap_with_force_value,none,
"force-unwrap using '!' to abort execution if the optional value contains "
"'nil'", ())
ERROR(optional_base_not_unwrapped,none,
"value of optional type %0 must be unwrapped to refer to member %1 of "
"wrapped base type %2", (Type, DeclName, Type))
NOTE(optional_base_chain,none,
"chain the optional using '?' to access member %0 only for non-'nil' "
"base values", (DeclName))
ERROR(missing_unwrap_optional_try,none,
"value of optional type %0 not unwrapped; did you mean to use 'try!' "
"or chain with '?'?",
Expand Down
1 change: 1 addition & 0 deletions include/swift/AST/KnownIdentifiers.def
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ IDENTIFIER(AssignmentPrecedence)
IDENTIFIER(CastingPrecedence)
IDENTIFIER(DefaultPrecedence)
IDENTIFIER(FunctionArrowPrecedence)
IDENTIFIER(NilCoalescingPrecedence)
IDENTIFIER(TernaryPrecedence)

// Builtins and literals
Expand Down
41 changes: 27 additions & 14 deletions lib/Sema/CSApply.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7906,6 +7906,27 @@ static bool exprNeedsParensAfterAddingAs(TypeChecker &TC, DeclContext *DC,
return exprNeedsParensOutsideFollowingOperator(TC, DC, expr, rootExpr, asPG);
}

bool swift::exprNeedsParensBeforeAddingNilCoalescing(TypeChecker &TC,
DeclContext *DC,
Expr *expr) {
auto asPG =
TC.lookupPrecedenceGroup(DC, DC->getASTContext().Id_NilCoalescingPrecedence,
SourceLoc());
if (!asPG) return true;
return exprNeedsParensInsideFollowingOperator(TC, DC, expr, asPG);
}

bool swift::exprNeedsParensAfterAddingNilCoalescing(TypeChecker &TC,
DeclContext *DC,
Expr *expr,
Expr *rootExpr) {
auto asPG =
TC.lookupPrecedenceGroup(DC, DC->getASTContext().Id_NilCoalescingPrecedence,
SourceLoc());
if (!asPG) return true;
return exprNeedsParensOutsideFollowingOperator(TC, DC, expr, rootExpr, asPG);
}

namespace {
class ExprWalker : public ASTWalker {
ExprRewriter &Rewriter;
Expand Down Expand Up @@ -8067,7 +8088,7 @@ bool ConstraintSystem::applySolutionFix(Expr *expr,

switch (fix.first.getKind()) {
case FixKind::ForceOptional: {
const Expr *unwrapped = affected->getValueProvidingExpr();
Expr *unwrapped = affected->getValueProvidingExpr();
auto type = solution.simplifyType(getType(affected))
->getRValueObjectType();

Expand All @@ -8078,25 +8099,17 @@ bool ConstraintSystem::applySolutionFix(Expr *expr,
"try!");

} else {
auto diag = TC.diagnose(affected->getLoc(),
diag::missing_unwrap_optional, type);
if (affected->canAppendPostfixExpression(true)) {
diag.fixItInsertAfter(affected->getEndLoc(), "!");
} else {
diag.fixItInsert(affected->getStartLoc(), "(")
.fixItInsertAfter(affected->getEndLoc(), ")!");
}
return diagnoseUnwrap(TC, DC, unwrapped, type);
}
return true;
}

case FixKind::OptionalChaining: {
case FixKind::UnwrapOptionalBase: {
auto type = solution.simplifyType(getType(affected))
->getRValueObjectType();
auto diag = TC.diagnose(affected->getLoc(),
diag::missing_unwrap_optional, type);
diag.fixItInsertAfter(affected->getEndLoc(), "?");
return true;
DeclName memberName = fix.first.getDeclNameArgument(*this);
return diagnoseBaseUnwrapForMemberAccess(affected, type, memberName,
SourceRange());
}

case FixKind::ForceDowncast: {
Expand Down
94 changes: 76 additions & 18 deletions lib/Sema/CSDiag.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2262,15 +2262,6 @@ Expr *FailureDiagnosis::typeCheckChildIndependently(
allowFreeTypeVariables)
TCEOptions |= TypeCheckExprFlags::AllowUnresolvedTypeVariables;

// If we're not passing down contextual type information this time, but the
// original failure had type info that wasn't an optional type,
// then set the flag to prefer fixits with force unwrapping.
if (!convertType) {
auto previousType = CS.getContextualType();
if (previousType && previousType->getOptionalObjectType().isNull())
TCEOptions |= TypeCheckExprFlags::PreferForceUnwrapToOptional;
}

auto resultTy = CS.TC.typeCheckExpression(
subExpr, CS.DC, TypeLoc::withoutLoc(convertType), convertTypePurpose,
TCEOptions, listener, &CS);
Expand Down Expand Up @@ -7751,15 +7742,9 @@ bool FailureDiagnosis::diagnoseMemberFailures(
}

if (!optionalResult.ViableCandidates.empty()) {
// By default we assume that the LHS type is not optional.
StringRef fixIt = "!";
auto contextualType = CS.getContextualType();
if (contextualType && isa<OptionalType>(contextualType.getPointer()))
fixIt = "?";

diagnose(BaseLoc, diag::missing_unwrap_optional, baseObjTy)
.fixItInsertAfter(baseExpr->getEndLoc(), fixIt);
return true;
if (diagnoseBaseUnwrapForMemberAccess(baseExpr, baseObjTy, memberName,
memberRange))
return true;
}
}

Expand Down Expand Up @@ -8666,3 +8651,76 @@ bool ConstraintSystem::salvage(SmallVectorImpl<Solution> &viable, Expr *expr) {
diagnoseFailureForExpr(expr);
return true;
}

bool swift::diagnoseUnwrap(TypeChecker &TC, DeclContext *DC,
Expr *expr, Type type) {
Type unwrappedType = type->getOptionalObjectType();
if (!unwrappedType)
return false;

TC.diagnose(expr->getLoc(), diag::optional_not_unwrapped, type,
unwrappedType);

// Suggest a default value via ?? <default value>
{
auto diag =
TC.diagnose(expr->getLoc(), diag::unwrap_with_default_value);

// Figure out what we need to parenthesize.
bool needsParensInside =
exprNeedsParensBeforeAddingNilCoalescing(TC, DC, expr);
bool needsParensOutside =
exprNeedsParensAfterAddingNilCoalescing(TC, DC, expr, expr);

llvm::SmallString<2> insertBefore;
llvm::SmallString<32> insertAfter;
if (needsParensOutside) {
insertBefore += "(";
}
if (needsParensInside) {
insertBefore += "(";
insertAfter += ")";
}
insertAfter += " ?? <" "#default value#" ">";
if (needsParensOutside)
insertAfter += ")";

if (!insertBefore.empty()) {
diag.fixItInsert(expr->getStartLoc(), insertBefore);
}
diag.fixItInsertAfter(expr->getEndLoc(), insertAfter);
}

// Suggest a force-unwrap.
{
auto diag =
TC.diagnose(expr->getLoc(), diag::unwrap_with_force_value);
if (expr->canAppendPostfixExpression(true)) {
diag.fixItInsertAfter(expr->getEndLoc(), "!");
} else {
diag.fixItInsert(expr->getStartLoc(), "(")
.fixItInsertAfter(expr->getEndLoc(), ")!");
}
}

return true;
}

bool swift::diagnoseBaseUnwrapForMemberAccess(Expr *baseExpr, Type baseType,
DeclName memberName,
SourceRange memberRange) {
auto unwrappedBaseType = baseType->getOptionalObjectType();
if (!unwrappedBaseType)
return false;

ASTContext &ctx = baseType->getASTContext();
DiagnosticEngine &diags = ctx.Diags;
diags.diagnose(baseExpr->getLoc(), diag::optional_base_not_unwrapped,
baseType, memberName, unwrappedBaseType);

diags.diagnose(baseExpr->getLoc(), diag::optional_base_chain, memberName)
.fixItInsertAfter(baseExpr->getEndLoc(), "?");
diags.diagnose(baseExpr->getLoc(), diag::unwrap_with_force_value)
.fixItInsertAfter(baseExpr->getEndLoc(), "!");
return true;
}
19 changes: 5 additions & 14 deletions lib/Sema/CSSimplify.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3548,18 +3548,9 @@ ConstraintSystem::SolutionKind ConstraintSystem::simplifyMemberConstraint(
// Value member lookup has some hacks too.
if (shouldAttemptFixes() && baseObjTy->getOptionalObjectType()) {
// If the base type was an optional, look through it.

// Determine whether or not we want to provide an optional chaining fixit or
// a force unwrap fixit.
bool optionalChain;
if (!getContextualType())
optionalChain = !(Options & ConstraintSystemFlags::PreferForceUnwrapToOptional);
else
optionalChain = !getContextualType()->getOptionalObjectType().isNull();
auto fixKind = optionalChain ? FixKind::OptionalChaining : FixKind::ForceOptional;

// Note the fix.
if (recordFix(fixKind, locator))

// We're unwrapping the base to perform a member access.
if (recordFix(Fix::getUnwrapOptionalBase(*this, member), locator))
return SolutionKind::Error;

// Look through one level of optional.
Expand Down Expand Up @@ -4871,8 +4862,8 @@ ConstraintSystem::simplifyFixConstraint(Fix fix, Type type1, Type type2,
getDefaultDecompositionOptions(flags) | TMF_ApplyingFix;
switch (fix.getKind()) {
case FixKind::ForceOptional:
case FixKind::OptionalChaining: {
// Assume that '!' was applied to the first type.
case FixKind::UnwrapOptionalBase: {
// Assume that we've unwrapped the first type.
auto result =
matchTypes(type1->getRValueObjectType()->getOptionalObjectType(), type2,
matchKind, subflags, locator);
Expand Down
8 changes: 4 additions & 4 deletions lib/Sema/CalleeCandidateInfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -992,10 +992,10 @@ bool CalleeCandidateInfo::diagnoseGenericParameterErrors(Expr *badArgExpr) {
// Check for optional near miss.
if (auto argOptType = substitution->getOptionalObjectType()) {
if (isSubstitutableFor(argOptType, paramArchetype, CS.DC)) {
CS.TC.diagnose(badArgExpr->getLoc(), diag::missing_unwrap_optional,
argType);
foundFailure = true;
break;
if (diagnoseUnwrap(CS.TC, CS.DC, badArgExpr, substitution)) {
foundFailure = true;
break;
}
}
}

Expand Down
16 changes: 14 additions & 2 deletions lib/Sema/Constraint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -487,17 +487,29 @@ Fix Fix::getForcedDowncast(ConstraintSystem &cs, Type toType) {
return Fix(FixKind::ForceDowncast, index);
}

Fix Fix::getUnwrapOptionalBase(ConstraintSystem &cs, DeclName memberName) {
unsigned index = cs.FixedDeclNames.size();
cs.FixedDeclNames.push_back(memberName);
return Fix(FixKind::UnwrapOptionalBase, index);
}

Type Fix::getTypeArgument(ConstraintSystem &cs) const {
assert(getKind() == FixKind::ForceDowncast);
return cs.FixedTypes[Data];
}

/// If this fix has a name argument, retrieve it.
DeclName Fix::getDeclNameArgument(ConstraintSystem &cs) const {
assert(getKind() == FixKind::UnwrapOptionalBase);
return cs.FixedDeclNames[Data];
}

StringRef Fix::getName(FixKind kind) {
switch (kind) {
case FixKind::ForceOptional:
return "fix: force optional";
case FixKind::OptionalChaining:
return "fix: optional chaining";
case FixKind::UnwrapOptionalBase:
return "fix: unwrap optional base of member lookup";
case FixKind::ForceDowncast:
return "fix: force downcast";
case FixKind::AddressOf:
Expand Down
13 changes: 11 additions & 2 deletions lib/Sema/Constraint.h
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,8 @@ enum class FixKind : uint8_t {
/// Introduce a '!' to force an optional unwrap.
ForceOptional,

/// Introduce a '?.' to begin optional chaining.
OptionalChaining,
/// Unwrap an optional base when we have a member access.
UnwrapOptionalBase,

/// Append 'as! T' to force a downcast to the specified type.
ForceDowncast,
Expand Down Expand Up @@ -265,17 +265,26 @@ class Fix {
public:
Fix(FixKind kind) : Kind(kind), Data(0) {
assert(kind != FixKind::ForceDowncast && "Use getForceDowncast()");
assert(kind != FixKind::UnwrapOptionalBase &&
"Use getUnwrapOptionalBase()");
}

/// Produce a new fix that performs a forced downcast to the given type.
static Fix getForcedDowncast(ConstraintSystem &cs, Type toType);

/// Produce a new fix that unwraps an optional base for an access to a member
/// with the given name.
static Fix getUnwrapOptionalBase(ConstraintSystem &cs, DeclName memberName);

/// Retrieve the kind of fix.
FixKind getKind() const { return Kind; }

/// If this fix has a type argument, retrieve it.
Type getTypeArgument(ConstraintSystem &cs) const;

/// If this fix has a name argument, retrieve it.
DeclName getDeclNameArgument(ConstraintSystem &cs) const;

/// Return a string representation of a fix.
static llvm::StringRef getName(FixKind kind);

Expand Down
37 changes: 33 additions & 4 deletions lib/Sema/ConstraintSystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -796,10 +796,6 @@ enum class ConstraintSystemFlags {
/// Whether we allow the solver to attempt fixes to the system.
AllowFixes = 0x01,

/// Set if the client prefers fixits to be in the form of force unwrapping
/// or optional chaining to return an optional.
PreferForceUnwrapToOptional = 0x02,

/// If set, this is going to prevent constraint system from erasing all
/// discovered solutions except the best one.
ReturnAllDiscoveredSolutions = 0x04,
Expand Down Expand Up @@ -1001,6 +997,9 @@ class ConstraintSystem {
/// Types used in fixes.
std::vector<Type> FixedTypes;

/// Declaration names used in fixes.
std::vector<DeclName> FixedDeclNames;

/// \brief The set of remembered disjunction choices used to reach
/// the current constraint system.
SmallVector<std::pair<ConstraintLocator*, unsigned>, 32>
Expand Down Expand Up @@ -3508,6 +3507,36 @@ class OverloadSetCounter : public ASTWalker {
return { true, expr };
}
};


/// Diagnose an attempt to recover when we have a value of optional type
/// that needs to be unwrapped.
///
/// \returns true if a diagnostic was produced.
bool diagnoseUnwrap(TypeChecker &TC, DeclContext *DC, Expr *expr, Type type);

/// Diagnose an attempt to recover from a member access into a value of
/// optional type which needed to be unwrapped for the member to be found.
///
/// \returns true if a diagnostic was produced.
bool diagnoseBaseUnwrapForMemberAccess(Expr *baseExpr, Type baseType,
DeclName memberName,
SourceRange memberRange);

// Return true if, when replacing "<expr>" with "<expr> ?? T", parentheses need
// to be added around <expr> first in order to maintain the correct precedence.
bool exprNeedsParensBeforeAddingNilCoalescing(TypeChecker &TC,
DeclContext *DC,
Expr *expr);

// Return true if, when replacing "<expr>" with "<expr> as T", parentheses need
// to be added around the new expression in order to maintain the correct
// precedence.
bool exprNeedsParensAfterAddingNilCoalescing(TypeChecker &TC,
DeclContext *DC,
Expr *expr,
Expr *rootExpr);

} // end namespace swift

#endif // LLVM_SWIFT_SEMA_CONSTRAINT_SYSTEM_H
2 changes: 0 additions & 2 deletions lib/Sema/TypeCheckConstraints.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1856,8 +1856,6 @@ Type TypeChecker::typeCheckExpression(Expr *&expr, DeclContext *dc,

// Construct a constraint system from this expression.
ConstraintSystemOptions csOptions = ConstraintSystemFlags::AllowFixes;
if (options.contains(TypeCheckExprFlags::PreferForceUnwrapToOptional))
csOptions |= ConstraintSystemFlags::PreferForceUnwrapToOptional;
ConstraintSystem cs(*this, dc, csOptions);
cs.baseCS = baseCS;
CleanupIllFormedExpressionRAII cleanup(Context, expr);
Expand Down
Loading