Skip to content

Commit 433d756

Browse files
committed
[CodeComplete] Match argument labels when completing function arguments
Previously, we always assumed that the position in the arguments would match the position in the parameter, which isn’t true in case of defaulted arguments. Try matching parameters based on argument labels to give better completion results. Fixes rdar://60346573
1 parent cbd0206 commit 433d756

File tree

3 files changed

+116
-20
lines changed

3 files changed

+116
-20
lines changed

lib/IDE/ExprContextAnalysis.cpp

Lines changed: 85 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,79 @@ static bool getPositionInArgs(DeclContext &DC, Expr *Args, Expr *CCExpr,
745745
return false;
746746
}
747747

748+
/// Get index of \p CCExpr in \p Params. Note that the position in \p Params may
749+
/// be different than the position in \p Args if there are defaulted arguments
750+
/// in \p Params which don't occur in \p Args.
751+
///
752+
/// \returns \c true if success, \c false if \p CCExpr is not a part of \p Args.
753+
static bool getPositionInParams(DeclContext &DC, Expr *Args, Expr *CCExpr,
754+
ArrayRef<AnyFunctionType::Param> Params,
755+
unsigned &PosInParams) {
756+
if (isa<ParenExpr>(Args)) {
757+
PosInParams = 0;
758+
return true;
759+
}
760+
761+
auto *tuple = dyn_cast<TupleExpr>(Args);
762+
if (!tuple) {
763+
return false;
764+
}
765+
766+
auto &SM = DC.getASTContext().SourceMgr;
767+
PosInParams = 0;
768+
unsigned PosInArgs = 0;
769+
bool LastParamWasVariadic = false;
770+
// We advance PosInArgs until we find argument that is after the code
771+
// completion token, which is when we stop.
772+
// For each argument, we try to find a matching parameter either by matching
773+
// argument labels, in which case PosInParams may be advanced by more than 1,
774+
// or by advancing PosInParams and PosInArgs both by 1.
775+
for (; PosInArgs < tuple->getNumElements(); ++PosInArgs) {
776+
if (!SM.isBeforeInBuffer(tuple->getElement(PosInArgs)->getEndLoc(),
777+
CCExpr->getStartLoc())) {
778+
// The arg is after the code completion position. Stop.
779+
break;
780+
}
781+
782+
auto ArgName = tuple->getElementName(PosInArgs);
783+
// If the last parameter we matched was variadic, we claim all following
784+
// unlabeled arguments for that variadic parameter -> advance PosInArgs but
785+
// not PosInParams.
786+
if (LastParamWasVariadic && ArgName.empty()) {
787+
continue;
788+
} else {
789+
LastParamWasVariadic = false;
790+
}
791+
792+
// Look for a matching parameter label.
793+
bool FoundLabelMatch = false;
794+
for (unsigned i = PosInParams; i < Params.size(); ++i) {
795+
if (Params[i].getLabel() == ArgName) {
796+
// We have found a label match. Advance the position in the params
797+
// to point to the param after the one with this label.
798+
PosInParams = i + 1;
799+
FoundLabelMatch = true;
800+
if (Params[i].isVariadic()) {
801+
LastParamWasVariadic = true;
802+
}
803+
break;
804+
}
805+
}
806+
807+
if (!FoundLabelMatch) {
808+
// We haven't found a matching argument label. Assume the current one is
809+
// named incorrectly and advance by one.
810+
++PosInParams;
811+
}
812+
}
813+
if (PosInArgs < tuple->getNumElements() && PosInParams < Params.size()) {
814+
// We didn't search until the end, so we found a position in Params. Success
815+
return true;
816+
} else {
817+
return false;
818+
}
819+
}
820+
748821
/// Given an expression and its context, the analyzer tries to figure out the
749822
/// expected type of the expression by analyzing its context.
750823
class ExprContextAnalyzer {
@@ -791,14 +864,12 @@ class ExprContextAnalyzer {
791864
PossibleCallees.assign(Candidates.begin(), Candidates.end());
792865

793866
// Determine the position of code completion token in call argument.
794-
unsigned Position;
867+
unsigned PositionInArgs;
795868
bool HasName;
796-
if (!getPositionInArgs(*DC, Arg, ParsedExpr, Position, HasName))
869+
if (!getPositionInArgs(*DC, Arg, ParsedExpr, PositionInArgs, HasName))
797870
return false;
798871

799872
// Collect possible types (or labels) at the position.
800-
// FIXME: Take variadic and optional parameters into account. We need to do
801-
// something equivalent to 'constraints::matchCallArguments'
802873
{
803874
bool MayNeedName = !HasName && !E->isImplicit() &&
804875
(isa<CallExpr>(E) | isa<SubscriptExpr>(E) ||
@@ -811,13 +882,22 @@ class ExprContextAnalyzer {
811882
memberDC = typeAndDecl.Decl->getInnermostDeclContext();
812883

813884
auto Params = typeAndDecl.Type->getParams();
885+
unsigned PositionInParams;
886+
if (!getPositionInParams(*DC, Arg, ParsedExpr, Params,
887+
PositionInParams)) {
888+
// If the argument doesn't have a matching position in the parameters,
889+
// indicate that with optional nullptr param.
890+
if (seenArgs.insert({Identifier(), CanType()}).second)
891+
recordPossibleParam(nullptr, /*isRequired=*/false);
892+
continue;
893+
}
814894
ParameterList *paramList = nullptr;
815895
if (auto VD = typeAndDecl.Decl) {
816896
paramList = getParameterList(VD);
817897
if (paramList && paramList->size() != Params.size())
818898
paramList = nullptr;
819899
}
820-
for (auto Pos = Position; Pos < Params.size(); ++Pos) {
900+
for (auto Pos = PositionInParams; Pos < Params.size(); ++Pos) {
821901
const auto &paramType = Params[Pos];
822902
Type ty = paramType.getPlainType();
823903
if (memberDC && ty->hasTypeParameter())
@@ -843,12 +923,6 @@ class ExprContextAnalyzer {
843923
if (!canSkip)
844924
break;
845925
}
846-
// If the argument position is out of expeceted number, indicate that
847-
// with optional nullptr param.
848-
if (Position >= Params.size()) {
849-
if (seenArgs.insert({Identifier(), CanType()}).second)
850-
recordPossibleParam(nullptr, /*isRequired=*/false);
851-
}
852926
}
853927
}
854928
return !PossibleTypes.empty() || !PossibleParams.empty();

test/IDE/complete_call_arg.swift

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@
116116

117117
// RUN: %target-swift-ide-test -code-completion -source-filename %s -code-completion-token=LABEL_IN_SELF_DOT_INIT | %FileCheck %s -check-prefix=LABEL_IN_SELF_DOT_INIT
118118

119+
// RUN: %target-swift-ide-test -code-completion -source-filename %s -code-completion-token=MISSING_REQUIRED_PARAM | %FileCheck %s -check-prefix=MISSING_REQUIRED_PARAM
120+
121+
// RUN: %target-swift-ide-test -code-completion -source-filename %s -code-completion-token=NAMED_PARAMETER_WITH_LEADING_VARIADIC | %FileCheck %s -check-prefix=NAMED_PARAMETER_WITH_LEADING_VARIADIC
122+
119123
var i1 = 1
120124
var i2 = 2
121125
var oi1 : Int?
@@ -700,9 +704,7 @@ func testStaticMemberCall() {
700704
// STATIC_METHOD_SECOND: End completions
701705

702706
let _ = TestStaticMemberCall.create2(1, arg3: 2, #^STATIC_METHOD_SKIPPED^#)
703-
// STATIC_METHOD_SKIPPED: Begin completions, 2 items
704-
// FIXME: 'arg3' shouldn't be suggested.
705-
// STATIC_METHOD_SKIPPED: Pattern/ExprSpecific: {#arg3: Int#}[#Int#];
707+
// STATIC_METHOD_SKIPPED: Begin completions, 1 item
706708
// STATIC_METHOD_SKIPPED: Pattern/ExprSpecific: {#arg4: Int#}[#Int#];
707709
// STATIC_METHOD_SKIPPED: End completions
708710

@@ -739,9 +741,7 @@ func testImplicitMember() {
739741
// IMPLICIT_MEMBER_SECOND: End completions
740742

741743
let _: TestStaticMemberCall = .create2(1, arg3: 2, #^IMPLICIT_MEMBER_SKIPPED^#)
742-
// IMPLICIT_MEMBER_SKIPPED: Begin completions, 2 items
743-
// FIXME: 'arg3' shouldn't be suggested.
744-
// IMPLICIT_MEMBER_SKIPPED: Pattern/ExprSpecific: {#arg3: Int#}[#Int#];
744+
// IMPLICIT_MEMBER_SKIPPED: Begin completions, 1 item
745745
// IMPLICIT_MEMBER_SKIPPED: Pattern/ExprSpecific: {#arg4: Int#}[#Int#];
746746
// IMPLICIT_MEMBER_SKIPPED: End completions
747747

@@ -923,3 +923,27 @@ func testLabelsInSelfDotInit() {
923923
}
924924
}
925925
}
926+
927+
func testMissingRequiredParameter() {
928+
class C {
929+
func foo(x: Int, y: Int, z: Int) {}
930+
}
931+
func test(c: C) {
932+
c.foo(y: 1, #^MISSING_REQUIRED_PARAM^#)
933+
// MISSING_REQUIRED_PARAM: Begin completions, 1 item
934+
// MISSING_REQUIRED_PARAM-DAG: Pattern/ExprSpecific: {#z: Int#}[#Int#]
935+
// MISSING_REQUIRED_PARAM: End completions
936+
}
937+
}
938+
939+
func testAfterVariadic() {
940+
class C {
941+
func foo(x: Int..., y: Int, z: Int) {}
942+
}
943+
func test(c: C) {
944+
c.foo(x: 10, 20, 30, y: 40, #^NAMED_PARAMETER_WITH_LEADING_VARIADIC^#)
945+
// NAMED_PARAMETER_WITH_LEADING_VARIADIC: Begin completions, 1 item
946+
// NAMED_PARAMETER_WITH_LEADING_VARIADIC-DAG: Pattern/ExprSpecific: {#z: Int#}[#Int#]
947+
// NAMED_PARAMETER_WITH_LEADING_VARIADIC: End completions
948+
}
949+
}

test/IDE/complete_call_as_function.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,8 @@ func testCallAsFunctionOverloaded(fn: Functor) {
103103
//OVERLOADED_PAREN: End completions
104104

105105
fn(h: .left, #^OVERLOADED_ARG2_LABEL^#)
106-
// FIXME: Should only suggest 'v:' (rdar://problem/60346573).
107-
//OVERLOADED_ARG2_LABEL: Begin completions, 2 items
106+
//OVERLOADED_ARG2_LABEL: Begin completions, 1 item
108107
//OVERLOADED_ARG2_LABEL-DAG: Pattern/ExprSpecific: {#v: Functor.Vertical#}[#Functor.Vertical#];
109-
//OVERLOADED_ARG2_LABEL-DAG: Pattern/ExprSpecific: {#h: Functor.Horizontal#}[#Functor.Horizontal#];
110108
//OVERLOADED_ARG2_LABEL: End completions
111109

112110
fn(h: .left, v: .#^OVERLOADED_ARG2_VALUE^#)

0 commit comments

Comments
 (0)