Skip to content

[Sema] Improve Optional-to-Any diagnostics for collections, nested optionals & IUOs #14221

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 6 commits into from
Feb 3, 2018
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
5 changes: 3 additions & 2 deletions include/swift/AST/DiagnosticsSema.def
Original file line number Diff line number Diff line change
Expand Up @@ -2753,13 +2753,14 @@ WARNING(optional_pattern_match_promotion,none,
"pattern match introduces an implicit promotion from %0 to %1",
(Type, Type))
WARNING(optional_to_any_coercion,none,
"expression implicitly coerced from %0 to Any", (Type))
"expression implicitly coerced from %0 to %1", (Type, Type))
NOTE(default_optional_to_any,none,
"provide a default value to avoid this warning", ())
NOTE(force_optional_to_any,none,
"force-unwrap the value to avoid this warning", ())
NOTE(silence_optional_to_any,none,
"explicitly cast to Any with 'as Any' to silence this warning", ())
"explicitly cast to %0 with '%1' to silence this warning",
(Type, StringRef))
WARNING(optional_in_string_interpolation_segment,none,
"string interpolation produces a debug description for an optional "
"value; did you mean to make this explicit?",
Expand Down
272 changes: 218 additions & 54 deletions lib/Sema/MiscDiagnostics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3449,7 +3449,211 @@ static void diagnoseUnintendedOptionalBehavior(TypeChecker &TC, const Expr *E,

class UnintendedOptionalBehaviorWalker : public ASTWalker {
TypeChecker &TC;
SmallPtrSet<Expr *, 4> ErasureCoercedToAny;
SmallPtrSet<Expr *, 16> IgnoredExprs;

class OptionalToAnyCoercion {
public:
Type DestType;
CoerceExpr *ParentCoercion;

bool shouldSuppressDiagnostic() {
// If we have a parent CoerceExpr that has the same type as our
// Optional-to-Any coercion, don't emit a diagnostic.
return ParentCoercion && ParentCoercion->getType()->isEqual(DestType);
}
};

/// Returns true iff a coercion from srcType to destType is an
/// Optional-to-Any coercion.
bool isOptionalToAnyCoercion(Type srcType, Type destType) {
size_t difference = 0;
return isOptionalToAnyCoercion(srcType, destType, difference);
}

/// Returns true iff a coercion from srcType to destType is an
/// Optional-to-Any coercion. On returning true, the value of 'difference'
/// will be the difference in the levels of optionality.
bool isOptionalToAnyCoercion(Type srcType, Type destType,
size_t &difference) {
SmallVector<Type, 4> destOptionals;
auto destValueType =
destType->lookThroughAllAnyOptionalTypes(destOptionals);

if (!destValueType->isAny())
return false;

SmallVector<Type, 4> srcOptionals;
srcType->lookThroughAllAnyOptionalTypes(srcOptionals);

if (srcOptionals.size() > destOptionals.size()) {
difference = srcOptionals.size() - destOptionals.size();
return true;
} else {
return false;
}
}

/// Returns true iff the collection upcast coercion is an Optional-to-Any
/// coercion.
bool isOptionalToAnyCoercion(CollectionUpcastConversionExpr::ConversionPair
conversion) {
if (!conversion.OrigValue || !conversion.Conversion)
return false;

auto srcType = conversion.OrigValue->getType();
auto destType = conversion.Conversion->getType();
return isOptionalToAnyCoercion(srcType, destType);
}

/// Looks through OptionalEvaluationExprs and InjectIntoOptionalExprs to
/// find a child ErasureExpr, returning nullptr if no such child is found.
/// Any intermediate OptionalEvaluationExprs will be marked as ignored.
ErasureExpr *findErasureExprThroughOptionalInjections(Expr *E) {
while (true) {
if (auto *next = dyn_cast<OptionalEvaluationExpr>(E)) {
// We don't want to re-visit any intermediate optional evaluations.
IgnoredExprs.insert(next);
E = next->getSubExpr();
} else if (auto *next = dyn_cast<InjectIntoOptionalExpr>(E)) {
E = next->getSubExpr();
} else {
break;
}
}
return dyn_cast<ErasureExpr>(E);
}

void emitSilenceOptionalAnyWarningWithCoercion(Expr *E, Type destType) {
SmallString<16> coercionString;
coercionString += " as ";
coercionString += destType->getWithoutParens()->getString();

TC.diagnose(E->getLoc(), diag::silence_optional_to_any,
destType, coercionString.substr(1))
.highlight(E->getSourceRange())
.fixItInsertAfter(E->getEndLoc(), coercionString);
}

void visitErasureExpr(ErasureExpr *E, OptionalToAnyCoercion coercion) {
if (coercion.shouldSuppressDiagnostic())
return;

auto subExpr = E->getSubExpr();

// Look through any BindOptionalExprs, as the coercion may have started
// from a higher level of optionality.
while (auto *bindExpr = dyn_cast<BindOptionalExpr>(subExpr))
subExpr = bindExpr->getSubExpr();

// We're taking the source type from the child of any BindOptionalExprs,
// and the destination from the parent of any
// (InjectIntoOptional/OptionalEvaluation)Exprs in order to take into
// account any bindings that need to be done for nested Optional-to-Any
// coercions, e.g Int??? to Any?.
auto srcType = subExpr->getType();
auto destType = coercion.DestType;

size_t optionalityDifference = 0;
if (!isOptionalToAnyCoercion(srcType, destType, optionalityDifference))
return;

TC.diagnose(subExpr->getStartLoc(), diag::optional_to_any_coercion,
/* from */ srcType, /* to */ destType)
.highlight(subExpr->getSourceRange());

if (optionalityDifference == 1) {
TC.diagnose(subExpr->getLoc(), diag::default_optional_to_any)
.highlight(subExpr->getSourceRange())
.fixItInsertAfter(subExpr->getEndLoc(), " ?? <#default value#>");
}

SmallString<4> forceUnwrapString;
for (size_t i = 0; i < optionalityDifference; i++)
forceUnwrapString += "!";

TC.diagnose(subExpr->getLoc(), diag::force_optional_to_any)
.highlight(subExpr->getSourceRange())
.fixItInsertAfter(subExpr->getEndLoc(), forceUnwrapString);

emitSilenceOptionalAnyWarningWithCoercion(subExpr, destType);
}

void visitCollectionUpcastExpr(CollectionUpcastConversionExpr *E,
OptionalToAnyCoercion coercion) {
// We only need to consider the valueConversion, as the Key type of a
// Dictionary cannot be implicitly coerced to Any.
auto valueConversion = E->getValueConversion();

// We're handling the coercion of the entire collection, so we don't need
// to re-visit a nested ErasureExpr for the value.
if (auto conversionExpr = valueConversion.Conversion)
if (auto *erasureExpr =
findErasureExprThroughOptionalInjections(conversionExpr))
IgnoredExprs.insert(erasureExpr);

if (coercion.shouldSuppressDiagnostic() ||
!isOptionalToAnyCoercion(valueConversion))
return;

auto subExpr = E->getSubExpr();

TC.diagnose(subExpr->getStartLoc(), diag::optional_to_any_coercion,
/* from */ subExpr->getType(), /* to */ E->getType())
.highlight(subExpr->getSourceRange());

emitSilenceOptionalAnyWarningWithCoercion(subExpr, E->getType());
}

void visitPossibleOptionalToAnyExpr(Expr *E,
OptionalToAnyCoercion coercion) {
if (auto *upcastExpr =
dyn_cast<CollectionUpcastConversionExpr>(E)) {
visitCollectionUpcastExpr(upcastExpr, coercion);
} else if (auto *erasureExpr = dyn_cast<ErasureExpr>(E)) {
visitErasureExpr(erasureExpr, coercion);
} else if (auto *optionalEvalExpr = dyn_cast<OptionalEvaluationExpr>(E)) {
// The ErasureExpr could be nested within optional injections and
// bindings, such as is the case for e.g Int??? to Any?. Try and find
// and visit it directly, making sure we don't re-visit it later.
auto subExpr = optionalEvalExpr->getSubExpr();
if (auto *erasureExpr =
findErasureExprThroughOptionalInjections(subExpr)) {
visitErasureExpr(erasureExpr, coercion);
IgnoredExprs.insert(erasureExpr);
}
}
}

void visitInterpolatedStringLiteralExpr(InterpolatedStringLiteralExpr *E) {
// Warn about interpolated segments that contain optionals.
for (auto &segment : E->getSegments()) {
// Allow explicit casts.
if (auto paren = dyn_cast<ParenExpr>(segment))
if (isa<ExplicitCastExpr>(paren->getSubExpr()))
continue;

// Bail out if we don't have an optional.
if (!segment->getType()->getRValueType()->getOptionalObjectType())
continue;

TC.diagnose(segment->getStartLoc(),
diag::optional_in_string_interpolation_segment)
.highlight(segment->getSourceRange());

// Suggest 'String(describing: <expr>)'.
auto segmentStart = segment->getStartLoc().getAdvancedLoc(1);
TC.diagnose(segment->getLoc(),
diag::silence_optional_in_interpolation_segment_call)
.highlight(segment->getSourceRange())
.fixItInsert(segmentStart, "String(describing: ")
.fixItInsert(segment->getEndLoc(), ")");

// Suggest inserting a default value.
TC.diagnose(segment->getLoc(), diag::default_optional_to_any)
.highlight(segment->getSourceRange())
.fixItInsert(segment->getEndLoc(), " ?? <#default value#>");
}
}

std::pair<bool, Expr *> walkToExprPre(Expr *E) override {
if (!E || isa<ErrorExpr>(E) || !E->getType())
Expand All @@ -3459,60 +3663,20 @@ static void diagnoseUnintendedOptionalBehavior(TypeChecker &TC, const Expr *E,
if (!CE->hasSingleExpressionBody())
return { false, E };

if (auto *coercion = dyn_cast<CoerceExpr>(E)) {
if (E->getType()->isAny() && isa<ErasureExpr>(coercion->getSubExpr()))
ErasureCoercedToAny.insert(coercion->getSubExpr());
} else if (isa<ErasureExpr>(E) && !ErasureCoercedToAny.count(E) &&
E->getType()->isAny()) {
auto subExpr = cast<ErasureExpr>(E)->getSubExpr();
auto erasedTy = subExpr->getType();
if (erasedTy->getOptionalObjectType()) {
TC.diagnose(subExpr->getStartLoc(), diag::optional_to_any_coercion,
erasedTy)
.highlight(subExpr->getSourceRange());

TC.diagnose(subExpr->getLoc(), diag::default_optional_to_any)
.highlight(subExpr->getSourceRange())
.fixItInsertAfter(subExpr->getEndLoc(), " ?? <#default value#>");
TC.diagnose(subExpr->getLoc(), diag::force_optional_to_any)
.highlight(subExpr->getSourceRange())
.fixItInsertAfter(subExpr->getEndLoc(), "!");
TC.diagnose(subExpr->getLoc(), diag::silence_optional_to_any)
.highlight(subExpr->getSourceRange())
.fixItInsertAfter(subExpr->getEndLoc(), " as Any");
}
} else if (auto *literal = dyn_cast<InterpolatedStringLiteralExpr>(E)) {
// Warn about interpolated segments that contain optionals.
for (auto &segment : literal->getSegments()) {
// Allow explicit casts.
if (auto paren = dyn_cast<ParenExpr>(segment)) {
if (isa<ExplicitCastExpr>(paren->getSubExpr())) {
continue;
}
}

// Bail out if we don't have an optional.
if (!segment->getType()->getRValueType()->getOptionalObjectType()) {
continue;
}
if (IgnoredExprs.count(E))
return { true, E };

TC.diagnose(segment->getStartLoc(),
diag::optional_in_string_interpolation_segment)
.highlight(segment->getSourceRange());

// Suggest 'String(describing: <expr>)'.
auto segmentStart = segment->getStartLoc().getAdvancedLoc(1);
TC.diagnose(segment->getLoc(),
diag::silence_optional_in_interpolation_segment_call)
.highlight(segment->getSourceRange())
.fixItInsert(segmentStart, "String(describing: ")
.fixItInsert(segment->getEndLoc(), ")");

// Suggest inserting a default value.
TC.diagnose(segment->getLoc(), diag::default_optional_to_any)
.highlight(segment->getSourceRange())
.fixItInsert(segment->getEndLoc(), " ?? <#default value#>");
}
if (auto *literal = dyn_cast<InterpolatedStringLiteralExpr>(E)) {
visitInterpolatedStringLiteralExpr(literal);
} else if (auto *coercion = dyn_cast<CoerceExpr>(E)) {
// If we come across a CoerceExpr, visit its subExpr with the coercion
// as the parent, making sure we don't re-visit the subExpr later.
auto subExpr = coercion->getSubExpr();
visitPossibleOptionalToAnyExpr(subExpr,
{ subExpr->getType(), coercion });
IgnoredExprs.insert(subExpr);
} else {
visitPossibleOptionalToAnyExpr(E, { E->getType(), nullptr });
}
return { true, E };
}
Expand Down
28 changes: 14 additions & 14 deletions test/ClangImporter/objc_parse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,27 +247,27 @@ func almostSubscriptableValueMismatch(_ as1: AlmostSubscriptable, a: A) {
func almostSubscriptableKeyMismatch(_ bc: BadCollection, key: NSString) {
// FIXME: We end up importing this as read-only due to the mismatch between
// getter/setter element types.
var _ : Any = bc[key] // expected-warning {{expression implicitly coerced from 'Any?' to Any}}
var _ : Any = bc[key] // expected-warning {{expression implicitly coerced from 'Any?' to 'Any'}}
// expected-note@-1 {{force-unwrap the value to avoid this warning}}
// expected-note@-2 {{provide a default value to avoid this warning}}
// expected-note@-3 {{explicitly cast to Any with 'as Any' to silence this warning}}
// expected-note@-3 {{explicitly cast to 'Any' with 'as Any' to silence this warning}}
}

func almostSubscriptableKeyMismatchInherited(_ bc: BadCollectionChild,
key: String) {
var value : Any = bc[key] // expected-warning {{expression implicitly coerced from 'Any?' to Any}}
var value : Any = bc[key] // expected-warning {{expression implicitly coerced from 'Any?' to 'Any'}}
// expected-note@-1 {{force-unwrap the value to avoid this warning}}
// expected-note@-2 {{provide a default value to avoid this warning}}
// expected-note@-3 {{explicitly cast to Any with 'as Any' to silence this warning}}
// expected-note@-3 {{explicitly cast to 'Any' with 'as Any' to silence this warning}}
bc[key] = value // expected-error{{cannot assign through subscript: subscript is get-only}}
}

func almostSubscriptableKeyMismatchInherited(_ roc: ReadOnlyCollectionChild,
key: String) {
var value : Any = roc[key] // expected-warning {{expression implicitly coerced from 'Any?' to Any}}
var value : Any = roc[key] // expected-warning {{expression implicitly coerced from 'Any?' to 'Any'}}
// expected-note@-1 {{force-unwrap the value to avoid this warning}}
// expected-note@-2 {{provide a default value to avoid this warning}}
// expected-note@-3 {{explicitly cast to Any with 'as Any' to silence this warning}}
// expected-note@-3 {{explicitly cast to 'Any' with 'as Any' to silence this warning}}
roc[key] = value // expected-error{{cannot assign through subscript: subscript is get-only}}
}

Expand Down Expand Up @@ -407,22 +407,22 @@ func testPropertyAndMethodCollision(_ obj: PropertyAndMethodCollision,
type(of: rev).classRef(rev, doSomething:#selector(getter: NSMenuItem.action))

var value: Any
value = obj.protoProp() // expected-warning {{expression implicitly coerced from 'Any?' to Any}}
value = obj.protoProp() // expected-warning {{expression implicitly coerced from 'Any?' to 'Any'}}
// expected-note@-1 {{force-unwrap the value to avoid this warning}}
// expected-note@-2 {{provide a default value to avoid this warning}}
// expected-note@-3 {{explicitly cast to Any with 'as Any' to silence this warning}}
value = obj.protoPropRO() // expected-warning {{expression implicitly coerced from 'Any?' to Any}}
// expected-note@-3 {{explicitly cast to 'Any' with 'as Any' to silence this warning}}
value = obj.protoPropRO() // expected-warning {{expression implicitly coerced from 'Any?' to 'Any'}}
// expected-note@-1 {{force-unwrap the value to avoid this warning}}
// expected-note@-2 {{provide a default value to avoid this warning}}
// expected-note@-3 {{explicitly cast to Any with 'as Any' to silence this warning}}
value = type(of: obj).protoClassProp() // expected-warning {{expression implicitly coerced from 'Any?' to Any}}
// expected-note@-3 {{explicitly cast to 'Any' with 'as Any' to silence this warning}}
value = type(of: obj).protoClassProp() // expected-warning {{expression implicitly coerced from 'Any?' to 'Any'}}
// expected-note@-1 {{force-unwrap the value to avoid this warning}}
// expected-note@-2 {{provide a default value to avoid this warning}}
// expected-note@-3 {{explicitly cast to Any with 'as Any' to silence this warning}}
value = type(of: obj).protoClassPropRO() // expected-warning {{expression implicitly coerced from 'Any?' to Any}}
// expected-note@-3 {{explicitly cast to 'Any' with 'as Any' to silence this warning}}
value = type(of: obj).protoClassPropRO() // expected-warning {{expression implicitly coerced from 'Any?' to 'Any'}}
// expected-note@-1 {{force-unwrap the value to avoid this warning}}
// expected-note@-2 {{provide a default value to avoid this warning}}
// expected-note@-3 {{explicitly cast to Any with 'as Any' to silence this warning}}
// expected-note@-3 {{explicitly cast to 'Any' with 'as Any' to silence this warning}}
_ = value
}

Expand Down
4 changes: 2 additions & 2 deletions test/Constraints/casts_objc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ func nsobject_as_class_cast<T>(_ x: NSObject, _: T) {
func test(_ a : CFString!, b : CFString) {
let dict = NSMutableDictionary()
let object = NSObject()
dict[a] = object // expected-warning {{expression implicitly coerced from 'CFString?' to Any}}
dict[a] = object // expected-warning {{expression implicitly coerced from 'CFString?' to 'Any'}}
// expected-note@-1 {{force-unwrap the value to avoid this warning}}
// expected-note@-2 {{provide a default value to avoid this warning}}
// expected-note@-3 {{explicitly cast to Any with 'as Any' to silence this warning}}
// expected-note@-3 {{explicitly cast to 'Any' with 'as Any' to silence this warning}}


dict[b] = object
Expand Down
4 changes: 2 additions & 2 deletions test/Constraints/diag_ambiguities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ struct SR3715 {
func take(_ a: [Any]) {}

func test() {
take([overloaded]) // expected-warning {{expression implicitly coerced from 'Int?' to Any}}
take([overloaded]) // expected-warning {{expression implicitly coerced from 'Int?' to 'Any'}}
// expected-note@-1 {{force-unwrap the value to avoid this warning}}
// expected-note@-2 {{provide a default value to avoid this warning}}
// expected-note@-3 {{explicitly cast to Any with 'as Any' to silence this warning}}
// expected-note@-3 {{explicitly cast to 'Any' with 'as Any' to silence this warning}}
}
}

Expand Down
Loading