diff --git a/completion/context.cpp b/completion/context.cpp --- a/completion/context.cpp +++ b/completion/context.cpp @@ -674,6 +674,7 @@ case Parser::Token_WHILE: case Parser::Token_WHITESPACE: case Parser::Token_YIELD: + case Parser::Token_YIELD_FROM: /// TODO: code completion after goto case Parser::Token_GOTO: case Parser::TokenTypeSize: diff --git a/duchain/builders/declarationbuilder.h b/duchain/builders/declarationbuilder.h --- a/duchain/builders/declarationbuilder.h +++ b/duchain/builders/declarationbuilder.h @@ -97,6 +97,7 @@ void visitUseNamespace(UseNamespaceAst* node) override; void visitClosure(ClosureAst* node) override; void visitLexicalVar(LexicalVarAst* node) override; + void visitVarExpression(VarExpressionAst* node) override; /// checks whether the body is empty (i.e. equals ";" instead of "{...}") bool isEmptyMethodBody(const MethodBodyAst* body) const { diff --git a/duchain/builders/declarationbuilder.cpp b/duchain/builders/declarationbuilder.cpp --- a/duchain/builders/declarationbuilder.cpp +++ b/duchain/builders/declarationbuilder.cpp @@ -1581,6 +1581,15 @@ } } +void DeclarationBuilder::visitVarExpression(VarExpressionAst* node) +{ + DeclarationBuilderBase::visitVarExpression(node); + + if (node->isGenerator != -1 && currentContext()->type() != DUContext::Other) { + reportError(i18n("The 'yield' expression can only be used inside a function"), node); + } +} + void DeclarationBuilder::updateCurrentType() { DUChainWriteLocker lock(DUChain::lock()); diff --git a/duchain/builders/typebuilder.h b/duchain/builders/typebuilder.h --- a/duchain/builders/typebuilder.h +++ b/duchain/builders/typebuilder.h @@ -63,15 +63,52 @@ void visitAssignmentExpression(AssignmentExpressionAst* node) override; void visitStaticVar(StaticVarAst *node) override; void visitCatchItem(CatchItemAst *node) override; + void visitVarExpression(VarExpressionAst *node) override; /// The declaration builder implements this and updates /// the type of the current declaration virtual void updateCurrentType(); KDevelop::AbstractType::Ptr getTypeForNode(AstNode* node); + + /** + * Opens the given context type, and sets it to be the current context type. + */ + void openContextType(const KDevelop::AbstractType::Ptr& type) + { + m_contextTypes.append(type); + } + + /** + * Close the current context type. + */ + void closeContextType() + { + // And the reference will be lost... + m_contextTypes.pop(); + } + + /** + * Retrieve the type of the current context. + * + * \returns the abstract type of the current context. + */ + inline KDevelop::AbstractType::Ptr currentContextType() + { + if (m_contextTypes.isEmpty()) { + return KDevelop::AbstractType::Ptr(); + } else { + return m_contextTypes.top(); + } + } + + /// Determine if the type builder has a type for the current context. \returns true if there is a current context type, else returns false. + inline bool hasCurrentContextType() { return !m_contextTypes.isEmpty(); } + private: KDevelop::FunctionType::Ptr m_currentFunctionType; QList m_currentFunctionParams; + KDevelop::Stack m_contextTypes; bool m_gotTypeFromDocComment; bool m_gotReturnTypeFromDocComment; diff --git a/duchain/builders/typebuilder.cpp b/duchain/builders/typebuilder.cpp --- a/duchain/builders/typebuilder.cpp +++ b/duchain/builders/typebuilder.cpp @@ -299,6 +299,7 @@ FunctionType::Ptr functionType = FunctionType::Ptr(new FunctionType()); openType(functionType); + openContextType(functionType); AbstractType::Ptr phpdocReturnType = parseDocComment(node, QStringLiteral("return")); functionType->setReturnType(returnType(node->returnType, phpdocReturnType, editor(), currentContext())); @@ -309,6 +310,7 @@ if (currentType() && !currentType()->returnType()) { currentType()->setReturnType(AbstractType::Ptr(new IntegralType(IntegralType::TypeVoid))); } + closeContextType(); closeType(); } else { //member-variable @@ -391,6 +393,8 @@ FunctionType::Ptr type = currentType(); Q_ASSERT(type); + openContextType(type); + AbstractType::Ptr phpdocReturnType = parseDocComment(node, QStringLiteral("return")); type->setReturnType(returnType(node->returnType, phpdocReturnType, editor(), currentContext())); m_gotReturnTypeFromDocComment = type->returnType(); @@ -402,13 +406,16 @@ if (!type->returnType()) { type->setReturnType(AbstractType::Ptr(new IntegralType(IntegralType::TypeVoid))); } + + closeContextType(); } void TypeBuilder::visitClosure(ClosureAst* node) { m_currentFunctionParams = parseDocCommentParams(node); FunctionType::Ptr type = FunctionType::Ptr(new FunctionType()); openType(type); + openContextType(type); AbstractType::Ptr phpdocReturnType = parseDocComment(node, QStringLiteral("return")); type->setReturnType(returnType(node->returnType, phpdocReturnType, editor(), currentContext())); @@ -421,6 +428,7 @@ if (!type->returnType()) { type->setReturnType(AbstractType::Ptr(new IntegralType(IntegralType::TypeVoid))); } + closeContextType(); closeType(); } @@ -568,6 +576,26 @@ } +void TypeBuilder::visitVarExpression(Php::VarExpressionAst *node) +{ + if (hasCurrentContextType() && node->isGenerator != -1 && !m_gotReturnTypeFromDocComment) { + FunctionType::Ptr ft = FunctionType::Ptr::dynamicCast(currentContextType()); + DeclarationPointer generatorDecl = findDeclarationImport(ClassDeclarationType, QualifiedIdentifier("generator")); + + if (ft && generatorDecl) { + AbstractType::Ptr generatorType = generatorDecl->abstractType(); + + if (generatorType) { + ft->setReturnType(generatorType); + } + } + + updateCurrentType(); + } + + TypeBuilderBase::visitVarExpression(node); +} + void TypeBuilder::updateCurrentType() { // do nothing diff --git a/duchain/expressionvisitor.h b/duchain/expressionvisitor.h --- a/duchain/expressionvisitor.h +++ b/duchain/expressionvisitor.h @@ -56,6 +56,7 @@ void visitAssignmentExpression(AssignmentExpressionAst *node) override; void visitArrayIndexSpecifier(ArrayIndexSpecifierAst* node) override; void visitCompoundVariableWithSimpleIndirectReference(CompoundVariableWithSimpleIndirectReferenceAst *node) override; + void visitVarExpression(VarExpressionAst *node) override; void visitVarExpressionNewObject(VarExpressionNewObjectAst *node) override; void visitVarExpressionArray(VarExpressionArrayAst *node) override; void visitClosure(ClosureAst* node) override; @@ -74,6 +75,7 @@ void visitRelationalExpression(RelationalExpressionAst* node) override; void visitRelationalExpressionRest(RelationalExpressionRestAst* node) override; void visitEqualityExpressionRest(EqualityExpressionRestAst* node) override; + void visitStatement(StatementAst* node) override; QString stringForNode(AstNode* id); KDevelop::QualifiedIdentifier identifierForNode(IdentifierAst* id); @@ -94,6 +96,40 @@ protected: EditorIntegrator* m_editor; + /** + * Opens the given closure return type, and sets it to be the current closure return type. + */ + void openClosureReturnType(const KDevelop::AbstractType::Ptr& type) + { + m_closureReturnTypes.append(type); + } + + /** + * Close the current closure return type. + */ + void closeClosureReturnType() + { + // And the reference will be lost... + m_closureReturnTypes.pop(); + } + + /** + * Retrieve the return type of the current closure. + * + * \returns the abstract type of the current context. + */ + inline KDevelop::AbstractType::Ptr currentClosureReturnType() + { + if (m_closureReturnTypes.isEmpty()) { + return KDevelop::AbstractType::Ptr(); + } else { + return m_closureReturnTypes.top(); + } + } + + /// Determine if the expression visitor has a return type for the current closure. \returns true if there is a current closure return type, else returns false. + inline bool hasCurrentClosureReturnType() { return !m_closureReturnTypes.isEmpty(); } + private: KDevelop::DUContext* findClassContext(NamespacedIdentifierAst* className); KDevelop::DUContext* findClassContext(IdentifierAst* className); @@ -104,6 +140,7 @@ KDevelop::DUContext* m_currentContext; ExpressionEvaluationResult m_result; + KDevelop::Stack m_closureReturnTypes; bool m_isAssignmentExpressionEqual; bool m_inDefine; diff --git a/duchain/expressionvisitor.cpp b/duchain/expressionvisitor.cpp --- a/duchain/expressionvisitor.cpp +++ b/duchain/expressionvisitor.cpp @@ -190,6 +190,22 @@ } } +void ExpressionVisitor::visitVarExpression(VarExpressionAst *node) +{ + DefaultVisitor::visitVarExpression(node); + if (node->isGenerator != -1) { + DeclarationPointer generatorDecl = findDeclarationImport(ClassDeclarationType, QualifiedIdentifier("generator")); + + if (generatorDecl) { + m_result.setType(AbstractType::Ptr(new IntegralType(IntegralType::TypeMixed))); + if (hasCurrentClosureReturnType()) { + FunctionType::Ptr closureType = currentClosureReturnType().cast(); + closureType->setReturnType(generatorDecl->abstractType()); + } + } + } +} + void ExpressionVisitor::visitVarExpressionNewObject(VarExpressionNewObjectAst *node) { DefaultVisitor::visitVarExpressionNewObject(node); @@ -216,7 +232,8 @@ void ExpressionVisitor::visitClosure(ClosureAst* node) { auto* closureType = new FunctionType; - m_result.setType(AbstractType::Ptr(new IntegralType(IntegralType::TypeVoid))); + closureType->setReturnType(AbstractType::Ptr(new IntegralType(IntegralType::TypeVoid))); + openClosureReturnType(AbstractType::Ptr(closureType)); if (node->functionBody) { visitInnerStatementList(node->functionBody); } @@ -229,13 +246,12 @@ buildNamespaceUses(objectType, id); } - //First try return typehint or phpdoc return typehint + //Override found type with return typehint or phpdoc return typehint AbstractType::Ptr type = returnType(node->returnType, {}, m_editor, m_currentContext); - if (!type) { - //If failed, use the inferred type from return statements - type = m_result.type(); + + if (type) { + closureType->setReturnType(type); } - closureType->setReturnType(type); if (node->parameters->parametersSequence) { const KDevPG::ListNode< ParameterAst* >* it = node->parameters->parametersSequence->front(); @@ -284,6 +300,7 @@ } m_result.setType(AbstractType::Ptr(closureType)); + closeClosureReturnType(); } void ExpressionVisitor::visitFunctionCallParameterList( FunctionCallParameterListAst* node ) @@ -767,6 +784,19 @@ } } +void ExpressionVisitor::visitStatement(StatementAst *node) +{ + DefaultVisitor::visitStatement(node); + + if (node->returnExpr) { + FunctionType::Ptr closureType = currentClosureReturnType().cast(); + + if (closureType) { + closureType->setReturnType(m_result.type()); + } + } +} + QString ExpressionVisitor::stringForNode(AstNode* id) { if (!id) diff --git a/duchain/tests/duchain.h b/duchain/tests/duchain.h --- a/duchain/tests/duchain.h +++ b/duchain/tests/duchain.h @@ -57,6 +57,9 @@ void declareNullableTypehintMixedFunction(); void declareTypehintNullableIterableFunction(); void declareTypehintWithPhpdocFunction(); + void returnTypeGenerator_data(); + void returnTypeGenerator(); + void returnTypeGeneratorDelegation(); void returnTypeClass(); void declarationReturnType(); void declarationReturnTypeInRecursingFunction(); @@ -187,6 +190,8 @@ void illegalExpression(); void printExpression_data(); void printExpression(); + void generatorAssignment(); + void generatorClosure(); }; } diff --git a/duchain/tests/duchain.cpp b/duchain/tests/duchain.cpp --- a/duchain/tests/duchain.cpp +++ b/duchain/tests/duchain.cpp @@ -478,6 +478,78 @@ QVERIFY(var->type()->dataType() == IntegralType::TypeInt); } +void TestDUChain::returnTypeGenerator_data() +{ + QTest::addColumn("code"); + + //Note: in practice, Generator is defined by php, but this class is not loaded in this test, so define it ourselves + QTest::newRow("simple yield expression") << QStringLiteral(" 1; }\n"); + QTest::newRow("yield equality expression") << QStringLiteral("> 1; }\n"); + QTest::newRow("yield bit expression") << QStringLiteral(" 'value'; }\n"); +} + +void TestDUChain::returnTypeGenerator() +{ + QFETCH(QString, code); + + TopDUContext* top = parse(code.toUtf8(), DumpNone); + DUChainReleaser releaseTop(top); + DUChainWriteLocker lock(DUChain::lock()); + + QVERIFY(!top->parentContext()); + QCOMPARE(top->childContexts().count(), 3); + QCOMPARE(top->localDeclarations().count(), 2); + + Declaration* dec = top->localDeclarations().at(1); + QCOMPARE(dec->qualifiedIdentifier(), QualifiedIdentifier("foo")); + FunctionType::Ptr functionType = dec->type(); + QVERIFY(functionType); + StructureType::Ptr retType = StructureType::Ptr::dynamicCast(functionType->returnType()); + QVERIFY(retType); + QCOMPARE(retType->qualifiedIdentifier(), QualifiedIdentifier("generator")); +} + +void TestDUChain::returnTypeGeneratorDelegation() +{ + //Note: in practice, Generator is defined by php, but this class is not loaded in this test, so define it ourselves + // 0 1 2 3 4 5 6 7 + // 01234567890123456789012345678901234567890123456789012345678901234567890123456789 + QByteArray method("parentContext()); + QCOMPARE(top->childContexts().count(), 5); + QCOMPARE(top->localDeclarations().count(), 3); + + Declaration* dec = top->localDeclarations().at(1); + QCOMPARE(dec->qualifiedIdentifier(), QualifiedIdentifier("foo")); + FunctionType::Ptr functionType = dec->type(); + QVERIFY(functionType); + StructureType::Ptr retType = StructureType::Ptr::dynamicCast(functionType->returnType()); + QVERIFY(retType); + QCOMPARE(retType->qualifiedIdentifier(), QualifiedIdentifier("generator")); + + dec = top->localDeclarations().at(2); + QCOMPARE(dec->qualifiedIdentifier(), QualifiedIdentifier("bar")); + functionType = dec->type(); + QVERIFY(functionType); + retType = StructureType::Ptr::dynamicCast(functionType->returnType()); + QVERIFY(retType); + QCOMPARE(retType->qualifiedIdentifier(), QualifiedIdentifier("generator")); +} + void TestDUChain::returnTypeClass() { // 0 1 2 3 4 5 6 7 @@ -3823,3 +3895,86 @@ QVERIFY(top->problems().isEmpty()); } + +void TestDUChain::generatorAssignment() +{ + //Note: in practice, Generator is defined by php, but this class is not loaded in this test, so define it ourselves + // 0 1 2 3 4 5 6 7 + // 01234567890123456789012345678901234567890123456789012345678901234567890123456789 + QByteArray method("parentContext()); + QCOMPARE(top->childContexts().count(), 3); + QCOMPARE(top->localDeclarations().count(), 2); + + Declaration* dec = top->localDeclarations().at(1); + QCOMPARE(dec->qualifiedIdentifier(), QualifiedIdentifier("foo")); + FunctionType::Ptr functionType = dec->type(); + QVERIFY(functionType); + StructureType::Ptr retType = StructureType::Ptr::dynamicCast(functionType->returnType()); + QVERIFY(retType); + QCOMPARE(retType->qualifiedIdentifier(), QualifiedIdentifier("generator")); + + dec = top->childContexts().at(2)->localDeclarations().at(0); + QCOMPARE(dec->identifier(), Identifier("a")); + IntegralType::Ptr type = dec->type(); + QVERIFY(type); + QVERIFY(type->dataType() == IntegralType::TypeMixed); +} + +void TestDUChain::generatorClosure() +{ + //Note: in practice, Generator is defined by php, but this class is not loaded in this test, so define it ourselves + // 0 1 2 3 4 5 6 7 + // 01234567890123456789012345678901234567890123456789012345678901234567890123456789 + QByteArray method("parentContext()); + QCOMPARE(top->childContexts().count(), 3); + QCOMPARE(top->localDeclarations().count(), 2); + + Declaration* dec = top->localDeclarations().at(1); + QCOMPARE(dec->qualifiedIdentifier(), QualifiedIdentifier("foo")); + FunctionType::Ptr functionType = dec->type(); + QVERIFY(functionType); + IntegralType::Ptr retType = IntegralType::Ptr::dynamicCast(functionType->returnType()); + QVERIFY(retType); + QVERIFY(retType->dataType() == IntegralType::TypeVoid); + + dec = top->childContexts().at(2)->localDeclarations().at(0); + QCOMPARE(dec->identifier(), Identifier("a")); + + Declaration* closure = top->childContexts().at(2)->localDeclarations().at(1); + QVERIFY(closure->identifier().isEmpty()); + + FunctionType::Ptr funcType = closure->type(); + QVERIFY(funcType); + QVERIFY(funcType->returnType().cast()); + QCOMPARE(funcType->returnType().cast()->dataType(), static_cast(IntegralType::TypeVoid)); + + QVERIFY(dec->abstractType()->equals(closure->abstractType().constData())); + + dec = top->childContexts().at(2)->childContexts().at(1)->localDeclarations().at(0); + QCOMPARE(dec->identifier(), Identifier("b")); + + closure = top->childContexts().at(2)->childContexts().at(1)->localDeclarations().at(1); + QVERIFY(closure->identifier().isEmpty()); + + funcType = closure->type(); + QVERIFY(funcType); + StructureType::Ptr generatorType = StructureType::Ptr::dynamicCast(funcType->returnType()); + QVERIFY(generatorType); + QCOMPARE(generatorType->qualifiedIdentifier(), QualifiedIdentifier("generator")); + + QVERIFY(dec->abstractType()->equals(closure->abstractType().constData())); +} diff --git a/parser/php.g b/parser/php.g --- a/parser/php.g +++ b/parser/php.g @@ -238,7 +238,7 @@ INCLUDE ("include"), INCLUDE_ONCE ("include_once"), EVAL ("eval"), REQUIRE ("require"), REQUIRE_ONCE ("require_once"), NAMESPACE ("namespace"), NAMESPACE_C("__NAMESPACE__"), USE("use"), GOTO ("goto"), TRAIT ("trait"), INSTEADOF ("insteadof"), CALLABLE ("callable"), - VOID ("void"), DIR ("__DIR__"), TRAIT_C ("__TRAIT__"), YIELD ("yield") ;; + VOID ("void"), DIR ("__DIR__"), TRAIT_C ("__TRAIT__"), YIELD ("yield"), YIELD_FROM("yield from") ;; -- casts: %token INT_CAST ("int cast"), DOUBLE_CAST ("double cast"), STRING_CAST ("string cast"), @@ -548,8 +548,10 @@ op=INC | op=DEC -> postprefixOperator ;; --- 10 first follow conflicts because we go back up the chain +-- 10 first follow conflicts because we go back up the chain (affects both print and yield) (print=PRINT+) printExpression=assignmentExpression + | isGenerator=YIELD (generatorExpression=printExpression ( DOUBLE_ARROW generatorValueExpr=printExpression | 0 ) | 0) + | isGenerator=YIELD_FROM generatorExpression=printExpression --first/first conflict - no problem because of ifs | ?[: m_state.varExpressionState == OnlyVariable :] 0 [: m_state.varExpressionState = Normal; :] variable=variable | ?[: m_state.varExpressionState == OnlyNewObject :] 0 [: m_state.varExpressionState = Normal; :] newObject=varExpressionNewObject diff --git a/parser/phplexer.cpp b/parser/phplexer.cpp --- a/parser/phplexer.cpp +++ b/parser/phplexer.cpp @@ -801,7 +801,24 @@ } else if (name.compare(QLatin1String("void"), Qt::CaseInsensitive) == 0) { token = Parser::Token_VOID; } else if (name.compare(QLatin1String("yield"), Qt::CaseInsensitive) == 0) { - token = Parser::Token_YIELD; + const QChar* lookAhead = it; + int pos = m_curpos; + while (pos < m_contentSize && lookAhead->isSpace()) { + ++lookAhead; + ++pos; + } + + auto nextToken = QString(); + nextToken += * lookAhead; + nextToken += * ++lookAhead; + nextToken += * ++lookAhead; + nextToken += * ++lookAhead; + if (pos + 4 < m_contentSize && nextToken == QStringLiteral("from")) { + m_curpos = pos + 4; + token = Parser::Token_YIELD_FROM; + } else { + token = Parser::Token_YIELD; + } } else { token = Parser::Token_STRING; }