Skip to content

Commit c667025

Browse files
committed
[Trailing closures] Diagnose the change in behavior with SE-0268.
SE-0248 changes the backward-scan matching behavior for the unlabeled trailing closure into a forward scan. In circumstances where this could silently change the meaning of a call to a particular function, i.e., when there are two defaulted closure parameters such that a given closure to match either one of them, produce an warning that describes the change in behavior. For example: t4.swift:2:24: warning: since Swift 5.3, unlabeled trailing closure argument matches parameter 'x' rather than parameter 'z' trailingClosureSingle2 { $0 } ^ t4.swift:2:24: note: label the argument with 'z' to retain the pre-Swift 5.3 behavior trailingClosureSingle2 { $0 } ^ (z: ) t4.swift:2:24: note: label the argument with 'x' to silence this warning for Swift 5.3 and newer trailingClosureSingle2 { $0 } ^ (x: ) t4.swift:1:6: note: 'trailingClosureSingle2(x:y:z:)' contains defaulted closure parameters 'x' and 'z' func trailingClosureSingle2(x: (Int) -> Int = { $0 } , y: (Int) -> Int = { $0 }, z: (Int) -> Int = { $0 }) {} ^ ~ This explains the (rare) case where SE-0286 silently changes the meaning of a program, offering Fix-Its to either restore the pre-SE-0286 behavior or silence the warning, as appropriate.
1 parent 1756979 commit c667025

File tree

5 files changed

+316
-1
lines changed

5 files changed

