Skip to content

Commit 9f2546b

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 f774fbd commit 9f2546b

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
@@ -1174,6 +1174,18 @@ ERROR(extra_trailing_closure_in_call,none,
11741174
ERROR(trailing_closure_bad_param,none,
11751175
"trailing closure passed to parameter of type %0 that does not "
11761176
"accept a closure", (Type))
1177+
WARNING(unlabeled_trailing_closure_changed_behavior,none,
1178+
"since Swift 5.3, unlabeled trailing closure argument matches parameter %0 rather than parameter %1",
1179+
(Identifier, Identifier))
1180+
WARNING(unlabeled_trailing_closure_changed_behavior_same_param_name,none,
1181+
"since Swift 5.3, unlabeled trailing closure argument matches earlier parameter %0 rather than later parameter with the same name",
1182+
(Identifier))
1183+
NOTE(trailing_closure_select_parameter,none,
1184+
"label the argument with %0 to %select{retain the pre-Swift 5.3 behavior|silence this warning for Swift 5.3 and newer}1",
1185+
(Identifier, unsigned))
1186+
NOTE(decl_multiple_defaulted_closure_parameters,none,
1187+
"%0 contains defaulted closure parameters %1 and %2",
1188+
(DeclName, Identifier, Identifier))
11771189
NOTE(candidate_with_extraneous_args,none,
11781190
"candidate %0 requires %1 argument%s1, "
11791191
"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
@@ -5521,6 +5521,195 @@ static bool hasCurriedSelf(ConstraintSystem &cs, ConcreteDeclRef callee,
55215521
return false;
55225522
}
55235523

5524+
/// Determine the arity of the given parameter of function type.
5525+
static unsigned functionParameterArity(AnyFunctionType::Param param) {
5526+
Type paramType = param.getPlainType();
5527+
if (param.isVariadic())
5528+
paramType = ParamDecl::getVarargBaseTy(paramType);
5529+
5530+
paramType = paramType->lookThroughAllOptionalTypes();
5531+
5532+
if (param.isAutoClosure()) {
5533+
paramType = paramType->castTo<AnyFunctionType>()->getResult();
5534+
paramType = paramType->lookThroughAllOptionalTypes();
5535+
}
5536+
5537+
return paramType->castTo<AnyFunctionType>()->getNumParams();
5538+
}
5539+
5540+
/// SE-0286 changed the direction in which the unlabeled trailing closure
5541+
/// argument is matched to a parameter, from backward (the pre-Swift 5.3
5542+
/// semantics) to forward (after SE-0286). Identify cases where this may
5543+
/// have resulted in a silent change in behavior.
5544+
static void maybeWarnAboutTrailingClosureBindingChange(
5545+
ConcreteDeclRef callee,
5546+
Expr *fn,
5547+
Expr *arg,
5548+
ArrayRef<AnyFunctionType::Param> args,
5549+
ArrayRef<AnyFunctionType::Param> params,
5550+
const ParameterListInfo &paramInfo,
5551+
Optional<unsigned> unlabeledTrailingClosureIndex,
5552+
ArrayRef<ParamBinding> parameterBindings) {
5553+
5554+
if (!unlabeledTrailingClosureIndex)
5555+
return;
5556+
5557+
if (*unlabeledTrailingClosureIndex != args.size() - 1)
5558+
return;
5559+
5560+
// Find the parameter that bound the unlabeled trailing closure argument.
5561+
unsigned paramIdx;
5562+
for (paramIdx = 0; paramIdx != parameterBindings.size(); ++paramIdx) {
5563+
if (llvm::find(parameterBindings[paramIdx], *unlabeledTrailingClosureIndex)
5564+
!= parameterBindings[paramIdx].end())
5565+
break;
5566+
}
5567+
assert(paramIdx != parameterBindings.size() && "Unbound trailing closure!");
5568+
5569+
// If this parameter requires an argument, it would have been unfilled
5570+
// prior to SE-2086; there is nothing to diagnose.
5571+
if (parameterRequiresArgument(params, paramInfo, paramIdx))
5572+
return;
5573+
5574+
// Look for a later parameter that could match a trailing closure; the
5575+
// last one of these would have been picked prior to SE-0286.
5576+
Optional<unsigned> matchingBackwardParamIdx;
5577+
for (unsigned backwardParamIdx :
5578+
range(paramIdx + 1, parameterBindings.size())) {
5579+
if (!paramInfo.acceptsUnlabeledTrailingClosureArgument(backwardParamIdx))
5580+
continue;
5581+
5582+
matchingBackwardParamIdx = backwardParamIdx;
5583+
}
5584+
5585+
// If there is no other parameter that could match the unlabeled trailing
5586+
// closure, there is nothing to diagnose.
5587+
if (!matchingBackwardParamIdx)
5588+
return;
5589+
5590+
// Do a simple arity check; if the matched parameter and backward-matched
5591+
// parameter accept functions with different arity, this would not have
5592+
// type-checked with the backward scan, so there is nothing to report.
5593+
if (functionParameterArity(params[paramIdx]) !=
5594+
functionParameterArity(params[*matchingBackwardParamIdx]))
5595+
return;
5596+
5597+
// Compute the various source locations where diagnostics will occur.
5598+
ASTContext &ctx = params[paramIdx].getPlainType()->getASTContext();
5599+
Expr *trailingClosure;
5600+
SourceLoc existingRParenLoc;
5601+
SourceLoc leadingCommaLoc;
5602+
if (auto tupleExpr = dyn_cast<TupleExpr>(arg)) {
5603+
trailingClosure = tupleExpr->getElements().back();
5604+
existingRParenLoc = tupleExpr->getRParenLoc();
5605+
assert(tupleExpr->getNumElements() >= 2 && "Should be a ParenExpr?");
5606+
leadingCommaLoc = Lexer::getLocForEndOfToken(
5607+
ctx.SourceMgr,
5608+
tupleExpr->getElements()[tupleExpr->getNumElements()-2]->getEndLoc());
5609+
} else {
5610+
auto parenExpr = cast<ParenExpr>(arg);
5611+
trailingClosure = parenExpr->getSubExpr();
5612+
existingRParenLoc = parenExpr->getRParenLoc();
5613+
}
5614+
5615+
// Determine the names of the parameters that would be matched by the
5616+
// forward and backward scans.
5617+
Identifier paramName = params[paramIdx].getLabel();
5618+
Identifier backwardParamName = params[*matchingBackwardParamIdx].getLabel();
5619+
5620+
// Produce a diagnostic referencing the callee.
5621+
auto noteCallee = [&] {
5622+
auto decl = callee.getDecl();
5623+
if (!decl)
5624+
return;
5625+
5626+
auto diag = ctx.Diags.diagnose(
5627+
decl, diag::decl_multiple_defaulted_closure_parameters,
5628+
decl->getName(), paramName, backwardParamName);
5629+
5630+
// Dig out the parameter declarations so we can highlight them.
5631+
// FIXME: There should be a utility for this.
5632+
const ParameterList *paramList = nullptr;
5633+
if (auto *func = dyn_cast<AbstractFunctionDecl>(decl)) {
5634+
paramList = func->getParameters();
5635+
} else if (auto *subscript = dyn_cast<SubscriptDecl>(decl)) {
5636+
paramList = subscript->getIndices();
5637+
} else if (auto *enumElement = dyn_cast<EnumElementDecl>(decl)) {
5638+
paramList = enumElement->getParameterList();
5639+
}
5640+
5641+
if (paramList) {
5642+
diag.highlight(paramList->get(paramIdx)->getLoc());
5643+
diag.highlight(paramList->get(*matchingBackwardParamIdx)->getLoc());
5644+
}
5645+
};
5646+
5647+
// If the parameters have the same name, provide a custom diagnostic and then
5648+
// bail out early; there are no useful notes we can provide here.
5649+
if (paramName == backwardParamName) {
5650+
ctx.Diags.diagnose(trailingClosure->getStartLoc(), diag::unlabeled_trailing_closure_changed_behavior_same_param_name,
5651+
paramName);
5652+
noteCallee();
5653+
return;
5654+
}
5655+
5656+
// Produce the diagnostic.
5657+
ctx.Diags.diagnose(trailingClosure->getStartLoc(), diag::unlabeled_trailing_closure_changed_behavior, paramName,
5658+
backwardParamName);
5659+
5660+
// Produce a note with a Fix-It describing how to resolve the ambiguity.
5661+
auto diagResolution = [&](Identifier paramName, unsigned which) {
5662+
// Emit the note.
5663+
auto diag = ctx.Diags.diagnose(
5664+
trailingClosure->getStartLoc(), diag::trailing_closure_select_parameter,
5665+
paramName, which);
5666+
5667+
// Figure out the text to be inserted before the trailing closure.
5668+
SmallString<16> insertionText;
5669+
SourceLoc insertionLoc;
5670+
if (leadingCommaLoc.isValid()) {
5671+
insertionText += ", ";
5672+
assert(existingRParenLoc.isValid());
5673+
insertionLoc = leadingCommaLoc;
5674+
} else if (existingRParenLoc.isInvalid()) {
5675+
insertionText += "(";
5676+
insertionLoc = Lexer::getLocForEndOfToken(
5677+
ctx.SourceMgr, fn->getEndLoc());
5678+
} else {
5679+
insertionLoc = existingRParenLoc;
5680+
}
5681+
5682+
// Add the label, if there is one.
5683+
if (!paramName.empty()) {
5684+
insertionText += paramName.str();
5685+
insertionText += ": ";
5686+
}
5687+
5688+
// If there is an existing right parentheses, remove it while we
5689+
// insert the new text.
5690+
if (existingRParenLoc.isValid()) {
5691+
SourceLoc afterExistingRParenLoc = Lexer::getLocForEndOfToken(
5692+
ctx.SourceMgr, existingRParenLoc);
5693+
diag.fixItReplaceChars(
5694+
insertionLoc, afterExistingRParenLoc, insertionText);
5695+
} else {
5696+
// Insert the appropriate prefix.
5697+
diag.fixItInsert(insertionLoc, insertionText);
5698+
}
5699+
5700+
5701+
// Insert a right parenthesis after the closing '}' of the trailing closure;
5702+
SourceLoc newRParenLoc = Lexer::getLocForEndOfToken(
5703+
ctx.SourceMgr, trailingClosure->getEndLoc());
5704+
diag.fixItInsert(newRParenLoc, ")");
5705+
};
5706+
5707+
diagResolution(backwardParamName, 0);
5708+
diagResolution(paramName, 1);
5709+
5710+
noteCallee();
5711+
}
5712+
55245713
Expr *ExprRewriter::coerceCallArguments(
55255714
Expr *arg, AnyFunctionType *funcType,
55265715
ConcreteDeclRef callee,
@@ -5594,6 +5783,12 @@ Expr *ExprRewriter::coerceCallArguments(
55945783
// FIXME: Eventually, we want to enforce that we have either argTuple or
55955784
// argParen here.
55965785

5786+
// Warn if there was a recent change in trailing closure binding semantics
5787+
// that might have lead to a silent change in behavior.
5788+
maybeWarnAboutTrailingClosureBindingChange(
5789+
callee, apply->getFn(), arg, args, params, paramInfo,
5790+
unlabeledTrailingClosureIndex, parameterBindings);
5791+
55975792
SourceLoc lParenLoc, rParenLoc;
55985793
if (argTuple) {
55995794
lParenLoc = argTuple->getLParenLoc();

lib/Sema/CSSimplify.cpp

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

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

lib/Sema/ConstraintSystem.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5439,6 +5439,12 @@ BraceStmt *applyFunctionBuilderTransform(
54395439
constraints::SolutionApplicationTarget)>
54405440
rewriteTarget);
54415441

5442+
/// Whether the given parameter requires an argument.
5443+
bool parameterRequiresArgument(
5444+
ArrayRef<AnyFunctionType::Param> params,
5445+
const ParameterListInfo &paramInfo,
5446+
unsigned paramIdx);
5447+
54425448
} // end namespace swift
54435449

54445450
#endif // LLVM_SWIFT_SEMA_CONSTRAINT_SYSTEM_H
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)