[CodeCompletion][Sema] Add fix to treat empty or single-element array literals as dictionaries when used as such

In the single-element case, it is treated as the dictionary key.

func takesDict(_ x: [Int: String]) {}
takesDict([]) // diagnose with fixit to add missing ':'
takesDict([1]) // diagnose with fixit to add missing ': <#value#>'
takesDict([foo.<complete>]) // prioritise Int members in completion results -
                            // the user just hasn't written the value yet.

The above previously failed with a generic mismatch error in normal type
checking (due to the literal being parsed as an array literal) and code
completion could not pick up the expected type from the context.
This commit is contained in:
Nathan Hawes
2020-10-30 12:11:53 -07:00
parent cdfff19832
commit edbbefce91
8 changed files with 307 additions and 33 deletions

View File

@@ -285,6 +285,10 @@ enum class FixKind : uint8_t {
/// Allow expressions to reference invalid declarations by turning
/// them into holes.
AllowRefToInvalidDecl,
/// Treat empty and single-element array literals as if they were incomplete
/// dictionary literals when used as such.
TreatArrayLiteralAsDictionary,
};
class ConstraintFix {
@@ -551,6 +555,25 @@ public:
ConstraintLocator *locator);
};
class TreatArrayLiteralAsDictionary final : public ContextualMismatch {
TreatArrayLiteralAsDictionary(ConstraintSystem &cs, Type dictionaryTy,
Type arrayTy, ConstraintLocator *locator)
: ContextualMismatch(cs, FixKind::TreatArrayLiteralAsDictionary,
dictionaryTy, arrayTy, locator) {
}
public:
std::string getName() const override {
return "treat array literal as dictionary";
}
bool diagnose(const Solution &solution, bool asNote = false) const override;
static TreatArrayLiteralAsDictionary *create(ConstraintSystem &cs,
Type dictionaryTy, Type arrayTy,
ConstraintLocator *loc);
};
/// Mark function type as explicitly '@escaping'.
class MarkExplicitlyEscaping final : public ContextualMismatch {
MarkExplicitlyEscaping(ConstraintSystem &cs, Type lhs, Type rhs,

View File

@@ -841,6 +841,27 @@ bool LabelingFailure::diagnoseAsNote() {
return false;
}
bool ArrayLiteralToDictionaryConversionFailure::diagnoseAsError() {
ArrayExpr *AE = getAsExpr<ArrayExpr>(getAnchor());
assert(AE);
if (AE->getNumElements() == 0) {
emitDiagnostic(diag::should_use_empty_dictionary_literal)
.fixItInsertAfter(getLoc(), ":");
return true;
}
auto CTP = getConstraintSystem().getContextualTypePurpose(AE);
emitDiagnostic(diag::should_use_dictionary_literal,
getToType()->lookThroughAllOptionalTypes(),
CTP == CTP_Initialization);
auto diagnostic = emitDiagnostic(diag::meant_dictionary_lit);
if (AE->getNumElements() == 1)
diagnostic.fixItInsertAfter(AE->getElement(0)->getEndLoc(), ": <#value#>");
return true;
}
bool NoEscapeFuncToTypeConversionFailure::diagnoseAsError() {
if (diagnoseParameterUse())
return true;
@@ -5129,16 +5150,27 @@ bool CollectionElementContextualFailure::diagnoseAsError() {
auto eltType = getFromType();
auto contextualType = getToType();
auto isFixedToDictionary = [&](ArrayExpr *anchor) {
return llvm::any_of(getSolution().Fixes, [&](ConstraintFix *fix) {
auto *fixAnchor = getAsExpr<ArrayExpr>(fix->getAnchor());
return fixAnchor && fixAnchor == anchor &&
fix->getKind() == FixKind::TreatArrayLiteralAsDictionary;
});
};
bool treatAsDictionary = false;
Optional<InFlightDiagnostic> diagnostic;
if (isExpr<ArrayExpr>(anchor)) {
if (auto *AE = getAsExpr<ArrayExpr>(anchor)) {
if (!(treatAsDictionary = isFixedToDictionary(AE))) {
if (diagnoseMergedLiteralElements())
return true;
diagnostic.emplace(emitDiagnostic(diag::cannot_convert_array_element,
eltType, contextualType));
}
}
if (isExpr<DictionaryExpr>(anchor)) {
if (treatAsDictionary || isExpr<DictionaryExpr>(anchor)) {
auto eltLoc = locator->castLastElementTo<LocatorPathElt::TupleElement>();
switch (eltLoc.getIndex()) {
case 0: // key

View File

@@ -690,6 +690,18 @@ protected:
getDiagnosticFor(ContextualTypePurpose context, Type contextualType);
};
/// Diagnose errors related to using an array literal where a
/// dictionary is expected.
class ArrayLiteralToDictionaryConversionFailure final : public ContextualFailure {
public:
ArrayLiteralToDictionaryConversionFailure(const Solution &solution,
Type arrayTy, Type dictTy,
ConstraintLocator *locator)
: ContextualFailure(solution, arrayTy, dictTy, locator) {}
bool diagnoseAsError() override;
};
/// Diagnose errors related to converting function type which
/// isn't explicitly '@escaping' to some other type.
class NoEscapeFuncToTypeConversionFailure final : public ContextualFailure {

View File

@@ -162,6 +162,23 @@ CoerceToCheckedCast *CoerceToCheckedCast::attempt(ConstraintSystem &cs,
CoerceToCheckedCast(cs, fromType, toType, locator);
}
bool TreatArrayLiteralAsDictionary::diagnose(const Solution &solution,
bool asNote) const {
ArrayLiteralToDictionaryConversionFailure failure(solution,
getToType(), getFromType(),
getLocator());
return failure.diagnose(asNote);
};
TreatArrayLiteralAsDictionary *
TreatArrayLiteralAsDictionary::create(ConstraintSystem &cs,
Type dictionaryTy, Type arrayTy,
ConstraintLocator *locator) {
assert(getAsExpr<ArrayExpr>(locator->getAnchor())->getNumElements() <= 1);
return new (cs.getAllocator())
TreatArrayLiteralAsDictionary(cs, dictionaryTy, arrayTy, locator);
};
bool MarkExplicitlyEscaping::diagnose(const Solution &solution,
bool asNote) const {
NoEscapeFuncToTypeConversionFailure failure(solution, getFromType(),

View File

@@ -1744,13 +1744,10 @@ namespace {
auto &DE = CS.getASTContext().Diags;
auto numElements = expr->getNumElements();
if (numElements == 0) {
DE.diagnose(expr->getStartLoc(),
diag::should_use_empty_dictionary_literal)
.fixItInsert(expr->getEndLoc(), ":");
return nullptr;
}
// Empty and single element array literals with dictionary contextual
// types are fixed during solving, so continue as normal in those
// cases.
if (numElements > 1) {
bool isIniting =
CS.getContextualTypePurpose(expr) == CTP_Initialization;
DE.diagnose(expr->getStartLoc(), diag::should_use_dictionary_literal,
@@ -1772,6 +1769,7 @@ namespace {
return nullptr;
}
}
auto arrayTy = CS.createTypeVariable(locator,
TVO_PrefersSubtypeBinding |

View File

@@ -3206,6 +3206,58 @@ repairViaOptionalUnwrap(ConstraintSystem &cs, Type fromType, Type toType,
return true;
}
static bool repairArrayLiteralUsedAsDictionary(
ConstraintSystem &cs, Type arrayType, Type dictType,
ConstraintKind matchKind,
SmallVectorImpl<RestrictionOrFix> &conversionsOrFixes,
ConstraintLocator *loc) {
if (!cs.isArrayType(arrayType))
return false;
// Determine the ArrayExpr from the locator.
auto *expr = getAsExpr(simplifyLocatorToAnchor(loc));
if (!expr)
return false;
if (auto *AE = dyn_cast<AssignExpr>(expr))
expr = AE->getSrc();
auto *arrayExpr = dyn_cast<ArrayExpr>(expr);
if (!arrayExpr)
return false;
// This fix currently only handles empty and single-element arrays:
// [] => [:] and [1] => [1:_]
if (arrayExpr->getNumElements() > 1)
return false;
// This fix only applies if the array is used as a dictionary.
auto unwrappedDict = dictType->lookThroughAllOptionalTypes();
if (unwrappedDict->isTypeVariableOrMember())
return false;
if (!conformsToKnownProtocol(
cs.DC, unwrappedDict,
KnownProtocolKind::ExpressibleByDictionaryLiteral))
return false;
// Ignore any attempts at promoting the value to an optional as even after
// stripping off all optionals above the underlying types don't match (array
// vs dictionary).
conversionsOrFixes.erase(llvm::remove_if(conversionsOrFixes,
[&](RestrictionOrFix &E) {
if (auto restriction = E.getRestriction())
return *restriction == ConversionRestrictionKind::ValueToOptional;
return false;
}), conversionsOrFixes.end());
auto argLoc = cs.getConstraintLocator(arrayExpr);
conversionsOrFixes.push_back(TreatArrayLiteralAsDictionary::create(
cs, dictType, arrayType, argLoc));
return true;
}
/// Let's check whether this is an out-of-order argument in binary
/// operator/function with concrete type parameters e.g.
/// `func ^^(x: Int, y: String)` called as `"" ^^ 42` instead of
@@ -3481,6 +3533,11 @@ bool ConstraintSystem::repairFailures(
});
};
if (repairArrayLiteralUsedAsDictionary(*this, lhs, rhs, matchKind,
conversionsOrFixes,
getConstraintLocator(locator)))
return true;
if (path.empty()) {
if (!anchor)
return false;
@@ -10185,6 +10242,44 @@ ConstraintSystem::SolutionKind ConstraintSystem::simplifyFixConstraint(
return recordFix(fix, impact) ? SolutionKind::Error : SolutionKind::Solved;
}
case FixKind::TreatArrayLiteralAsDictionary: {
ArrayExpr *AE = getAsExpr<ArrayExpr>(fix->getAnchor());
assert(AE);
// If the array was empty, there's nothing to do.
if (AE->getNumElements() == 0)
return recordFix(fix) ? SolutionKind::Error : SolutionKind::Solved;
// For arrays with a single element, match the element type to the
// dictionary's key type.
SmallVector<Type, 2> optionals;
auto dictTy = type2->lookThroughAllOptionalTypes(optionals);
// If the fix is worse than the best solution, there's no point continuing.
if (recordFix(fix, optionals.size() + 1))
return SolutionKind::Error;
// Extract the dictionary key type.
ProtocolDecl *dictionaryProto =
Context.getProtocol(KnownProtocolKind::ExpressibleByDictionaryLiteral);
auto keyAssocTy = dictionaryProto->getAssociatedType(Context.Id_Key);
auto valueBaseTy = createTypeVariable(getConstraintLocator(locator),
TVO_CanBindToLValue |
TVO_CanBindToNoEscape |
TVO_CanBindToHole);
assignFixedType(valueBaseTy, dictTy);
auto dictionaryKeyTy = DependentMemberType::get(valueBaseTy, keyAssocTy);
// Extract the array element type.
auto elemTy = isArrayType(type1);
ConstraintLocator *elemLoc = getConstraintLocator(AE->getElement(0));
ConstraintKind kind = isDictionaryType(dictTy)
? ConstraintKind::Conversion
: ConstraintKind::Equal;
return matchTypes(*elemTy, dictionaryKeyTy, kind, subflags, elemLoc);
}
case FixKind::ContextualMismatch: {
auto impact = 1;

View File

@@ -71,6 +71,53 @@ var _: MyDictionary<String, Int>? = ["foo" : 1.0] // expected-error {{cannot co
// <rdar://problem/24058895> QoI: Should handle [] in dictionary contexts better
var _: [Int: Int] = [] // expected-error {{use [:] to get an empty dictionary literal}} {{22-22=:}}
var _ = useDictStringInt([]) // expected-error {{use [:] to get an empty dictionary literal}} {{27-27=:}}
var _: [[Int: Int]] = [[]] // expected-error {{use [:] to get an empty dictionary literal}} {{25-25=:}}
var _: [[Int: Int]?] = [[]] // expected-error {{use [:] to get an empty dictionary literal}} {{26-26=:}}
var assignDict = [1: 2]
assignDict = [] // expected-error {{use [:] to get an empty dictionary literal}} {{15-15=:}}
var _: [Int: Int] = [1] // expected-error {{dictionary of type '[Int : Int]' cannot be initialized with array literal}}
// expected-note@-1 {{did you mean to use a dictionary literal instead?}} {{23-23=: <#value#>}}
var _: [Float: Int] = [1] // expected-error {{dictionary of type '[Float : Int]' cannot be initialized with array literal}}
// expected-note@-1 {{did you mean to use a dictionary literal instead?}} {{25-25=: <#value#>}}
var _: [Int: Int] = ["foo"] // expected-error {{dictionary of type '[Int : Int]' cannot be initialized with array literal}}
// expected-note@-1 {{did you mean to use a dictionary literal instead?}} {{27-27=: <#value#>}}
// expected-error@-2 {{cannot convert value of type 'String' to expected dictionary key type 'Int'}}
var _ = useDictStringInt(["Key"]) // expected-error {{dictionary of type 'DictStringInt' cannot be used with array literal}}
// expected-note@-1 {{did you mean to use a dictionary literal instead?}} {{32-32=: <#value#>}}
var _ = useDictStringInt([4]) // expected-error {{dictionary of type 'DictStringInt' cannot be used with array literal}}
// expected-note@-1 {{did you mean to use a dictionary literal instead?}} {{28-28=: <#value#>}}
// expected-error@-2 {{cannot convert value of type 'Int' to expected dictionary key type 'DictStringInt.Key' (aka 'String')}}
var _: [[Int: Int]] = [[5]] // expected-error {{dictionary of type '[Int : Int]' cannot be used with array literal}}
// expected-note@-1 {{did you mean to use a dictionary literal instead?}} {{26-26=: <#value#>}}
var _: [[Int: Int]] = [["bar"]] // expected-error {{dictionary of type '[Int : Int]' cannot be used with array literal}}
// expected-note@-1 {{did you mean to use a dictionary literal instead?}} {{30-30=: <#value#>}}
// expected-error@-2 {{cannot convert value of type 'String' to expected dictionary key type 'Int'}}
assignDict = [1] // expected-error {{dictionary of type '[Int : Int]' cannot be used with array literal}}
// expected-note@-1 {{did you mean to use a dictionary literal instead?}} {{16-16=: <#value#>}}
assignDict = [""] // expected-error {{dictionary of type '[Int : Int]' cannot be used with array literal}}
// expected-note@-1 {{did you mean to use a dictionary literal instead?}} {{17-17=: <#value#>}}
// expected-error@-2 {{cannot convert value of type 'String' to expected dictionary key type 'Int'}}
func arrayLiteralDictionaryMismatch<T>(a: inout T) where T: ExpressibleByDictionaryLiteral, T.Key == Int, T.Value == Int {
a = [] // expected-error {{use [:] to get an empty dictionary literal}} {{8-8=:}}
a = [1] // expected-error {{dictionary of type 'T' cannot be used with array literal}}
// expected-note@-1 {{did you mean to use a dictionary literal instead?}} {{9-9=: <#value#>}}
a = [""] // expected-error {{dictionary of type 'T' cannot be used with array literal}}
// expected-note@-1 {{did you mean to use a dictionary literal instead?}} {{10-10=: <#value#>}}
// expected-error@-2 {{cannot convert value of type 'String' to expected dictionary key type 'Int'}}
}
class A { }

View File

@@ -14,6 +14,18 @@
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=UNAMBIGUOUSCLOSURE_ARG | %FileCheck %s --check-prefix=UNAMBIGUOUSCLOSURE_ARG
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=AMBIGUOUSCLOSURE_ARG | %FileCheck %s --check-prefix=AMBIGUOUSCLOSURE_ARG
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=AMBIGUOUSCLOSURE_ARG_RETURN | %FileCheck %s --check-prefix=AMBIGUOUSCLOSURE_ARG_RETURN
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=PARSED_AS_ARRAY | %FileCheck %s --check-prefix=PARSED_AS_ARRAY_KEY
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=PARSED_AS_ARRAY_OPTIONAL | %FileCheck %s --check-prefix=PARSED_AS_ARRAY_KEY
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=PARSED_AS_ARRAY_NESTED | %FileCheck %s --check-prefix=PARSED_AS_ARRAY_KEY
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=PARSED_AS_ARRAY_ASSIGN | %FileCheck %s --check-prefix=PARSED_AS_ARRAY_KEY
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=PARSED_AS_ARRAY_INDIRECT | %FileCheck %s --check-prefix=PARSED_AS_ARRAY_KEY
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=PARSED_AS_ARRAY_INDIRECT_NESTED | %FileCheck %s --check-prefix=PARSED_AS_ARRAY_KEY
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=PARSED_AS_ARRAY_INDIRECT_CALL | %FileCheck %s --check-prefix=PARSED_AS_ARRAY_KEY
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=PARSED_AS_ARRAY_INDIRECT_CALL_OPT | %FileCheck %s --check-prefix=PARSED_AS_ARRAY_KEY
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=PARSED_AS_ARRAY_INDIRECT_RETURN | %FileCheck %s --check-prefix=PARSED_AS_ARRAY_KEY
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=PARSED_AS_ARRAY_GENERIC | %FileCheck %s --check-prefix=PARSED_AS_ARRAY_KEY
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=PARSED_AS_ARRAY_TUPLE | %FileCheck %s --check-prefix=PARSED_AS_ARRAY_TUPLE
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=PARSED_AS_ARRAY_ARRAY | %FileCheck %s --check-prefix=PARSED_AS_ARRAY_TUPLE
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=OVERLOADEDFUNC_FOO | %FileCheck %s --check-prefix=OVERLOADEDFUNC_FOO
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=OVERLOADEDFUNC_BAR | %FileCheck %s --check-prefix=OVERLOADEDFUNC_BAR
// RUN: %swift-ide-test -code-completion -source-filename %s -code-completion-token=OVERLOADEDFUNC_MISSINGLABEL | %FileCheck %s --check-prefix=OVERLOADEDFUNC_BOTH
@@ -165,6 +177,45 @@ takesAnonClosure { TestRelations.#^AMBIGUOUSCLOSURE_ARG_RETURN^# }
// AMBIGUOUSCLOSURE_ARG_RETURN-DAG: Decl[Constructor]/CurrNominal: init()[#TestRelations#]{{; name=.+$}}
// AMBIGUOUSCLOSURE_ARG_RETURN: End completions
func takesDictAB(_ x: [A: B]) {}
func takesOptDictAB(_ x: [A: B]?) {}
func overloadedGivesAorB(_ x: A) -> A {}
func overloadedGivesAorB(_ x: B) -> B {}
var assignDict: [A : B] = [:]
let _: [A : B] = [TestRelations.#^PARSED_AS_ARRAY^#]
let _: [A : B]? = [TestRelations.#^PARSED_AS_ARRAY_OPTIONAL^#]
let _: [[A : B]] = [[TestRelations.#^PARSED_AS_ARRAY_NESTED^#]]
assignDict = [TestRelations.#^PARSED_AS_ARRAY_ASSIGN^#]
let _: [A: B] = [overloadedGivesAorB(TestRelations.#^PARSED_AS_ARRAY_INDIRECT^#)]
let _: [[A: B]] = [[overloadedGivesAorB(TestRelations.#^PARSED_AS_ARRAY_INDIRECT_NESTED^#)]]
takesDictAB([overloadedGivesAorB(TestRelations.#^PARSED_AS_ARRAY_INDIRECT_CALL^#)]);
takesOptDictAB([overloadedGivesAorB(TestRelations.#^PARSED_AS_ARRAY_INDIRECT_CALL_OPT^#)]);
func testReturnLiteralMismatch() -> [A: B] { return [overloadedGivesAorB(TestRelations.#^PARSED_AS_ARRAY_INDIRECT_RETURN^#)] }
func arrayLiteralDictionaryMismatch<T>(a: inout T) where T: ExpressibleByDictionaryLiteral, T.Key == A, T.Value == B {
a = [TestRelations.#^PARSED_AS_ARRAY_GENERIC^#]
}
// PARSED_AS_ARRAY_KEY: Begin completions, 6 items
// PARSED_AS_ARRAY_KEY-DAG: Keyword[self]/CurrNominal: self[#TestRelations.Type#]{{; name=.+$}}
// PARSED_AS_ARRAY_KEY-DAG: Keyword/CurrNominal: Type[#TestRelations.Type#]{{; name=.+$}}
// PARSED_AS_ARRAY_KEY-DAG: Decl[StaticVar]/CurrNominal/TypeRelation[Identical]: a[#A#]{{; name=.+$}}
// PARSED_AS_ARRAY_KEY-DAG: Decl[StaticVar]/CurrNominal: b[#B#]{{; name=.+$}}
// PARSED_AS_ARRAY_KEY-DAG: Decl[StaticVar]/CurrNominal: ab[#(A, B)#]{{; name=.+$}}
// PARSED_AS_ARRAY_KEY-DAG: Decl[Constructor]/CurrNominal: init()[#TestRelations#]{{; name=.+$}}
// PARSED_AS_ARRAY_KEY: End completions
let _: [(A, B) : B] = [TestRelations.#^PARSED_AS_ARRAY_TUPLE^#]
let _: [(A, B)] = [TestRelations.#^PARSED_AS_ARRAY_ARRAY^#]
// PARSED_AS_ARRAY_TUPLE: Begin completions, 6 items
// PARSED_AS_ARRAY_TUPLE-DAG: Keyword[self]/CurrNominal: self[#TestRelations.Type#]{{; name=.+$}}
// PARSED_AS_ARRAY_TUPLE-DAG: Keyword/CurrNominal: Type[#TestRelations.Type#]{{; name=.+$}}
// PARSED_AS_ARRAY_TUPLE-DAG: Decl[StaticVar]/CurrNominal: a[#A#]{{; name=.+$}}
// PARSED_AS_ARRAY_TUPLE-DAG: Decl[StaticVar]/CurrNominal: b[#B#]{{; name=.+$}}
// PARSED_AS_ARRAY_TUPLE-DAG: Decl[StaticVar]/CurrNominal/TypeRelation[Identical]: ab[#(A, B)#]{{; name=.+$}}
// PARSED_AS_ARRAY_TUPLE-DAG: Decl[Constructor]/CurrNominal: init()[#TestRelations#]{{; name=.+$}}
// PARSED_AS_ARRAY_TUPLE: End completions
func testMissingArgs() {
enum Foo { case foo }
@@ -344,4 +395,3 @@ CreateThings {
}
Thing. // ErrorExpr
}