+316
-1
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,18 @@ ERROR(extra_trailing_closure_in_call,none,
11861186
ERROR(trailing_closure_bad_param,none,
11871187
"trailing closure passed to parameter of type %0 that does not "
11881188
"accept a closure", (Type))
1189+
WARNING(unlabeled_trailing_closure_changed_behavior,none,
1190+
"since Swift 5.3, unlabeled trailing closure argument matches parameter %0 rather than parameter %1",
1191+
(Identifier, Identifier))
1192+
WARNING(unlabeled_trailing_closure_changed_behavior_same_param_name,none,
1193+
"since Swift 5.3, unlabeled trailing closure argument matches earlier parameter %0 rather than later parameter with the same name",
1194+
(Identifier))
1195+
NOTE(trailing_closure_select_parameter,none,
1196+
"label the argument with %0 to %select{retain the pre-Swift 5.3 behavior|silence this warning for Swift 5.3 and newer}1",
1197+
(Identifier, unsigned))
1198+
NOTE(decl_multiple_defaulted_closure_parameters,none,
1199+
"%0 contains defaulted closure parameters %1 and %2",
1200+
(DeclName, Identifier, Identifier))
11891201
NOTE(candidate_with_extraneous_args,none,
11901202
"candidate %0 requires %1 argument%s1, "
11911203
"but %2 %select{were|was}3 %select{provided|used in closure body}4",

lib/Sema/CSApply.cpp

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5455,6 +5455,195 @@ static bool hasCurriedSelf(ConstraintSystem &cs, ConcreteDeclRef callee,
54555455
return false;
54565456
}
54575457

5458+
/// Determine the arity of the given parameter of function type.
5459+
static unsigned functionParameterArity(AnyFunctionType::Param param) {
5460+
Type paramType = param.getPlainType();
5461+
if (param.isVariadic())
5462+
paramType = ParamDecl::getVarargBaseTy(paramType);
5463+
5464+
paramType = paramType->lookThroughAllOptionalTypes();
5465+
5466+
if (param.isAutoClosure()) {
5467+
paramType = paramType->castTo<AnyFunctionType>()->getResult();
5468+
paramType = paramType->lookThroughAllOptionalTypes();
5469+
}
5470+
5471+
return paramType->castTo<AnyFunctionType>()->getNumParams();
5472+
}
5473+
5474+
/// SE-0286 changed the direction in which the unlabeled trailing closure
5475+
/// argument is matched to a parameter, from backward (the pre-Swift 5.3
5476+
/// semantics) to forward (after SE-0286). Identify cases where this may
5477+
/// have resulted in a silent change in behavior.
5478+
static void maybeWarnAboutTrailingClosureBindingChange(
5479+
ConcreteDeclRef callee,
5480+
Expr *fn,
5481+
Expr *arg,
5482+
ArrayRef<AnyFunctionType::Param> args,
5483+
ArrayRef<AnyFunctionType::Param> params,
5484+
const ParameterListInfo &paramInfo,
5485+
Optional<unsigned> unlabeledTrailingClosureIndex,
5486+
ArrayRef<ParamBinding> parameterBindings) {
5487+
5488+
if (!unlabeledTrailingClosureIndex)
5489+
return;
5490+
5491+
if (*unlabeledTrailingClosureIndex != args.size() - 1)
5492+
return;
5493+
5494+
// Find the parameter that bound the unlabeled trailing closure argument.
5495+
unsigned paramIdx;
5496+
for (paramIdx = 0; paramIdx != parameterBindings.size(); ++paramIdx) {
5497+
if (llvm::find(parameterBindings[paramIdx], *unlabeledTrailingClosureIndex)
5498+
!= parameterBindings[paramIdx].end())
5499+
break;
5500+
}
5501+
assert(paramIdx != parameterBindings.size() && "Unbound trailing closure!");
5502+
5503+
// If this parameter requires an argument, it would have been unfilled
5504+
// prior to SE-2086; there is nothing to diagnose.
5505+
if (parameterRequiresArgument(params, paramInfo, paramIdx))
5506+
return;
5507+
5508+
// Look for a later parameter that could match a trailing closure; the
5509+
// last one of these would have been picked prior to SE-0286.
5510+
Optional<unsigned> matchingBackwardParamIdx;
5511+
for (unsigned backwardParamIdx :
5512+
range(paramIdx + 1, parameterBindings.size())) {
5513+
if (!paramInfo.acceptsUnlabeledTrailingClosureArgument(backwardParamIdx))
5514+
continue;
5515+
5516+
matchingBackwardParamIdx = backwardParamIdx;
5517+
}
5518+
5519+
// If there is no other parameter that could match the unlabeled trailing
5520+
// closure, there is nothing to diagnose.
5521+
if (!matchingBackwardParamIdx)
5522+
return;
5523+
5524+
// Do a simple arity check; if the matched parameter and backward-matched
5525+
// parameter accept functions with different arity, this would not have
5526+
// type-checked with the backward scan, so there is nothing to report.
5527+
if (functionParameterArity(params[paramIdx]) !=
5528+
functionParameterArity(params[*matchingBackwardParamIdx]))
5529+
return;
5530+
5531+
// Compute the various source locations where diagnostics will occur.
5532+
ASTContext &ctx = params[paramIdx].getPlainType()->getASTContext();
5533+
Expr *trailingClosure;
5534+
SourceLoc existingRParenLoc;
5535+
SourceLoc leadingCommaLoc;
5536+
if (auto tupleExpr = dyn_cast<TupleExpr>(arg)) {
5537+
trailingClosure = tupleExpr->getElements().back();
5538+
existingRParenLoc = tupleExpr->getRParenLoc();
5539+
assert(tupleExpr->getNumElements() >= 2 && "Should be a ParenExpr?");
5540+
leadingCommaLoc = Lexer::getLocForEndOfToken(
5541+
ctx.SourceMgr,
5542+
tupleExpr->getElements()[tupleExpr->getNumElements()-2]->getEndLoc());
5543+
} else {
5544+
auto parenExpr = cast<ParenExpr>(arg);
5545+
trailingClosure = parenExpr->getSubExpr();
5546+
existingRParenLoc = parenExpr->getRParenLoc();
5547+
}
5548+
5549+
// Determine the names of the parameters that would be matched by the
5550+
// forward and backward scans.
5551+
Identifier paramName = params[paramIdx].getLabel();
5552+
Identifier backwardParamName = params[*matchingBackwardParamIdx].getLabel();
5553+
5554+
// Produce a diagnostic referencing the callee.
5555+
auto noteCallee = [&] {
5556+
auto decl = callee.getDecl();
5557+
if (!decl)
5558+
return;
5559+
5560+
auto diag = ctx.Diags.diagnose(
5561+
decl, diag::decl_multiple_defaulted_closure_parameters,
5562+
decl->getName(), paramName, backwardParamName);
5563+
5564+
// Dig out the parameter declarations so we can highlight them.
5565+
// FIXME: There should be a utility for this.
5566+
const ParameterList *paramList = nullptr;
5567+
if (auto *func = dyn_cast<AbstractFunctionDecl>(decl)) {
5568+
paramList = func->getParameters();
5569+
} else if (auto *subscript = dyn_cast<SubscriptDecl>(decl)) {
5570+
paramList = subscript->getIndices();
5571+
} else if (auto *enumElement = dyn_cast<EnumElementDecl>(decl)) {
5572+
paramList = enumElement->getParameterList();
5573+
}
5574+
5575+
if (paramList) {
5576+
diag.highlight(paramList->get(paramIdx)->getLoc());
5577+
diag.highlight(paramList->get(*matchingBackwardParamIdx)->getLoc());
5578+
}
5579+
};
5580+
5581+
// If the parameters have the same name, provide a custom diagnostic and then
5582+
// bail out early; there are no useful notes we can provide here.
5583+
if (paramName == backwardParamName) {
5584+
ctx.Diags.diagnose(trailingClosure->getStartLoc(), diag::unlabeled_trailing_closure_changed_behavior_same_param_name,
5585+
paramName);
5586+
noteCallee();
5587+
return;
5588+
}
5589+
5590+
// Produce the diagnostic.
5591+
ctx.Diags.diagnose(trailingClosure->getStartLoc(), diag::unlabeled_trailing_closure_changed_behavior, paramName,
5592+
backwardParamName);
5593+
5594+
// Produce a note with a Fix-It describing how to resolve the ambiguity.
5595+
auto diagResolution = [&](Identifier paramName, unsigned which) {
5596+
// Emit the note.
5597+
auto diag = ctx.Diags.diagnose(
5598+
trailingClosure->getStartLoc(), diag::trailing_closure_select_parameter,
5599+
paramName, which);
5600+
5601+
// Figure out the text to be inserted before the trailing closure.
5602+
SmallString<16> insertionText;
5603+
SourceLoc insertionLoc;
5604+
if (leadingCommaLoc.isValid()) {
5605+
insertionText += ", ";
5606+
assert(existingRParenLoc.isValid());
5607+
insertionLoc = leadingCommaLoc;
5608+
} else if (existingRParenLoc.isInvalid()) {
5609+
insertionText += "(";
5610+
insertionLoc = Lexer::getLocForEndOfToken(
5611+
ctx.SourceMgr, fn->getEndLoc());
5612+
} else {
5613+
insertionLoc = existingRParenLoc;
5614+
}
5615+
5616+
// Add the label, if there is one.
5617+
if (!paramName.empty()) {
5618+
insertionText += paramName.str();
5619+
insertionText += ": ";
5620+
}
5621+
5622+
// If there is an existing right parentheses, remove it while we
5623+
// insert the new text.
5624+
if (existingRParenLoc.isValid()) {
5625+
SourceLoc afterExistingRParenLoc = Lexer::getLocForEndOfToken(
5626+
ctx.SourceMgr, existingRParenLoc);
5627+
diag.fixItReplaceChars(
5628+
insertionLoc, afterExistingRParenLoc, insertionText);
5629+
} else {
5630+
// Insert the appropriate prefix.
5631+
diag.fixItInsert(insertionLoc, insertionText);
5632+
}
5633+
5634+
5635+
// Insert a right parenthesis after the closing '}' of the trailing closure;
5636+
SourceLoc newRParenLoc = Lexer::getLocForEndOfToken(
5637+
ctx.SourceMgr, trailingClosure->getEndLoc());
5638+
diag.fixItInsert(newRParenLoc, ")");
5639+
};
5640+
5641+
diagResolution(backwardParamName, 0);
5642+
diagResolution(paramName, 1);
5643+
5644+
noteCallee();
5645+
}
5646+
54585647
Expr *ExprRewriter::coerceCallArguments(
54595648
Expr *arg, AnyFunctionType *funcType,
54605649
ConcreteDeclRef callee,
@@ -5531,6 +5720,12 @@ Expr *ExprRewriter::coerceCallArguments(
55315720
// FIXME: Eventually, we want to enforce that we have either argTuple or
55325721
// argParen here.
55335722

5723+
// Warn if there was a recent change in trailing closure binding semantics
5724+
// that might have lead to a silent change in behavior.
5725+
maybeWarnAboutTrailingClosureBindingChange(
5726+
callee, apply->getFn(), arg, args, params, paramInfo,
5727+
unlabeledTrailingClosureIndex, parameterBindings);
5728+
55345729
SourceLoc lParenLoc, rParenLoc;
55355730
if (argTuple) {
55365731
lParenLoc = argTuple->getLParenLoc();

lib/Sema/CSSimplify.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ static ConstraintSystem::TypeMatchOptions getDefaultDecompositionOptions(
196196
}
197197

198198
/// Whether the given parameter requires an argument.
199-
static bool parameterRequiresArgument(
199+
bool swift::parameterRequiresArgument(
200200
ArrayRef<AnyFunctionType::Param> params,
201201
const ParameterListInfo &paramInfo,
202202
unsigned paramIdx) {

lib/Sema/ConstraintSystem.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5757,6 +5757,12 @@ bool shouldTypeCheckInEnclosingExpression(ClosureExpr *expr);
57575757
void forEachExprInConstraintSystem(
57585758
Expr *expr, llvm::function_ref<Expr *(Expr *)> callback);
57595759

5760+
/// Whether the given parameter requires an argument.
5761+
bool parameterRequiresArgument(
5762+
ArrayRef<AnyFunctionType::Param> params,
5763+
const ParameterListInfo &paramInfo,
5764+
unsigned paramIdx);
5765+
57605766
} // end namespace swift
57615767

57625768
namespace llvm {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// RUN: %target-typecheck-verify-swift
2+
3+
// Ambiguity when calling.
4+
func ambiguous1( // expected-note 3 {{'ambiguous1(x:a:y:b:z:c:)' contains defaulted closure parameters '}}
5+
6+
x: (Int) -> Int = { $0 },
7+
a: Int = 5,
8+
y: (Int) -> Int = { $0 },
9+
b: Int = 5,
10+
z: (Int) -> Int = { $0 },
11+
c: Int = 5
12+
) {}
13+
14+
func testAmbiguous1() {
15+
ambiguous1 { $0 } // expected-warning{{since Swift 5.3, unlabeled trailing closure argument matches parameter 'x' rather than parameter 'z'}}
16+
// expected-note@-1{{label the argument with 'z' to retain the pre-Swift 5.3 behavior}}{{13-13=(z: }}{{20-20=)}}
17+
// expected-note@-2{{label the argument with 'x' to silence this warning for Swift 5.3 and newer}}{{13-13=(x: }}{{20-20=)}}
18+
19+
ambiguous1() { $0 } // expected-warning{{since Swift 5.3, unlabeled trailing closure argument matches parameter 'x' rather than parameter 'z'}}
20+
// expected-note@-1{{label the argument with 'z' to retain the pre-Swift 5.3 behavior}}{{14-15=z: }}{{22-22=)}}
21+
// expected-note@-2{{label the argument with 'x' to silence this warning for Swift 5.3 and newer}}{{14-15=x: }}{{22-22=)}}
22+
23+
ambiguous1(a: 3) { $0 } // expected-warning{{since Swift 5.3, unlabeled trailing closure argument matches parameter 'y' rather than parameter 'z'}}
24+
// expected-note@-1{{label the argument with 'z' to retain the pre-Swift 5.3 behavior}}{{18-19=, z: }}{{26-26=)}}
25+
// expected-note@-2{{label the argument with 'y' to silence this warning for Swift 5.3 and newer}}{{18-19=, y: }}{{26-26=)}}
26+
27+
// No warning; this is matching the last parameter.
28+
ambiguous1(b: 3) { $0 }
29+
}
30+
31+
// Ambiguity with two unlabeled arguments.
32+
func ambiguous2( // expected-note{{'ambiguous2(_:a:y:b:_:x:)' contains defaulted closure parameters '_' and '_'}}
33+
_: (Int) -> Int = { $0 },
34+
a: Int = 5,
35+
y: (Int) -> Int = { $0 },
36+
b: Int = 5,
37+
_: (Int) -> Int = { $0 },
38+
x: Int = 5
39+
) {}
40+
41+
func testAmbiguous2() {
42+
ambiguous2 { $0 } // expected-warning{{since Swift 5.3, unlabeled trailing closure argument matches earlier parameter '_' rather than later parameter with the same name}}
43+
}
44+
45+
// Ambiguity with one unlabeled argument.
46+
func ambiguous3( // expected-note{{'ambiguous3(x:a:y:b:_:c:)' contains defaulted closure parameters 'x' and '_'}}
47+
x: (Int) -> Int = { $0 },
48+
a: Int = 5,
49+
y: (Int) -> Int = { $0 },
50+
b: Int = 5,
51+
_: (Int) -> Int = { $0 },
52+
c: Int = 5
53+
) {}
54+
55+
func testAmbiguous3() {
56+
ambiguous3 { $0 } // expected-warning{{since Swift 5.3, unlabeled trailing closure argument matches parameter 'x' rather than parameter '_'}}
57+
// expected-note@-1{{label the argument with '_' to retain the pre-Swift 5.3 behavior}}{{13-13=(}}{{20-20=)}}
58+
// expected-note@-2{{label the argument with 'x' to silence this warning for Swift 5.3 and newer}}{{13-13=(x: }}{{20-20=)}}
59+
}
60+
61+
// Not ambiguous because of an arity mismatch that would lead to different
62+
// type-checks.
63+
func notAmbiguous1(
64+
x: (Int, Int) -> Int = { $0 + $1 },
65+
a: Int = 5,
66+
y: (Int) -> Int = { $0 },
67+
b: Int = 5,
68+
_: (Int) -> Int = { $0 },
69+
c: Int = 5
70+
) { }
71+
72+
func testNotAmbiguous1() {
73+
notAmbiguous1 { $0 / $1 }
74+
}
75+
76+
// Not ambiguous because of a missing default argument.
77+
func notAmbiguous2(
78+
x: (Int) -> Int,
79+
a: Int = 5,
80+
y: (Int) -> Int = { $0 },
81+
b: Int = 5,
82+
_: (Int) -> Int = { $0 },
83+
c: Int = 5
84+
) { }
85+
86+
func testNotAmbiguous2() {
87+
notAmbiguous2 { $0 }
88+
}
89+
90+
// Not ambiguous because of a missing default argument.
91+
func notAmbiguous3(
92+
x: (Int) -> Int = { $0 },
93+
a: Int = 5,
94+
y: (Int) -> Int = { $0 },
95+
b: Int = 5,
96+
_: (Int) -> Int ,
97+
c: Int = 5
98+
) { }
99+
100+
func testNotAmbiguous3() {
101+
notAmbiguous3 { $0 }
102+
}

0 commit comments

Comments
 (0)