From 38f534b8e5da26aac4e56d633f5345209b9d2d22 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 18 May 2016 22:06:28 -0700 Subject: [PATCH 1/5] SE-0062: Implement #keyPath expression. Implement the Objective-C #keyPath expression, which maps a sequence of @objc property accesses to a key-path suitable for use with Cocoa[Touch]. The implementation handles @objc properties of types that are either @objc or can be bridged to Objective-C, including the collections that work with key-value coding (Array/NSArray, Dictionary/NSDictionary, Set/NSSet). Still to come: code completion support and Fix-Its to migrate string literal keypaths to #keyPath. Implements the bulk of SR-1237 / rdar://problem/25710611. --- include/swift/AST/DiagnosticsParse.def | 14 +- include/swift/AST/DiagnosticsSema.def | 22 +- include/swift/AST/Expr.h | 119 +++++++++ include/swift/AST/ExprNodes.def | 1 + include/swift/Parse/Parser.h | 1 + include/swift/Parse/Tokens.def | 1 + lib/AST/ASTDumper.cpp | 18 ++ lib/AST/ASTWalker.cpp | 5 + lib/AST/Expr.cpp | 42 +++ lib/Parse/ParseExpr.cpp | 85 +++++- lib/SILGen/SILGenExpr.cpp | 5 + lib/Sema/CMakeLists.txt | 1 + lib/Sema/CSApply.cpp | 9 +- lib/Sema/CSGen.cpp | 12 + lib/Sema/TypeCheckExprObjC.cpp | 341 ++++++++++++++++++++++++ lib/Sema/TypeCheckStmt.cpp | 7 + lib/Sema/TypeChecker.h | 3 + test/Interpreter/SDK/objc_keypath.swift | 70 +++++ test/SILGen/objc_keypath.swift | 17 ++ test/expr/unary/keypath/keypath.swift | 112 ++++++++ 20 files changed, 880 insertions(+), 5 deletions(-) create mode 100644 lib/Sema/TypeCheckExprObjC.cpp create mode 100644 test/Interpreter/SDK/objc_keypath.swift create mode 100644 test/SILGen/objc_keypath.swift create mode 100644 test/expr/unary/keypath/keypath.swift diff --git a/include/swift/AST/DiagnosticsParse.def b/include/swift/AST/DiagnosticsParse.def index 5ff9fc18d8390..43f7988700c70 100644 --- a/include/swift/AST/DiagnosticsParse.def +++ b/include/swift/AST/DiagnosticsParse.def @@ -1081,11 +1081,23 @@ ERROR(expected_type_after_as,none, ERROR(string_interpolation_extra,none, "extra tokens after interpolated string expression", ()) +// Keypath expressions. +ERROR(expr_keypath_expected_lparen,PointsToFirstBadToken, + "expected '(' following '#keyPath'", ()) +ERROR(expr_keypath_expected_property_or_type,PointsToFirstBadToken, + "expected property or type name within '#keyPath(...)'", ()) +ERROR(expr_keypath_expected_rparen,PointsToFirstBadToken, + "expected ')' to complete '#keyPath' expression", ()) +ERROR(expr_keypath_compound_name,none, + "cannot use compound name %0 in '#keyPath' expression", (DeclName)) + // Selector expressions. ERROR(expr_selector_expected_lparen,PointsToFirstBadToken, "expected '(' following '#selector'", ()) -ERROR(expr_selector_expected_expr,PointsToFirstBadToken, +ERROR(expr_selector_expected_method_expr,PointsToFirstBadToken, "expected expression naming a method within '#selector(...)'", ()) +ERROR(expr_selector_expected_property_expr,PointsToFirstBadToken, + "expected expression naming a property within '#selector(...)'", ()) ERROR(expr_selector_expected_rparen,PointsToFirstBadToken, "expected ')' to complete '#selector' expression", ()) diff --git a/include/swift/AST/DiagnosticsSema.def b/include/swift/AST/DiagnosticsSema.def index 372c2f6fca002..a4a95bea9720f 100644 --- a/include/swift/AST/DiagnosticsSema.def +++ b/include/swift/AST/DiagnosticsSema.def @@ -368,6 +368,26 @@ ERROR(noescape_functiontype_mismatch,none, "invalid conversion from non-escaping function of type %0 to " "potentially escaping function type %1", (Type, Type)) +// Key-path expressions. +ERROR(expr_keypath_no_objc_runtime,none, + "'#keyPath' can only be used with the Objective-C runtime", ()) +ERROR(expression_unused_keypath_result,none, + "result of '#keyPath' is unused", ()) +ERROR(expr_keypath_non_objc_property,none, + "argument of '#keyPath' refers to non-'@objc' property %0", + (DeclName)) +ERROR(stdlib_anyobject_not_found,none, + "broken standard library: cannot find 'AnyObject' protocol", ()) +ERROR(expr_keypath_type_of_property,none, + "cannot refer to type member %0 within instance of type %1", + (DeclName, Type)) +ERROR(expr_keypath_generic_type,none, + "'#keyPath' cannot refer to generic type %0", (DeclName)) +ERROR(expr_keypath_not_property,none, + "'#keyPath' cannot refer to %0 %1", (DescriptiveDeclKind, DeclName)) +ERROR(expr_keypath_empty,none, + "empty '#keyPath' does not refer to a property", ()) + // Selector expressions. ERROR(expr_selector_no_objc_runtime,none, "'#selector' can only be used with the Objective-C runtime", ()) @@ -401,7 +421,7 @@ ERROR(expr_selector_not_objc,none, "argument of '#selector' refers to %0 %1 that is not exposed to " "Objective-C", (DescriptiveDeclKind, DeclName)) -NOTE(expr_selector_make_objc,none, +NOTE(make_decl_objc,none, "add '@objc' to expose this %0 to Objective-C", (DescriptiveDeclKind)) diff --git a/include/swift/AST/Expr.h b/include/swift/AST/Expr.h index 8d4ae4c41a7f2..453281c281262 100644 --- a/include/swift/AST/Expr.h +++ b/include/swift/AST/Expr.h @@ -311,6 +311,19 @@ class alignas(8) Expr { enum { NumObjCSelectorExprBits = NumExprBits + 2 }; static_assert(NumObjCSelectorExprBits <= 32, "fits in an unsigned"); + class ObjCKeyPathExprBitfields { + friend class ObjCKeyPathExpr; + unsigned : NumExprBits; + + /// The number of components in the selector path. + unsigned NumComponents : 8; + + /// Whether the names have corresponding source locations. + unsigned HaveSourceLocations : 1; + }; + enum { NumObjCKeyPathExprBits = NumExprBits + 17 }; + static_assert(NumObjCKeyPathExprBits <= 32, "fits in an unsigned"); + protected: union { ExprBitfields ExprBits; @@ -333,6 +346,7 @@ class alignas(8) Expr { CollectionUpcastConversionExprBitfields CollectionUpcastConversionExprBits; TupleShuffleExprBitfields TupleShuffleExprBits; ObjCSelectorExprBitfields ObjCSelectorExprBits; + ObjCKeyPathExprBitfields ObjCKeyPathExprBits; }; private: @@ -3860,6 +3874,111 @@ class ObjCSelectorExpr : public Expr { } }; +/// Produces a keypath string for the given referenced property. +/// +/// \code +/// #keyPath(Person.friends.firstName) +/// \endcode +class ObjCKeyPathExpr : public Expr { + SourceLoc KeywordLoc; + SourceLoc LParenLoc; + SourceLoc RParenLoc; + Expr *SemanticExpr = nullptr; + + /// A single stored component, which will be either an identifier or + /// a resolved declaration. + typedef llvm::PointerUnion StoredComponent; + + ObjCKeyPathExpr(SourceLoc keywordLoc, SourceLoc lParenLoc, + ArrayRef names, + ArrayRef nameLocs, + SourceLoc rParenLoc); + + /// Retrieve a mutable version of the "components" array, for + /// initialization purposes. + MutableArrayRef getComponentsMutable() { + return { reinterpret_cast(this + 1), getNumComponents() }; + } + + /// Retrieve the "components" storage. + ArrayRef getComponents() const { + return { reinterpret_cast(this + 1), + getNumComponents() }; + } + + /// Retrieve a mutable version of the name locations array, for + /// initialization purposes. + MutableArrayRef getNameLocsMutable() { + if (!ObjCKeyPathExprBits.HaveSourceLocations) return { }; + + auto mutableComponents = getComponentsMutable(); + return { reinterpret_cast(mutableComponents.end()), + mutableComponents.size() }; + } + +public: + /// Create a new #keyPath expression. + /// + /// \param nameLocs The locations of the names in the key-path, + /// which must either have the same number of entries as \p names or + /// must be empty. + static ObjCKeyPathExpr *create(ASTContext &ctx, + SourceLoc keywordLoc, SourceLoc lParenLoc, + ArrayRef names, + ArrayRef nameLocs, + SourceLoc rParenLoc); + + SourceLoc getLoc() const { return KeywordLoc; } + SourceRange getSourceRange() const { + return SourceRange(KeywordLoc, RParenLoc); + } + + /// Retrieve the number of components in the key-path. + unsigned getNumComponents() const { + return ObjCKeyPathExprBits.NumComponents; + } + + /// Retrieve's the name for the (i)th component; + Identifier getComponentName(unsigned i) const; + + /// Retrieve's the declaration corresponding to the (i)th component, + /// or null if this component has not yet been resolved. + ValueDecl *getComponentDecl(unsigned i) const { + return getComponents()[i].dyn_cast(); + } + + /// Retrieve the location corresponding to the (i)th name. + /// + /// If no location information is available, returns an empty + /// \c DeclNameLoc. + SourceLoc getComponentNameLoc(unsigned i) const { + if (!ObjCKeyPathExprBits.HaveSourceLocations) return { }; + + auto components = getComponents(); + ArrayRef nameLocs( + reinterpret_cast(components.end()), + components.size()); + + return nameLocs[i]; + } + + /// Retrieve the semantic expression, which will be \c NULL prior to + /// type checking and a string literal after type checking. + Expr *getSemanticExpr() const { return SemanticExpr; } + + /// Set the semantic expression. + void setSemanticExpr(Expr *expr) { SemanticExpr = expr; } + + /// Resolve the given component to the given declaration. + void resolveComponent(unsigned idx, ValueDecl *decl) { + getComponentsMutable()[idx] = decl; + } + + static bool classof(const Expr *E) { + return E->getKind() == ExprKind::ObjCKeyPath; + } +}; + #undef SWIFT_FORWARD_SOURCE_LOCS_TO } // end namespace swift diff --git a/include/swift/AST/ExprNodes.def b/include/swift/AST/ExprNodes.def index b0071c48ac0f4..276d8f6ca8eec 100644 --- a/include/swift/AST/ExprNodes.def +++ b/include/swift/AST/ExprNodes.def @@ -158,6 +158,7 @@ EXPR(CodeCompletion, Expr) UNCHECKED_EXPR(UnresolvedPattern, Expr) EXPR(EditorPlaceholder, Expr) EXPR(ObjCSelector, Expr) +EXPR(ObjCKeyPath, Expr) #undef EXPR_RANGE #undef UNCHECKED_EXPR diff --git a/include/swift/Parse/Parser.h b/include/swift/Parse/Parser.h index 9159b6c1df2a0..45b67e668af93 100644 --- a/include/swift/Parse/Parser.h +++ b/include/swift/Parse/Parser.h @@ -1109,6 +1109,7 @@ class Parser { bool isExprBasic); ParserResult parseExprPostfix(Diag<> ID, bool isExprBasic); ParserResult parseExprUnary(Diag<> ID, bool isExprBasic); + ParserResult parseExprKeyPath(); ParserResult parseExprSelector(); ParserResult parseExprSuper(); ParserResult parseExprConfiguration(); diff --git a/include/swift/Parse/Tokens.def b/include/swift/Parse/Tokens.def index 288190bbe585c..33d7c104cf15e 100644 --- a/include/swift/Parse/Tokens.def +++ b/include/swift/Parse/Tokens.def @@ -195,6 +195,7 @@ POUND_KEYWORD(if) POUND_KEYWORD(else) POUND_KEYWORD(elseif) POUND_KEYWORD(endif) +POUND_KEYWORD(keyPath) POUND_KEYWORD(line) POUND_KEYWORD(setline) POUND_KEYWORD(sourceLocation) diff --git a/lib/AST/ASTDumper.cpp b/lib/AST/ASTDumper.cpp index cba9f7a435456..7b1d683a6a836 100644 --- a/lib/AST/ASTDumper.cpp +++ b/lib/AST/ASTDumper.cpp @@ -2226,6 +2226,24 @@ class PrintExpr : public ExprVisitor { printRec(E->getSubExpr()); OS << ')'; } + + void visitObjCKeyPathExpr(ObjCKeyPathExpr *E) { + printCommon(E, "keypath_expr"); + for (unsigned i = 0, n = E->getNumComponents(); i != n; ++i) { + OS << "\n"; + OS.indent(Indent + 2); + OS << "component="; + if (auto decl = E->getComponentDecl(i)) + decl->dumpRef(OS); + else + OS << E->getComponentName(i); + } + if (auto semanticE = E->getSemanticExpr()) { + OS << '\n'; + printRec(semanticE); + } + OS << ")"; + } }; } // end anonymous namespace. diff --git a/lib/AST/ASTWalker.cpp b/lib/AST/ASTWalker.cpp index e7f1e1511f2ac..0001e7226fe82 100644 --- a/lib/AST/ASTWalker.cpp +++ b/lib/AST/ASTWalker.cpp @@ -833,6 +833,11 @@ class Traversal : public ASTVisitorgetInstanceType(); return type->castTo(); } + +ObjCKeyPathExpr::ObjCKeyPathExpr(SourceLoc keywordLoc, SourceLoc lParenLoc, + ArrayRef names, + ArrayRef nameLocs, + SourceLoc rParenLoc) + : Expr(ExprKind::ObjCKeyPath, /*Implicit=*/nameLocs.empty()), + KeywordLoc(keywordLoc), LParenLoc(lParenLoc), RParenLoc(rParenLoc) +{ + // Copy components (which are all names). + ObjCKeyPathExprBits.NumComponents = names.size(); + for (auto idx : indices(names)) + getComponentsMutable()[idx] = names[idx]; + + assert(nameLocs.empty() || nameLocs.size() == names.size()); + ObjCKeyPathExprBits.HaveSourceLocations = !nameLocs.empty(); + if (ObjCKeyPathExprBits.HaveSourceLocations) { + memcpy(getNameLocsMutable().data(), nameLocs.data(), + nameLocs.size() * sizeof(SourceLoc)); + } +} + +Identifier ObjCKeyPathExpr::getComponentName(unsigned i) const { + if (auto decl = getComponentDecl(i)) + return decl->getFullName().getBaseName(); + + return getComponents()[i].get(); +} + +ObjCKeyPathExpr *ObjCKeyPathExpr::create(ASTContext &ctx, + SourceLoc keywordLoc, SourceLoc lParenLoc, + ArrayRef names, + ArrayRef nameLocs, + SourceLoc rParenLoc) { + unsigned size = sizeof(ObjCKeyPathExpr) + + names.size() * sizeof(Identifier) + + nameLocs.size() * sizeof(SourceLoc); + void *mem = ctx.Allocate(size, alignof(ObjCKeyPathExpr)); + return new (mem) ObjCKeyPathExpr(keywordLoc, lParenLoc, names, nameLocs, + rParenLoc); +} diff --git a/lib/Parse/ParseExpr.cpp b/lib/Parse/ParseExpr.cpp index 99a7a5594ade0..3d5e19704b9ec 100644 --- a/lib/Parse/ParseExpr.cpp +++ b/lib/Parse/ParseExpr.cpp @@ -501,6 +501,9 @@ ParserResult Parser::parseExprUnary(Diag<> Message, bool isExprBasic) { new (Context) InOutExpr(Loc, SubExpr.get(), Type())); } + case tok::pound_keyPath: + return parseExprKeyPath(); + case tok::pound_selector: return parseExprSelector(); @@ -549,6 +552,82 @@ ParserResult Parser::parseExprUnary(Diag<> Message, bool isExprBasic) { new (Context) PrefixUnaryExpr(Operator, SubExpr.get())); } +/// expr-keypath: +/// '#keyPath' '(' unqualified-name ('.' unqualified-name) * ')' +/// +ParserResult Parser::parseExprKeyPath() { + // Consume '#keyPath'. + SourceLoc keywordLoc = consumeToken(tok::pound_keyPath); + + // Parse the leading '('. + if (!Tok.is(tok::l_paren)) { + diagnose(Tok, diag::expr_keypath_expected_lparen); + return makeParserError(); + } + SourceLoc lParenLoc = consumeToken(tok::l_paren); + + // Parse the sequence of unqualified-names. + SmallVector names; + SmallVector nameLocs; + ParserStatus status; + while (true) { + // FIXME: Code completion. + + // Parse the next name. + DeclNameLoc nameLoc; + bool afterDot = !names.empty(); + auto name = parseUnqualifiedDeclName( + afterDot, nameLoc, + diag::expr_keypath_expected_property_or_type); + if (!name) { + status.setIsParseError(); + break; + } + + // Cannot use compound names here. + if (name.isCompoundName()) { + diagnose(nameLoc.getBaseNameLoc(), diag::expr_keypath_compound_name, + name) + .fixItReplace(nameLoc.getSourceRange(), name.getBaseName().str()); + } + + // Record the name we parsed. + names.push_back(name.getBaseName()); + nameLocs.push_back(nameLoc.getBaseNameLoc()); + + // Parse the next period to continue the path. + if (consumeIf(tok::period)) + continue; + + break; + } + + // Parse the closing ')'. + SourceLoc rParenLoc; + if (status.isError()) { + skipUntilDeclStmtRBrace(tok::r_paren); + if (Tok.is(tok::r_paren)) + rParenLoc = consumeToken(); + else + rParenLoc = Tok.getLoc(); + } else { + parseMatchingToken(tok::r_paren, rParenLoc, + diag::expr_keypath_expected_rparen, lParenLoc); + } + + // If we cannot build a useful expression, just return an error + // expression. + if (names.empty() || status.isError()) { + return makeParserResult( + new (Context) ErrorExpr(SourceRange(keywordLoc, rParenLoc))); + } + + // We're done: create the key-path expression. + return makeParserResult( + ObjCKeyPathExpr::create(Context, keywordLoc, lParenLoc, names, + nameLocs, rParenLoc)); +} + /// parseExprSelector /// /// expr-selector: @@ -568,7 +647,6 @@ ParserResult Parser::parseExprSelector() { SourceLoc lParenLoc = consumeToken(tok::l_paren); SourceLoc modifierLoc; - // Parse possible 'getter:' or 'setter:' modifiers, and determine // the kind of selector we're working with. ObjCSelectorExpr::ObjCSelectorKind selectorKind; @@ -602,7 +680,10 @@ ParserResult Parser::parseExprSelector() { // Parse the subexpression. CodeCompletionCallbacks::InObjCSelectorExprRAII InObjCSelectorExpr(CodeCompletion, selectorContext); - ParserResult subExpr = parseExpr(diag::expr_selector_expected_expr); + ParserResult subExpr = + parseExpr(selectorKind == ObjCSelectorExpr::Method + ? diag::expr_selector_expected_method_expr + : diag::expr_selector_expected_property_expr); if (subExpr.hasCodeCompletion()) return makeParserCodeCompletionResult(); diff --git a/lib/SILGen/SILGenExpr.cpp b/lib/SILGen/SILGenExpr.cpp index 7d64bd62194db..c786a6f1718e7 100644 --- a/lib/SILGen/SILGenExpr.cpp +++ b/lib/SILGen/SILGenExpr.cpp @@ -198,6 +198,7 @@ namespace { RValue visitObjectLiteralExpr(ObjectLiteralExpr *E, SGFContext C); RValue visitEditorPlaceholderExpr(EditorPlaceholderExpr *E, SGFContext C); RValue visitObjCSelectorExpr(ObjCSelectorExpr *E, SGFContext C); + RValue visitObjCKeyPathExpr(ObjCKeyPathExpr *E, SGFContext C); RValue visitMagicIdentifierLiteralExpr(MagicIdentifierLiteralExpr *E, SGFContext C); RValue visitCollectionExpr(CollectionExpr *E, SGFContext C); @@ -2025,6 +2026,10 @@ RValue RValueEmitter::visitObjCSelectorExpr(ObjCSelectorExpr *e, SGFContext C) { return RValue(SGF, e, ManagedValue::forUnmanaged(selectorValue)); } +RValue RValueEmitter::visitObjCKeyPathExpr(ObjCKeyPathExpr *E, SGFContext C) { + return visit(E->getSemanticExpr(), C); +} + static StringRef getMagicFunctionString(SILGenFunction &gen) { assert(gen.MagicFunctionName diff --git a/lib/Sema/CMakeLists.txt b/lib/Sema/CMakeLists.txt index 3be760033af9d..fdb53bb499065 100644 --- a/lib/Sema/CMakeLists.txt +++ b/lib/Sema/CMakeLists.txt @@ -29,6 +29,7 @@ add_swift_library(swiftSema TypeChecker.cpp TypeCheckError.cpp TypeCheckExpr.cpp + TypeCheckExprObjC.cpp TypeCheckGeneric.cpp TypeCheckNameLookup.cpp TypeCheckPattern.cpp diff --git a/lib/Sema/CSApply.cpp b/lib/Sema/CSApply.cpp index df2b8dc652c1b..6c59ea7876ce0 100644 --- a/lib/Sema/CSApply.cpp +++ b/lib/Sema/CSApply.cpp @@ -3568,7 +3568,7 @@ namespace { tc.diagnose(E->getLoc(), diag::expr_selector_not_objc, foundDecl->getDescriptiveKind(), foundDecl->getFullName()) .highlight(subExpr->getSourceRange()); - tc.diagnose(foundDecl, diag::expr_selector_make_objc, + tc.diagnose(foundDecl, diag::make_decl_objc, foundDecl->getDescriptiveKind()) .fixItInsert(foundDecl->getAttributeInsertionLoc(false), "@objc "); @@ -3580,6 +3580,13 @@ namespace { return E; } + Expr *visitObjCKeyPathExpr(ObjCKeyPathExpr *E) { + if (auto semanticE = E->getSemanticExpr()) + E->setType(semanticE->getType()); + + return E; + } + /// Interface for ExprWalker void walkToExprPre(Expr *expr) { ExprStack.push_back(expr); diff --git a/lib/Sema/CSGen.cpp b/lib/Sema/CSGen.cpp index 36a3d19488f7c..71a4b7d1c61a3 100644 --- a/lib/Sema/CSGen.cpp +++ b/lib/Sema/CSGen.cpp @@ -2832,6 +2832,7 @@ namespace { return nullptr; } + // Make sure we can reference ObjectiveC.Selector. // FIXME: Fix-It to add the import? auto type = CS.getTypeChecker().getObjCSelectorType(CS.DC); @@ -2842,6 +2843,10 @@ namespace { return type; } + + Type visitObjCKeyPathExpr(ObjCKeyPathExpr *E) { + return E->getSemanticExpr()->getType(); + } }; /// \brief AST walker that "sanitizes" an expression for the @@ -2916,6 +2921,13 @@ namespace { CG.getConstraintSystem().UnevaluatedRootExprs.insert(sel->getSubExpr()); } + // Check a key-path expression, which fills in its semantic + // expression as a string literal. + if (auto keyPath = dyn_cast(expr)) { + auto &cs = CG.getConstraintSystem(); + (void)cs.getTypeChecker().checkObjCKeyPathExpr(cs.DC, keyPath); + } + // For closures containing only a single expression, the body participates // in type checking. if (auto closure = dyn_cast(expr)) { diff --git a/lib/Sema/TypeCheckExprObjC.cpp b/lib/Sema/TypeCheckExprObjC.cpp new file mode 100644 index 0000000000000..5f6e4fc385fc5 --- /dev/null +++ b/lib/Sema/TypeCheckExprObjC.cpp @@ -0,0 +1,341 @@ +//===--- TypeCheckExprObjC.cpp - Type Checking for ObjC Expressions -------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +// +// This file implements semantic analysis for Objective-C-specific +// expressions. +// +//===----------------------------------------------------------------------===// +#include "TypeChecker.h" +#include "swift/Basic/Range.h" +using namespace swift; + +ObjCKeyPathExpr *TypeChecker::checkObjCKeyPathExpr(DeclContext *dc, + ObjCKeyPathExpr *expr) { + // If there is already a semantic expression, do nothing. + if (expr->getSemanticExpr()) return expr; + + // #keyPath only makes sense when we have the Objective-C runtime. + if (!Context.LangOpts.EnableObjCInterop) { + diagnose(expr->getLoc(), diag::expr_keypath_no_objc_runtime); + + expr->setSemanticExpr( + new (Context) StringLiteralExpr("", expr->getSourceRange(), + /*Implicit=*/true)); + return expr; + } + + // The key path string we're forming. + SmallString<32> keyPathScratch; + llvm::raw_svector_ostream keyPathOS(keyPathScratch); + + // Captures the state of semantic resolution. + enum State { + Beginning, + ResolvingType, + ResolvingProperty, + ResolvingArray, + ResolvingSet, + ResolvingDictionary, + } state = Beginning; + + /// Determine whether we are currently resolving a property. + auto isResolvingProperty = [&] { + switch (state) { + case Beginning: + case ResolvingType: + return false; + + case ResolvingProperty: + case ResolvingArray: + case ResolvingSet: + case ResolvingDictionary: + return true; + } + }; + + // The type of AnyObject, which is used whenever we don't have + // sufficient type information. + Type anyObjectType; + if (auto anyObject = Context.getProtocol(KnownProtocolKind::AnyObject)) { + validateDecl(anyObject); + anyObjectType = anyObject->getDeclaredInterfaceType(); + } else { + diagnose(expr->getLoc(), diag::stdlib_anyobject_not_found); + return expr; + } + + // Local function to update the state after we've resolved a + // component. + Type currentType; + auto updateState = [&](bool isProperty, Type newType) { + // Strip off optionals. + newType = newType->lookThroughAllAnyOptionalTypes(); + + // If updating to a type, just set the new type; there's nothing + // more to do. + if (!isProperty) { + assert(state == Beginning || state == ResolvingType); + state = ResolvingType; + currentType = newType; + return; + } + + // We're updating to a property. Determine whether we're looking + // into a bridged Swift collection of some sort. + if (auto boundGeneric = newType->getAs()) { + auto nominal = boundGeneric->getDecl(); + + // Array + if (nominal == Context.getArrayDecl()) { + // Further lookups into the element type. + state = ResolvingArray; + currentType = boundGeneric->getGenericArgs()[0]; + return; + } + + // Set + if (nominal == Context.getSetDecl()) { + // Further lookups into the element type. + state = ResolvingSet; + currentType = boundGeneric->getGenericArgs()[0]; + return; + } + + // Dictionary + if (nominal == Context.getDictionaryDecl()) { + // Key paths look into the keys of a dictionary; further + // lookups into the value type. + state = ResolvingDictionary; + currentType = boundGeneric->getGenericArgs()[1]; + return; + } + } + + // Determine whether we're looking into a Foundation collection. + if (auto classDecl = newType->getClassOrBoundGenericClass()) { + if (classDecl->isObjC() && classDecl->hasClangNode()) { + SmallString<32> scratch; + StringRef objcClassName = classDecl->getObjCRuntimeName(scratch); + + // NSArray + if (objcClassName == "NSArray") { + // The element type is unknown, so use AnyObject. + state = ResolvingArray; + currentType = anyObjectType; + return; + } + + // NSSet + if (objcClassName == "NSSet") { + // The element type is unknown, so use AnyObject. + state = ResolvingSet; + currentType = anyObjectType; + return; + } + + // NSDictionary + if (objcClassName == "NSDictionary") { + // Key paths look into the keys of a dictionary; there's no + // type to help us here. + state = ResolvingDictionary; + currentType = anyObjectType; + return; + } + } + } + + // It's just a property. + state = ResolvingProperty; + currentType = newType; + }; + + // Local function to perform name lookup for the current index. + auto performLookup = [&](unsigned idx, Identifier componentName, + SourceLoc componentNameLoc) -> LookupResult { + if (state == Beginning) + return lookupUnqualified(dc, componentName, componentNameLoc); + + assert(currentType && "Non-beginning state must have a type"); + + // Determine the type in which the lookup should occur. If we have + // a bridged value type, this will be the Objective-C class to + // which it is bridged. + Type lookupType; + if (auto bridgedClass = getBridgedToObjC(dc, currentType)) + lookupType = bridgedClass; + else + lookupType = currentType; + + // Look for a member with the given name within this type. + return lookupMember(dc, lookupType, componentName); + }; + + // Local function to print a component to the string. + bool needDot = false; + auto printComponent = [&](Identifier component) { + if (needDot) + keyPathOS << "."; + else + needDot = true; + + keyPathOS << component.str(); + }; + + bool isInvalid = false; + for (unsigned idx : range(expr->getNumComponents())) { + auto componentName = expr->getComponentName(idx); + auto componentNameLoc = expr->getComponentNameLoc(idx); + + // If we are resolving into a dictionary, any component is + // well-formed because the keys are unknown dynamically. + if (state == ResolvingDictionary) { + // Just print the component unchanged; there's no checking we + // can do here. + printComponent(componentName); + + // From here, we're resolving a property. Use the current type. + updateState(/*isProperty=*/true, currentType); + + continue; + } + + // Look for this component. + LookupResult lookup = performLookup(idx, componentName, componentNameLoc); + + // If we didn't find anything, complain and bail out. + if (!lookup) { + // FIXME: Typo correction. + if (currentType) + diagnose(componentNameLoc, diag::could_not_find_type_member, + currentType, componentName); + else + diagnose(componentNameLoc, diag::use_unresolved_identifier, + componentName, false); + isInvalid = true; + break; + } + + // If we have more than one result, filter out unavailable or + // obviously unusable candidates. + if (lookup.size() > 1) { + lookup.filter([&](LookupResult::Result result) -> bool { + // Drop unavailable candidates. + if (result->getAttrs().isUnavailable(Context)) + return false; + + // Drop non-property, non-type candidates. + if (!isa(result.Decl) && !isa(result.Decl)) + return false; + + return true; + }); + } + + // If we *still* have more than one result, fail. + if (lookup.size() > 1) { + if (currentType) + diagnose(componentNameLoc, diag::ambiguous_member_overload_set, + componentName); + else + diagnose(componentNameLoc, diag::ambiguous_decl_ref, + componentName); + + for (auto result : lookup) { + diagnose(result, diag::decl_declared_here, result->getFullName()); + } + isInvalid = true; + break; + } + + auto found = lookup.front().Decl; + + // Handle property references. + if (auto var = dyn_cast(found)) { + validateDecl(var); + + // Resolve this component to the variable we found. + expr->resolveComponent(idx, var); + updateState(/*isProperty=*/true, var->getType()->getRValueObjectType()); + + // Check that the property is @objc. + if (!var->isObjC()) { + diagnose(componentNameLoc, diag::expr_keypath_non_objc_property, + componentName); + if (var->getLoc().isValid() && var->getDeclContext()->isTypeContext()) { + diagnose(var, diag::make_decl_objc, + var->getDescriptiveKind()) + .fixItInsert(var->getAttributeInsertionLoc(false), + "@objc "); + } + } else { + // FIXME: Warn about non-KVC-compliant getter/setter names? + } + + // Print the Objective-C property name. + printComponent(var->getObjCPropertyName()); + continue; + } + + // Handle type references. + if (auto type = dyn_cast(found)) { + // We cannot refer to a type via a property. + if (isResolvingProperty()) { + diagnose(componentNameLoc, diag::expr_keypath_type_of_property, + componentName, currentType); + isInvalid = true; + break; + } + + // We cannot refer to a generic type. + if (type->getDeclaredInterfaceType()->hasTypeParameter()) { + diagnose(componentNameLoc, diag::expr_keypath_generic_type, + componentName); + isInvalid = true; + break; + } + + Type newType; + if (currentType && !currentType->isAnyObject()) { + newType = currentType->getTypeOfMember(dc->getParentModule(), type, + this); + } else { + newType = type->getDeclaredInterfaceType(); + } + if (!newType) { + isInvalid = true; + break; + } + + updateState(/*isProperty=*/false, newType); + continue; + } + + // Declarations that cannot be part of a key-path. + diagnose(componentNameLoc, diag::expr_keypath_not_property, + found->getDescriptiveKind(), found->getFullName()); + isInvalid = true; + break; + } + + // Check for an empty key-path string. + auto keyPathString = keyPathOS.str(); + if (keyPathString.empty() && !isInvalid) + diagnose(expr->getLoc(), diag::expr_keypath_empty); + + // Set the semantic expression. + expr->setSemanticExpr( + new (Context) StringLiteralExpr(Context.AllocateCopy(keyPathString), + expr->getSourceRange(), + /*Implicit=*/true)); + return expr; +} + diff --git a/lib/Sema/TypeCheckStmt.cpp b/lib/Sema/TypeCheckStmt.cpp index ea15cc105b6c3..8056b450f8396 100644 --- a/lib/Sema/TypeCheckStmt.cpp +++ b/lib/Sema/TypeCheckStmt.cpp @@ -1032,6 +1032,13 @@ void TypeChecker::checkIgnoredExpr(Expr *E) { .highlight(E->getSourceRange()); return; } + + // Complain about '#keyPath'. + if (isa(valueE)) { + diagnose(valueE->getLoc(), diag::expression_unused_keypath_result) + .highlight(E->getSourceRange()); + return; + } // Always complain about 'try?'. if (auto *OTE = dyn_cast(valueE)) { diff --git a/lib/Sema/TypeChecker.h b/lib/Sema/TypeChecker.h index 9a50308353c7b..39a76f91a5dd6 100644 --- a/lib/Sema/TypeChecker.h +++ b/lib/Sema/TypeChecker.h @@ -1175,6 +1175,9 @@ class TypeChecker final : public LazyResolver { /// \returns true if an error occurred, false otherwise. bool typeCheckExpressionShallow(Expr *&expr, DeclContext *dc); + /// Check the key-path expression. + ObjCKeyPathExpr *checkObjCKeyPathExpr(DeclContext *dc, ObjCKeyPathExpr *expr); + /// \brief Type check whether the given type declaration includes members of /// unsupported recursive value types. /// diff --git a/test/Interpreter/SDK/objc_keypath.swift b/test/Interpreter/SDK/objc_keypath.swift new file mode 100644 index 0000000000000..f400e21e556dd --- /dev/null +++ b/test/Interpreter/SDK/objc_keypath.swift @@ -0,0 +1,70 @@ +// RUN: %target-run-simple-swift | FileCheck %s +// REQUIRES: executable_test + +// REQUIRES: objc_interop + +import Foundation + +class Person : NSObject { + @objc(firstNameString) var firstName: String + var lastName: String + + init(firstName: String, lastName: String) { + self.firstName = firstName + self.lastName = lastName + } + + override var description: String { + return "\(lastName), \(firstName)" + } +} + +class Band : NSObject { + var members: [Person] = [] +} + +class RecordLabel : NSObject { + var bands: [String : Band] = [:] +} + +let band = Band() +band.members = [Person(firstName: "John", lastName: "Lennon"), + Person(firstName: "Paul", lastName: "McCartney"), + Person(firstName: "George", lastName: "Harrison"), + Person(firstName: "Ringo", lastName: "Star")] + +// CHECK: ===Members=== +// CHECK-NEXT: ( +// CHECK-NEXT: "Lennon, John", +// CHECK-NEXT "McCartney, Paul", +// CHECK-NEXT "Harrison, George", +// CHECK-NEXT "Star, Ringo" +// CHECK-NEXT) +print("===Members===") +print(band.value(forKeyPath: #keyPath(Band.members))!.description) + +// CHECK: ===First Names=== +// CHECK-NEXT: ( +// CHECK-NEXT: John, +// CHECK-NEXT: Paul, +// CHECK-NEXT: George, +// CHECK-NEXT: Ringo +// CHECK-NEXT:) +print("===First Names===") +print(band.value(forKeyPath: #keyPath(Band.members.firstName))!.description) + +let recordLabel = RecordLabel() +recordLabel.bands["Beatles"] = band + +// CHECK: ===Last Names=== +// CHECK-NEXT: ( +// CHECK-NEXT: Lennon, +// CHECK-NEXT: McCartney, +// CHECK-NEXT: Harrison, +// CHECK-NEXT: Star +// CHECK-NEXT: ) +print("===Last Names===") +print(recordLabel.value(forKeyPath: #keyPath(RecordLabel.bands.Beatles.members.lastName))!.description) + +// CHECK: DONE +print("DONE") diff --git a/test/SILGen/objc_keypath.swift b/test/SILGen/objc_keypath.swift new file mode 100644 index 0000000000000..d11517977d8ee --- /dev/null +++ b/test/SILGen/objc_keypath.swift @@ -0,0 +1,17 @@ +// RUN: %target-swift-frontend -emit-sil -sdk %S/Inputs -I %S/Inputs -enable-source-import %s | FileCheck %s + +// REQUIRES: objc_interop + +import ObjectiveC +import Foundation + +@objc class Foo : NSObject { + @objc(firstProp) var fooProp: Foo? + @objc(secondProp) var stringProp: String? +} + +// CHECK-LABEL: sil hidden @_TF12objc_keypath13createKeyPathFT_SS +func createKeyPath() -> String { + // CHECK: string_literal utf8 "firstProp.secondProp" + return #keyPath(Foo.fooProp.stringProp) +} diff --git a/test/expr/unary/keypath/keypath.swift b/test/expr/unary/keypath/keypath.swift new file mode 100644 index 0000000000000..4a4a4ed831a0b --- /dev/null +++ b/test/expr/unary/keypath/keypath.swift @@ -0,0 +1,112 @@ +// RUN: %target-swift-frontend(mock-sdk: %clang-importer-sdk) -parse -parse-as-library %s -verify +import ObjectiveC +import Foundation + +// REQUIRES: objc_interop + +@objc class A : NSObject { + @objc var propB: B = B() + @objc var propString: String = "" + @objc var propArray: [String] = [] + @objc var propDict: [String: B] = [:] + @objc var propSet: Set = [] + @objc var propNSString: NSString? + @objc var propNSArray: NSArray? + @objc var propNSDict: NSDictionary? + @objc var propNSSet: NSSet? + @objc var propAnyObject: AnyObject? + + @objc var ambiguous: String? // expected-note{{'ambiguous' declared here}} + + @objc func someMethod() { } + + @objc var `repeat`: String? +} + +@objc class B : NSObject { + @objc var propA: A? + + @objc var ambiguous: String? // expected-note{{'ambiguous' declared here}} +} + +class C { + var nonObjC: String? // expected-note{{add '@objc' to expose this var to Objective-C}}{{3-3=@objc }} +} + +func testKeyPath(a: A, b: B) { + // Property + let _: String = #keyPath(A.propB) + + // Chained property + let _: String = #keyPath(A.propB.propA) + + // Optional property + let _: String = #keyPath(A.propB.propA.propB) + + // String property + let _: String = #keyPath(A.propString) + + // Property of String property (which looks on NSString) + let _: String = #keyPath(A.propString.URLsInText) + + // Array property (make sure we look at the array element). + let _: String = #keyPath(A.propArray) + let _: String = #keyPath(A.propArray.URLsInText) + + // Dictionary property (make sure we look at the value type). + let _: String = #keyPath(A.propDict.anyKeyName) + let _: String = #keyPath(A.propDict.anyKeyName.propA) + + // Set property (make sure we look at the set element). + let _: String = #keyPath(A.propSet) + let _: String = #keyPath(A.propSet.URLsInText) + + // AnyObject property + let _: String = #keyPath(A.propAnyObject.URLsInText) + let _: String = #keyPath(A.propAnyObject.propA) + let _: String = #keyPath(A.propAnyObject.propB) + let _: String = #keyPath(A.propAnyObject.description) + + // NSString property + let _: String = #keyPath(A.propNSString.URLsInText) + + // NSArray property (AnyObject array element). + let _: String = #keyPath(A.propNSArray) + let _: String = #keyPath(A.propNSArray.URLsInText) + + // NSDictionary property (AnyObject value type). + let _: String = #keyPath(A.propNSDict.anyKeyName) + let _: String = #keyPath(A.propNSDict.anyKeyName.propA) + + // NSSet property (AnyObject set elemtn). + let _: String = #keyPath(A.propNSSet) + let _: String = #keyPath(A.propNSSet.URLsInText) + + // Property with keyword name. + let _: String = #keyPath(A.repeat) +} + +func testAsStaticString() { + let _: StaticString = #keyPath(A.propB) +} + +func testSemanticErrors() { + let _: String = #keyPath(A.blarg) // expected-error{{type 'A' has no member 'blarg'}} + let _: String = #keyPath(blarg) // expected-error{{use of unresolved identifier 'blarg'}} + let _: String = #keyPath(AnyObject.ambiguous) // expected-error{{ambiguous reference to member 'ambiguous'}} + let _: String = #keyPath(C.nonObjC) // expected-error{{argument of '#keyPath' refers to non-'@objc' property 'nonObjC'}} + let _: String = #keyPath(A.propArray.UTF8View) // expected-error{{type 'String' has no member 'UTF8View'}} + let _: String = #keyPath(A.someMethod) // expected-error{{'#keyPath' cannot refer to instance method 'someMethod()'}} + let _: String = #keyPath(A) // expected-error{{empty '#keyPath' does not refer to a property}} + let _: String = #keyPath(A.propDict.anyKeyName.unknown) // expected-error{{type 'B' has no member 'unknown'}} + let _: String = #keyPath(A.propNSDict.anyKeyName.unknown) // expected-error{{type 'AnyObject' has no member 'unknown'}} +} + +func testParseErrors() { + let _: String = #keyPath; // expected-error{{expected '(' following '#keyPath'}} + let _: String = #keyPath(123; // expected-error{{expected property or type name within '#keyPath(...)'}} + let _: String = #keyPath(a.123; // expected-error{{expected property or type name within '#keyPath(...)'}} + let _: String = #keyPath(A(b:c:d:).propSet); // expected-error{{cannot use compound name 'A(b:c:d:)' in '#keyPath' expression}} + let _: String = #keyPath(A.propString; // expected-error{{expected ')' to complete '#keyPath' expression}} + // expected-note@-1{{to match this opening '('}} +} From 354c77ed4adfe03f98d3f0eedd7a28328f64a1bd Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Fri, 20 May 2016 10:07:23 -0700 Subject: [PATCH 2/5] Fix a test for the #keyPath expression. --- test/Interpreter/SDK/objc_keypath.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/Interpreter/SDK/objc_keypath.swift b/test/Interpreter/SDK/objc_keypath.swift index f400e21e556dd..bc26f81f15bad 100644 --- a/test/Interpreter/SDK/objc_keypath.swift +++ b/test/Interpreter/SDK/objc_keypath.swift @@ -35,10 +35,10 @@ band.members = [Person(firstName: "John", lastName: "Lennon"), // CHECK: ===Members=== // CHECK-NEXT: ( -// CHECK-NEXT: "Lennon, John", -// CHECK-NEXT "McCartney, Paul", -// CHECK-NEXT "Harrison, George", -// CHECK-NEXT "Star, Ringo" +// CHECK-NEXT: Lennon, John +// CHECK-NEXT McCartney, Paul, +// CHECK-NEXT Harrison, George, +// CHECK-NEXT Star, Ringo // CHECK-NEXT) print("===Members===") print(band.value(forKeyPath: #keyPath(Band.members))!.description) From 6a951ca7907456f8c6b7c7381151896daa0235f5 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sat, 21 May 2016 22:26:04 -0700 Subject: [PATCH 3/5] [IDE] Code completion for Objective-C #keyPath expressions. Implement code completion support for Objective-C #keyPath expressions, using semantic analysis of the partially-typed keypath argument to provide an appropriate set of results (i.e., just properties and types). This implements all of the necessary parts of SE-0062 / SR-1237 / rdar://problem/25710611, although at some point I'd like to follow it up with some warnings to help migrate existing string literals to --- include/swift/IDE/CodeCompletion.h | 2 + include/swift/Parse/CodeCompletionCallbacks.h | 8 ++ include/swift/Sema/IDETypeChecking.h | 10 ++ lib/IDE/CodeCompletion.cpp | 108 ++++++++++++++++-- lib/Parse/ParseExpr.cpp | 26 ++++- lib/Sema/TypeCheckExprObjC.cpp | 26 +++-- lib/Sema/TypeChecker.cpp | 31 +++-- lib/Sema/TypeChecker.h | 5 +- test/IDE/complete_pound_keypath.swift | 46 ++++++++ 9 files changed, 231 insertions(+), 31 deletions(-) create mode 100644 test/IDE/complete_pound_keypath.swift diff --git a/include/swift/IDE/CodeCompletion.h b/include/swift/IDE/CodeCompletion.h index 320744827cc41..4427c45459c46 100644 --- a/include/swift/IDE/CodeCompletion.h +++ b/include/swift/IDE/CodeCompletion.h @@ -487,6 +487,8 @@ enum class CompletionKind { PostfixExprParen, SuperExpr, SuperExprDot, + KeyPathExpr, + KeyPathExprDot, TypeSimpleBeginning, TypeIdentifierWithDot, TypeIdentifierWithoutDot, diff --git a/include/swift/Parse/CodeCompletionCallbacks.h b/include/swift/Parse/CodeCompletionCallbacks.h index a47c0f805dbda..55a0de0f7b6bb 100644 --- a/include/swift/Parse/CodeCompletionCallbacks.h +++ b/include/swift/Parse/CodeCompletionCallbacks.h @@ -165,6 +165,14 @@ class CodeCompletionCallbacks { /// a dot. virtual void completeExprSuperDot(SuperRefExpr *SRE) = 0; + /// \brief Complete the argument to an Objective-C #keyPath + /// expression. + /// + /// \param KPE A partial #keyPath expression that can be used to + /// provide context. This will be \c NULL if no components of the + /// #keyPath argument have been parsed yet. + virtual void completeExprKeyPath(ObjCKeyPathExpr *KPE, bool HasDot) = 0; + /// \brief Complete the beginning of type-simple -- no tokens provided /// by user. virtual void completeTypeSimpleBeginning() = 0; diff --git a/include/swift/Sema/IDETypeChecking.h b/include/swift/Sema/IDETypeChecking.h index d67d1a69ae2f6..56cc8810698fc 100644 --- a/include/swift/Sema/IDETypeChecking.h +++ b/include/swift/Sema/IDETypeChecking.h @@ -77,10 +77,20 @@ namespace swift { /// \returns True on applied, false on not applied. bool isExtensionApplied(DeclContext &DC, Type Ty, const ExtensionDecl *ED); +/// The kind of type checking to perform for code completion. + enum class CompletionTypeCheckKind { + /// Type check the expression as normal. + Normal, + + /// Type check the argument to an Objective-C #keyPath. + ObjCKeyPath, + }; + /// \brief Return the type of an expression parsed during code completion, or /// None on error. Optional getTypeOfCompletionContextExpr(ASTContext &Ctx, DeclContext *DC, + CompletionTypeCheckKind kind, Expr *&parsedExpr); /// Typecheck the sequence expression \p parsedExpr for code completion. diff --git a/lib/IDE/CodeCompletion.cpp b/lib/IDE/CodeCompletion.cpp index 8ddcdf68a12c6..c55ccbaaf224d 100644 --- a/lib/IDE/CodeCompletion.cpp +++ b/lib/IDE/CodeCompletion.cpp @@ -280,6 +280,10 @@ void getSwiftDocKeyword(const Decl* D, CommandWordsPairs &Words) { typedef llvm::function_ref DeclFilter; DeclFilter DefaultFilter = [] (ValueDecl* VD, DeclVisibilityKind Kind) {return true;}; +DeclFilter KeyPathFilter = [](ValueDecl* decl, DeclVisibilityKind) -> bool { + return isa(decl) || + (isa(decl) && decl->getDeclContext()->isTypeContext()); +}; std::string swift::ide::removeCodeCompletionTokens( StringRef Input, StringRef TokenName, unsigned *CompletionOffset) { @@ -1281,16 +1285,24 @@ class CodeCompletionCallbacksImpl : public CodeCompletionCallbacks { Optional getTypeOfParsedExpr() { assert(ParsedExpr && "should have an expression"); + + // Figure out the kind of type-check we'll be performing. + auto CheckKind = CompletionTypeCheckKind::Normal; + if (Kind == CompletionKind::KeyPathExpr || + Kind == CompletionKind::KeyPathExprDot) + CheckKind = CompletionTypeCheckKind::ObjCKeyPath; + // If we've already successfully type-checked the expression for some // reason, just return the type. // FIXME: if it's ErrorType but we've already typechecked we shouldn't // typecheck again. rdar://21466394 - if (ParsedExpr->getType() && !ParsedExpr->getType()->is()) + if (CheckKind == CompletionTypeCheckKind::Normal && + ParsedExpr->getType() && !ParsedExpr->getType()->is()) return ParsedExpr->getType(); Expr *ModifiedExpr = ParsedExpr; if (auto T = getTypeOfCompletionContextExpr(P.Context, CurDeclContext, - ModifiedExpr)) { + CheckKind, ModifiedExpr)) { // FIXME: even though we don't apply the solution, the type checker may // modify the original expression. We should understand what effect that // may have on code completion. @@ -1323,6 +1335,7 @@ class CodeCompletionCallbacksImpl : public CodeCompletionCallbacks { void completePostfixExprParen(Expr *E, Expr *CodeCompletionE) override; void completeExprSuper(SuperRefExpr *SRE) override; void completeExprSuperDot(SuperRefExpr *SRE) override; + void completeExprKeyPath(ObjCKeyPathExpr *KPE, bool HasDot) override; void completeTypeSimpleBeginning() override; void completeTypeIdentifierWithDot(IdentTypeRepr *ITR) override; @@ -1525,6 +1538,7 @@ class CompletionLookup final : public swift::VisibleDeclConsumer { bool HaveLParen = false; bool HaveRParen = false; bool IsSuperRefExpr = false; + bool IsKeyPathExpr = false; bool IsDynamicLookup = false; bool PreferFunctionReferencesToCalls = false; bool HaveLeadingSpace = false; @@ -1677,6 +1691,10 @@ class CompletionLookup final : public swift::VisibleDeclConsumer { IsSuperRefExpr = true; } + void setIsKeyPathExpr() { + IsKeyPathExpr = true; + } + void setIsDynamicLookup() { IsDynamicLookup = true; } @@ -2147,6 +2165,25 @@ class CompletionLookup final : public swift::VisibleDeclConsumer { Builder.addRightParen(); } + void addPoundKeyPath(bool needPound) { + // #keyPath is only available when the Objective-C runtime is. + if (!Ctx.LangOpts.EnableObjCInterop) return; + + CodeCompletionResultBuilder Builder( + Sink, + CodeCompletionResult::ResultKind::Keyword, + SemanticContextKind::ExpressionSpecific, + ExpectedTypes); + if (needPound) + Builder.addTextChunk("#keyPath"); + else + Builder.addTextChunk("keyPath"); + Builder.addLeftParen(); + Builder.addSimpleTypedParameter("@objc property sequence", + /*isVarArg=*/false); + Builder.addRightParen(); + } + void addFunctionCallPattern(const AnyFunctionType *AFT, const AbstractFunctionDecl *AFD = nullptr) { foundFunction(AFT); @@ -2596,6 +2633,9 @@ class CompletionLookup final : public swift::VisibleDeclConsumer { if (D->getName().isEditorPlaceholder()) return; + if (IsKeyPathExpr && !KeyPathFilter(D, Reason)) + return; + if (!D->hasType()) TypeResolver->resolveDeclSignature(D); else if (isa(D)) { @@ -3046,7 +3086,9 @@ class CompletionLookup final : public swift::VisibleDeclConsumer { if (auto T = getTypeOfCompletionContextExpr( CurrDeclContext->getASTContext(), - const_cast(CurrDeclContext), tempExpr)) + const_cast(CurrDeclContext), + CompletionTypeCheckKind::Normal, + tempExpr)) addPostfixOperatorCompletion(op, *T); } @@ -3371,7 +3413,8 @@ class CompletionLookup final : public swift::VisibleDeclConsumer { void getValueCompletionsInDeclContext(SourceLoc Loc, DeclFilter Filter = DefaultFilter, bool IncludeTopLevel = false, - bool RequestCache = true) { + bool RequestCache = true, + bool LiteralCompletions = true) { Kind = LookupKind::ValueInDeclContext; NeedLeadingDot = false; FilteredDeclConsumer Consumer(*this, Filter); @@ -3397,19 +3440,33 @@ class CompletionLookup final : public swift::VisibleDeclConsumer { } } - addValueLiteralCompletions(); + if (LiteralCompletions) + addValueLiteralCompletions(); - // If the expected type is ObjectiveC.Selector, add #selector. + // If the expected type is ObjectiveC.Selector, add #selector. If + // it's String, add #keyPath. if (Ctx.LangOpts.EnableObjCInterop) { + bool addedSelector = false; + bool addedKeyPath = false; for (auto T : ExpectedTypes) { T = T->lookThroughAllAnyOptionalTypes(); if (auto structDecl = T->getStructOrBoundGenericStruct()) { - if (structDecl->getName() == Ctx.Id_Selector && + if (!addedSelector && + structDecl->getName() == Ctx.Id_Selector && structDecl->getParentModule()->getName() == Ctx.Id_ObjectiveC) { addPoundSelector(/*needPound=*/true); - break; + if (addedKeyPath) break; + addedSelector = true; + continue; } } + + if (!addedKeyPath && T->getAnyNominal() == Ctx.getStringDecl()) { + addPoundKeyPath(/*needPound=*/true); + if (addedSelector) break; + addedKeyPath = true; + continue; + } } } } @@ -4192,6 +4249,13 @@ void CodeCompletionCallbacksImpl::completeExprSuperDot(SuperRefExpr *SRE) { CurDeclContext = P.CurDeclContext; } +void CodeCompletionCallbacksImpl::completeExprKeyPath(ObjCKeyPathExpr *KPE, + bool HasDot) { + Kind = HasDot ? CompletionKind::KeyPathExprDot : CompletionKind::KeyPathExpr; + ParsedExpr = KPE; + CurDeclContext = P.CurDeclContext; +} + void CodeCompletionCallbacksImpl::completePoundAvailablePlatform() { Kind = CompletionKind::PoundAvailablePlatform; CurDeclContext = P.CurDeclContext; @@ -4447,6 +4511,8 @@ void CodeCompletionCallbacksImpl::addKeywords(CodeCompletionResultSink &Sink) { case CompletionKind::CallArg: case CompletionKind::AfterPound: case CompletionKind::GenericParams: + case CompletionKind::KeyPathExpr: + case CompletionKind::KeyPathExprDot: break; case CompletionKind::StmtOrExpr: @@ -4722,7 +4788,9 @@ void CodeCompletionCallbacksImpl::doneParsing() { if (ParsedExpr) { ExprType = getTypeOfParsedExpr(); if (!ExprType && Kind != CompletionKind::PostfixExprParen && - Kind != CompletionKind::CallArg) + Kind != CompletionKind::CallArg && + Kind != CompletionKind::KeyPathExpr && + Kind != CompletionKind::KeyPathExprDot) return; if (ExprType) ParsedExpr->setType(*ExprType); @@ -4856,6 +4924,27 @@ void CodeCompletionCallbacksImpl::doneParsing() { break; } + case CompletionKind::KeyPathExprDot: + Lookup.setHaveDot(SourceLoc()); + SWIFT_FALLTHROUGH; + + case CompletionKind::KeyPathExpr: { + Lookup.setIsKeyPathExpr(); + Lookup.includeInstanceMembers(); + + if (ExprType) { + if (isDynamicLookup(*ExprType)) + Lookup.setIsDynamicLookup(); + + Lookup.getValueExprCompletions(*ExprType); + } else { + SourceLoc Loc = P.Context.SourceMgr.getCodeCompletionLoc(); + Lookup.getValueCompletionsInDeclContext(Loc, KeyPathFilter, + false, true, false); + } + break; + } + case CompletionKind::TypeSimpleBeginning: { Lookup.getTypeCompletionsInDeclContext( P.Context.SourceMgr.getCodeCompletionLoc()); @@ -4965,6 +5054,7 @@ void CodeCompletionCallbacksImpl::doneParsing() { case CompletionKind::AfterPound: { Lookup.addPoundAvailable(ParentStmtKind); Lookup.addPoundSelector(/*needPound=*/false); + Lookup.addPoundKeyPath(/*needPound=*/false); break; } diff --git a/lib/Parse/ParseExpr.cpp b/lib/Parse/ParseExpr.cpp index 3d5e19704b9ec..0becf0540c72b 100644 --- a/lib/Parse/ParseExpr.cpp +++ b/lib/Parse/ParseExpr.cpp @@ -566,12 +566,30 @@ ParserResult Parser::parseExprKeyPath() { } SourceLoc lParenLoc = consumeToken(tok::l_paren); - // Parse the sequence of unqualified-names. + // Handle code completion. SmallVector names; SmallVector nameLocs; + auto handleCodeCompletion = [&](bool hasDot) -> ParserResult { + ObjCKeyPathExpr *expr = nullptr; + if (!names.empty()) { + expr = ObjCKeyPathExpr::create(Context, keywordLoc, lParenLoc, names, + nameLocs, Tok.getLoc()); + } + + if (CodeCompletion) + CodeCompletion->completeExprKeyPath(expr, hasDot); + + // Eat the code completion token because we handled it. + consumeToken(tok::code_complete); + return makeParserCodeCompletionResult(expr); + }; + + // Parse the sequence of unqualified-names. ParserStatus status; while (true) { - // FIXME: Code completion. + // Handle code completion. + if (Tok.is(tok::code_complete)) + return handleCodeCompletion(!names.empty()); // Parse the next name. DeclNameLoc nameLoc; @@ -595,6 +613,10 @@ ParserResult Parser::parseExprKeyPath() { names.push_back(name.getBaseName()); nameLocs.push_back(nameLoc.getBaseNameLoc()); + // Handle code completion. + if (Tok.is(tok::code_complete)) + return handleCodeCompletion(false); + // Parse the next period to continue the path. if (consumeIf(tok::period)) continue; diff --git a/lib/Sema/TypeCheckExprObjC.cpp b/lib/Sema/TypeCheckExprObjC.cpp index 5f6e4fc385fc5..2c9097d735858 100644 --- a/lib/Sema/TypeCheckExprObjC.cpp +++ b/lib/Sema/TypeCheckExprObjC.cpp @@ -18,10 +18,11 @@ #include "swift/Basic/Range.h" using namespace swift; -ObjCKeyPathExpr *TypeChecker::checkObjCKeyPathExpr(DeclContext *dc, - ObjCKeyPathExpr *expr) { +Optional TypeChecker::checkObjCKeyPathExpr(DeclContext *dc, + ObjCKeyPathExpr *expr, + bool requireResultType) { // If there is already a semantic expression, do nothing. - if (expr->getSemanticExpr()) return expr; + if (expr->getSemanticExpr() && !requireResultType) return None; // #keyPath only makes sense when we have the Objective-C runtime. if (!Context.LangOpts.EnableObjCInterop) { @@ -30,7 +31,7 @@ ObjCKeyPathExpr *TypeChecker::checkObjCKeyPathExpr(DeclContext *dc, expr->setSemanticExpr( new (Context) StringLiteralExpr("", expr->getSourceRange(), /*Implicit=*/true)); - return expr; + return None; } // The key path string we're forming. @@ -70,7 +71,7 @@ ObjCKeyPathExpr *TypeChecker::checkObjCKeyPathExpr(DeclContext *dc, anyObjectType = anyObject->getDeclaredInterfaceType(); } else { diagnose(expr->getLoc(), diag::stdlib_anyobject_not_found); - return expr; + return None; } // Local function to update the state after we've resolved a @@ -332,10 +333,13 @@ ObjCKeyPathExpr *TypeChecker::checkObjCKeyPathExpr(DeclContext *dc, diagnose(expr->getLoc(), diag::expr_keypath_empty); // Set the semantic expression. - expr->setSemanticExpr( - new (Context) StringLiteralExpr(Context.AllocateCopy(keyPathString), - expr->getSourceRange(), - /*Implicit=*/true)); - return expr; -} + if (!expr->getSemanticExpr()) { + expr->setSemanticExpr( + new (Context) StringLiteralExpr(Context.AllocateCopy(keyPathString), + expr->getSourceRange(), + /*Implicit=*/true)); + } + if (!currentType) return None; + return currentType; +} diff --git a/lib/Sema/TypeChecker.cpp b/lib/Sema/TypeChecker.cpp index ec41d6b5263d4..e40d65a83d9ab 100644 --- a/lib/Sema/TypeChecker.cpp +++ b/lib/Sema/TypeChecker.cpp @@ -743,9 +743,22 @@ Type swift::lookUpTypeInContext(DeclContext *DC, StringRef Name) { } } -static Optional getTypeOfCompletionContextExpr(TypeChecker &TC, - DeclContext *DC, - Expr *&parsedExpr) { +static Optional getTypeOfCompletionContextExpr( + TypeChecker &TC, + DeclContext *DC, + CompletionTypeCheckKind kind, + Expr *&parsedExpr) { + switch (kind) { + case CompletionTypeCheckKind::Normal: + // Handle below. + break; + + case CompletionTypeCheckKind::ObjCKeyPath: + if (auto keyPath = dyn_cast(parsedExpr)) + return TC.checkObjCKeyPathExpr(DC, keyPath, /*requireResulType=*/true); + + return None; + } CanType originalType = parsedExpr->getType().getCanonicalTypeOrNull(); if (auto T = TC.getTypeOfExpressionWithoutApplying(parsedExpr, DC, @@ -764,19 +777,21 @@ static Optional getTypeOfCompletionContextExpr(TypeChecker &TC, /// \brief Return the type of an expression parsed during code completion, or /// a null \c Type on error. -Optional swift::getTypeOfCompletionContextExpr(ASTContext &Ctx, - DeclContext *DC, - Expr *&parsedExpr) { +Optional swift::getTypeOfCompletionContextExpr( + ASTContext &Ctx, + DeclContext *DC, + CompletionTypeCheckKind kind, + Expr *&parsedExpr) { if (Ctx.getLazyResolver()) { TypeChecker *TC = static_cast(Ctx.getLazyResolver()); - return ::getTypeOfCompletionContextExpr(*TC, DC, parsedExpr); + return ::getTypeOfCompletionContextExpr(*TC, DC, kind, parsedExpr); } else { // Set up a diagnostics engine that swallows diagnostics. DiagnosticEngine diags(Ctx.SourceMgr); TypeChecker TC(Ctx, diags); // Try to solve for the actual type of the expression. - return ::getTypeOfCompletionContextExpr(TC, DC, parsedExpr); + return ::getTypeOfCompletionContextExpr(TC, DC, kind, parsedExpr); } } diff --git a/lib/Sema/TypeChecker.h b/lib/Sema/TypeChecker.h index 39a76f91a5dd6..ba6a72f6276e7 100644 --- a/lib/Sema/TypeChecker.h +++ b/lib/Sema/TypeChecker.h @@ -1176,7 +1176,10 @@ class TypeChecker final : public LazyResolver { bool typeCheckExpressionShallow(Expr *&expr, DeclContext *dc); /// Check the key-path expression. - ObjCKeyPathExpr *checkObjCKeyPathExpr(DeclContext *dc, ObjCKeyPathExpr *expr); + /// + /// Returns the type of the last component of the key-path. + Optional checkObjCKeyPathExpr(DeclContext *dc, ObjCKeyPathExpr *expr, + bool requireResultType = false); /// \brief Type check whether the given type declaration includes members of /// unsupported recursive value types. diff --git a/test/IDE/complete_pound_keypath.swift b/test/IDE/complete_pound_keypath.swift new file mode 100644 index 0000000000000..45cafb7732d26 --- /dev/null +++ b/test/IDE/complete_pound_keypath.swift @@ -0,0 +1,46 @@ +// RUN: %target-swift-ide-test(mock-sdk: %clang-importer-sdk) -code-completion -source-filename %s -code-completion-token=AFTER_POUND | FileCheck -check-prefix=CHECK-AFTER_POUND %s + +// RUN: %target-swift-ide-test(mock-sdk: %clang-importer-sdk) -code-completion -source-filename %s -code-completion-token=KEYPATH_ARG | FileCheck -check-prefix=CHECK-KEYPATH_ARG %s + +// RUN: %target-swift-ide-test(mock-sdk: %clang-importer-sdk) -code-completion -source-filename %s -code-completion-token=IN_KEYPATH_1 | FileCheck -check-prefix=CHECK-IN_KEYPATH %s + +// RUN: %target-swift-ide-test(mock-sdk: %clang-importer-sdk) -code-completion -source-filename %s -code-completion-token=IN_KEYPATH_2 | FileCheck -check-prefix=CHECK-IN_KEYPATH %s + + +// REQUIRES: objc_interop + +import Foundation + +{ + if ##^AFTER_POUND^# +} + +func acceptKeyPath(_ keyPath: String) { } + +func selectorArg1(obj: NSObject) { + acceptKeyPath(#^KEYPATH_ARG^# +} + +class ObjCClass : NSObject { + var prop1: String = "" + var prop2: ObjCClass? + + func completeInKeyPath1() { + _ = #keyPath(#^IN_KEYPATH_1^# + } +} + +func completeInKeyPath2() { + _ = #keyPath(ObjCClass.#^IN_KEYPATH_2^# +} + +// CHECK-AFTER_POUND: Keyword/ExprSpecific: keyPath({#@objc property sequence#}); name=keyPath(@objc property sequence) + +// CHECK-KEYPATH_ARG: Keyword/ExprSpecific: #keyPath({#@objc property sequence#}); name=#keyPath(@objc property sequence) + +// CHECK-IN_KEYPATH: Decl[InstanceVar]/CurrNominal: prop1[#String#]; name=prop1 +// CHECK-IN_KEYPATH: Decl[InstanceVar]/CurrNominal: prop2[#ObjCClass?#]; name=prop2 +// CHECK-IN_KEYPATH: Decl[InstanceVar]/Super: hashValue[#Int#]; name=hashValue +// CHECK-IN_KEYPATH: Decl[InstanceVar]/Super: hash[#Int#]; name=hash + + From 6092d8aaa9fbee73034157bdbcac4a8519d563b7 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sun, 22 May 2016 21:28:48 -0700 Subject: [PATCH 4/5] Fix the # of Expr bits used by ObjCKeyPathExpr. --- include/swift/AST/Expr.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/swift/AST/Expr.h b/include/swift/AST/Expr.h index 453281c281262..7b1d96a4a8b92 100644 --- a/include/swift/AST/Expr.h +++ b/include/swift/AST/Expr.h @@ -321,7 +321,7 @@ class alignas(8) Expr { /// Whether the names have corresponding source locations. unsigned HaveSourceLocations : 1; }; - enum { NumObjCKeyPathExprBits = NumExprBits + 17 }; + enum { NumObjCKeyPathExprBits = NumExprBits + 9 }; static_assert(NumObjCKeyPathExprBits <= 32, "fits in an unsigned"); protected: From d648a086f4ed3de524bf0ac2adc8ba3d01f8afb8 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 23 May 2016 12:43:05 -0700 Subject: [PATCH 5/5] Add SE-0062 to the changelog. --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aec44099b5927..97b016c9ff7dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -219,6 +219,11 @@ Swift 3.0 let sel2 = #selector(setter: UIView.backgroundColor) // sel2 has type Selector ``` +* [SE-0062](https://github.com/apple/swift-evolution/blob/master/proposals/0062-objc-keypaths.md): A key-path can now be formed with `#keyPath`. For example: + + ```swift + person.valueForKeyPath(#keyPath(Person.bestFriend.lastName)) + ``` Swift 2.2 ---------