// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "bringidentifierintoscope.h" #include "../cppeditortr.h" #include "../cppeditorwidget.h" #include "../cpplocatordata.h" #include "../cppprojectfile.h" #include "../cpprefactoringchanges.h" #include "../indexitem.h" #include "../insertionpointlocator.h" #include "cppquickfix.h" #include "cppquickfixhelpers.h" #include #include #include #include #include #include #include #include #include #ifdef WITH_TESTS #include "../cppsourceprocessertesthelper.h" #include "../cpptoolstestcase.h" #include "cppquickfix_test.h" #include #endif using namespace Core; using namespace CPlusPlus; using namespace ProjectExplorer; using namespace TextEditor; using namespace Utils; #ifdef WITH_TESTS namespace CppEditor::Internal::Tests { typedef QList QuickFixTestDocuments; } Q_DECLARE_METATYPE(CppEditor::Internal::Tests::QuickFixTestDocuments) #endif namespace CppEditor::Internal { namespace { static QString findShortestInclude( const QString currentDocumentFilePath, const QString candidateFilePath, const HeaderPaths &headerPaths) { QString result; const QFileInfo fileInfo(candidateFilePath); if (fileInfo.path() == QFileInfo(currentDocumentFilePath).path()) { result = QLatin1Char('"') + fileInfo.fileName() + QLatin1Char('"'); } else { for (const HeaderPath &headerPath : headerPaths) { if (!candidateFilePath.startsWith(headerPath.path.path())) continue; QString relativePath = candidateFilePath.mid(headerPath.path.path().size()); if (!relativePath.isEmpty() && relativePath.at(0) == QLatin1Char('/')) relativePath = relativePath.mid(1); if (result.isEmpty() || relativePath.size() + 2 < result.size()) result = QLatin1Char('<') + relativePath + QLatin1Char('>'); } } return result; } static QString findMatchingInclude(const QString &className, const HeaderPaths &headerPaths) { const QStringList candidateFileNames{ className, className + ".h", className + ".hpp", className.toLower(), className.toLower() + ".h", className.toLower() + ".hpp"}; for (const QString &fileName : candidateFileNames) { for (const HeaderPath &headerPath : headerPaths) { const QString headerPathCandidate = headerPath.path.path() + QLatin1Char('/') + fileName; const QFileInfo fileInfo(headerPathCandidate); if (fileInfo.exists() && fileInfo.isFile()) return '<' + fileName + '>'; } } return {}; } static HeaderPaths relevantHeaderPaths(const QString &filePath) { HeaderPaths headerPaths; const QList projectParts = CppModelManager::projectPart(filePath); if (projectParts.isEmpty()) { // Not part of any project, better use all include paths than none headerPaths += CppModelManager::headerPaths(); } else { for (const ProjectPart::ConstPtr &part : projectParts) headerPaths += part->headerPaths; } return headerPaths; } static NameAST *nameUnderCursor(const QList &path) { if (path.isEmpty()) return nullptr; NameAST *nameAst = nullptr; for (int i = path.size() - 1; i >= 0; --i) { AST * const ast = path.at(i); if (SimpleNameAST *simpleName = ast->asSimpleName()) { nameAst = simpleName; } else if (TemplateIdAST *templateId = ast->asTemplateId()) { nameAst = templateId; } else if (nameAst && ast->asNamedTypeSpecifier()) { break; // Stop at "Foo" for "N::Bar<@Foo>" } else if (QualifiedNameAST *qualifiedName = ast->asQualifiedName()) { nameAst = qualifiedName; break; } } return nameAst; } enum class LookupResult { Declared, ForwardDeclared, NotDeclared }; static LookupResult lookUpDefinition(const CppQuickFixInterface &interface, const NameAST *nameAst) { QTC_ASSERT(nameAst && nameAst->name, return LookupResult::NotDeclared); // Find the enclosing scope int line, column; const Document::Ptr doc = interface.semanticInfo().doc; doc->translationUnit()->getTokenPosition(nameAst->firstToken(), &line, &column); Scope *scope = doc->scopeAt(line, column); if (!scope) return LookupResult::NotDeclared; // Try to find the class/template definition const Name *name = nameAst->name; const QList results = interface.context().lookup(name, scope); LookupResult best = LookupResult::NotDeclared; for (const LookupItem &item : results) { if (Symbol *declaration = item.declaration()) { if (declaration->asClass()) return LookupResult::Declared; if (declaration->asForwardClassDeclaration()) { best = LookupResult::ForwardDeclared; continue; } if (Template *templ = declaration->asTemplate()) { if (Symbol *declaration = templ->declaration()) { if (declaration->asClass()) return LookupResult::Declared; if (declaration->asForwardClassDeclaration()) { best = LookupResult::ForwardDeclared; continue; } } } return LookupResult::Declared; } } return best; } static QString templateNameAsString(const TemplateNameId *templateName) { const Identifier *id = templateName->identifier(); return QString::fromUtf8(id->chars(), id->size()); } static Snapshot forwardingHeaders(const CppQuickFixInterface &interface) { Snapshot result; const Snapshot docs = interface.snapshot(); for (Document::Ptr doc : docs) { if (doc->globalSymbolCount() == 0 && doc->resolvedIncludes().size() == 1) result.insert(doc); } return result; } static QList matchName(const Name *name, QString *className) { if (!name) return {}; QString simpleName; QList matches; CppLocatorData *locatorData = CppModelManager::locatorData(); const Overview oo; if (const QualifiedNameId *qualifiedName = name->asQualifiedNameId()) { const Name *name = qualifiedName->name(); if (const TemplateNameId *templateName = name->asTemplateNameId()) { *className = templateNameAsString(templateName); } else { simpleName = oo.prettyName(name); *className = simpleName; matches = locatorData->findSymbols(IndexItem::Class, *className); if (matches.isEmpty()) { if (const Name *name = qualifiedName->base()) { if (const TemplateNameId *templateName = name->asTemplateNameId()) *className = templateNameAsString(templateName); else *className = oo.prettyName(name); } } } } else if (const TemplateNameId *templateName = name->asTemplateNameId()) { *className = templateNameAsString(templateName); } else { *className = oo.prettyName(name); } if (matches.isEmpty()) matches = locatorData->findSymbols(IndexItem::Class, *className); if (matches.isEmpty() && !simpleName.isEmpty()) *className = simpleName; return matches; } class AddIncludeForUndefinedIdentifierOp : public CppQuickFixOperation { public: AddIncludeForUndefinedIdentifierOp(const CppQuickFixInterface &interface, int priority, const QString &include, const QString &module); void perform() override; QString include() const { return m_include; } private: const QString m_include; const QString m_module; }; class AddForwardDeclForUndefinedIdentifierOp : public CppQuickFixOperation { public: AddForwardDeclForUndefinedIdentifierOp(const CppQuickFixInterface &interface, int priority, const QString &fqClassName, int symbolPos); private: void perform() override { const QStringList parts = m_className.split("::"); QTC_ASSERT(!parts.isEmpty(), return); const QStringList namespaces = parts.mid(0, parts.length() - 1); CppRefactoringFilePtr file = currentFile(); NSVisitor visitor(file.data(), namespaces, m_symbolPos); visitor.accept(file->cppDocument()->translationUnit()->ast()); const auto stringToInsert = [&visitor, symbol = parts.last()] { QString s = "\n"; for (const QString &ns : visitor.remainingNamespaces()) s += "namespace " + ns + " { "; s += "class " + symbol + ';'; for (int i = 0; i < visitor.remainingNamespaces().size(); ++i) s += " }"; return s; }; int insertPos = 0; // Find the position to insert: // If we have a matching namespace, we do the insertion there. // If we don't have a matching namespace, but there is another namespace in the file, // we assume that to be a good position for our insertion. // Otherwise, do the insertion after the last include that comes before the use of the symbol. // If there is no such include, do the insertion before the first token. if (visitor.enclosingNamespace()) { insertPos = file->startOf(visitor.enclosingNamespace()->linkage_body) + 1; } else if (visitor.firstNamespace()) { insertPos = file->startOf(visitor.firstNamespace()); } else { static const QRegularExpression regexp("^\\s*#include .*$"); const QTextCursor tc = file->document()->find( regexp, m_symbolPos, QTextDocument::FindBackward | QTextDocument::FindCaseSensitively); if (!tc.isNull()) insertPos = tc.position() + 1; else if (visitor.firstToken()) insertPos = file->startOf(visitor.firstToken()); } QString insertion = stringToInsert(); if (file->charAt(insertPos - 1) != QChar::ParagraphSeparator) insertion.prepend('\n'); if (file->charAt(insertPos) != QChar::ParagraphSeparator) insertion.append('\n'); file->apply(ChangeSet::makeInsert(insertPos, insertion)); } const QString m_className; const int m_symbolPos; }; AddIncludeForUndefinedIdentifierOp::AddIncludeForUndefinedIdentifierOp( const CppQuickFixInterface &interface, int priority, const QString &include, const QString &module = {}) : CppQuickFixOperation(interface, priority) , m_include(include) , m_module(module) { const QString desc = !m_module.isEmpty() ? Tr::tr("Add #include %1 and Project Dependency %2").arg(m_include, m_module) : Tr::tr("Add #include %1").arg(m_include); setDescription(desc); } void AddIncludeForUndefinedIdentifierOp::perform() { CppRefactoringFilePtr file = currentFile(); ChangeSet changes; insertNewIncludeDirective(m_include, file, semanticInfo().doc, changes); file->apply(changes); if (!m_module.isEmpty()) { Project * const project = ProjectManager::projectForFile(file->filePath()); if (!project) return; ProjectNode * const product = project->productNodeForFilePath(file->filePath()); if (product) product->addDependencies({m_module}); } } AddForwardDeclForUndefinedIdentifierOp::AddForwardDeclForUndefinedIdentifierOp( const CppQuickFixInterface &interface, int priority, const QString &fqClassName, int symbolPos) : CppQuickFixOperation(interface, priority), m_className(fqClassName), m_symbolPos(symbolPos) { setDescription(Tr::tr("Add Forward Declaration for %1").arg(m_className)); } /*! Adds an include for an undefined identifier or only forward declared identifier. Activates on: the undefined identifier */ class AddIncludeForUndefinedIdentifier : public CppQuickFixFactory { #ifdef WITH_TESTS public: static QObject *createTest(); #endif private: void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override { const NameAST *nameAst = nameUnderCursor(interface.path()); if (!nameAst || !nameAst->name) return; const LookupResult lookupResult = lookUpDefinition(interface, nameAst); if (lookupResult == LookupResult::Declared) return; QString className; const QString currentDocumentFilePath = interface.semanticInfo().doc->filePath().toUrlishString(); const HeaderPaths headerPaths = relevantHeaderPaths(currentDocumentFilePath); FilePaths headers; const QList matches = matchName(nameAst->name, &className); // Find an include file through the locator if (!matches.isEmpty()) { QList indexItems; const Snapshot forwardHeaders = forwardingHeaders(interface); for (const IndexItem::Ptr &info : matches) { if (!info || info->symbolName() != className || !ProjectFile::isHeader(info->filePath())) { continue; } indexItems << info; Snapshot localForwardHeaders = forwardHeaders; localForwardHeaders.insert(interface.snapshot().document(info->filePath())); FilePaths headerAndItsForwardingHeaders; headerAndItsForwardingHeaders << info->filePath(); headerAndItsForwardingHeaders += localForwardHeaders.filesDependingOn(info->filePath()); for (const FilePath &header : std::as_const(headerAndItsForwardingHeaders)) { const QString include = findShortestInclude(currentDocumentFilePath, header.toUrlishString(), headerPaths); if (include.size() > 2) { const QString headerFileName = info->filePath().fileName(); QTC_ASSERT(!headerFileName.isEmpty(), break); int priority = 0; if (headerFileName == className) priority = 2; else if (headerFileName.at(1).isUpper()) priority = 1; result << new AddIncludeForUndefinedIdentifierOp(interface, priority, include); headers << header; } } } if (lookupResult == LookupResult::NotDeclared && indexItems.size() == 1) { QString qualifiedName = Overview().prettyName(nameAst->name); if (qualifiedName.startsWith("::")) qualifiedName.remove(0, 2); if (indexItems.first()->scopedSymbolName().endsWith(qualifiedName)) { const Node * const node = ProjectTree::nodeForFile(interface.filePath()); FileType fileType = node && node->asFileNode() ? node->asFileNode()->fileType() : FileType::Unknown; if (fileType == FileType::Unknown && ProjectFile::isHeader(ProjectFile::classify(interface.filePath()))) { fileType = FileType::Header; } if (fileType == FileType::Header) { result << new AddForwardDeclForUndefinedIdentifierOp( interface, 0, indexItems.first()->scopedSymbolName(), interface.currentFile()->startOf(nameAst)); } } } } if (className.isEmpty()) return; // Fallback: Check the include paths for files that look like candidates // for the given name. if (!Utils::contains(headers, [&className](const FilePath &fp) { return fp.fileName() == className; })) { const QString include = findMatchingInclude(className, headerPaths); const auto matcher = [&include](const TextEditor::QuickFixOperation::Ptr &o) { const auto includeOp = o.dynamicCast(); return includeOp && includeOp->include() == include; }; if (!include.isEmpty() && !contains(result, matcher)) result << new AddIncludeForUndefinedIdentifierOp(interface, 1, include); } if (!result.isEmpty()) return; // Convenience functionality: A matching header might be available, but not currently // reachable because the respective component has not been added as a dependency // to the project. Kit * const currentKit = activeKitForCurrentProject(); if (!currentKit) return; if (const QString module = currentKit->moduleForHeader(className); !module.isEmpty()) { result << new AddIncludeForUndefinedIdentifierOp( interface, 2, '<' + className + '>', module); } } }; #ifdef WITH_TESTS using namespace Tests; using namespace ::CppEditor::Tests; using namespace ::CppEditor::Tests::Internal; // FIXME: Get rid of this nonsense namespace /// Delegates directly to AddIncludeForUndefinedIdentifierOp for easier testing. class AddIncludeForUndefinedIdentifierTestFactory : public CppQuickFixFactory { public: AddIncludeForUndefinedIdentifierTestFactory(const QString &include) : m_include(include) {} void doMatch(const CppQuickFixInterface &cppQuickFixInterface, QuickFixOperations &result) override { result << new AddIncludeForUndefinedIdentifierOp(cppQuickFixInterface, 0, m_include); } private: const QString m_include; }; class AddForwardDeclForUndefinedIdentifierTestFactory : public CppQuickFixFactory { public: AddForwardDeclForUndefinedIdentifierTestFactory(const QString &className, int symbolPos) : m_className(className), m_symbolPos(symbolPos) {} void doMatch(const CppQuickFixInterface &cppQuickFixInterface, QuickFixOperations &result) override { result << new AddForwardDeclForUndefinedIdentifierOp(cppQuickFixInterface, 0, m_className, m_symbolPos); } private: const QString m_className; const int m_symbolPos; }; class BringIdentifierIntoScopeTest : public QObject { Q_OBJECT private slots: void testAddIncludeForUndefinedIdentifier_data() { QTest::addColumn("headerPath"); QTest::addColumn("testDocuments"); QTest::addColumn("refactoringOperationIndex"); QTest::addColumn("includeForTestFactory"); const int firstRefactoringOperation = 0; const int secondRefactoringOperation = 1; QList testDocuments; QByteArray original; QByteArray expected; // ------------------------------------------------------------------------------------------- // Header File original = "class Foo {};\n"; expected = original; testDocuments << CppTestDocument::create("afile.h", original, expected); // Source File original = "#include \"header.h\"\n" "\n" "void f()\n" "{\n" " Fo@o foo;\n" "}\n" ; expected = "#include \"afile.h\"\n" "#include \"header.h\"\n" "\n" "void f()\n" "{\n" " Foo foo;\n" "}\n" ; testDocuments << CppTestDocument::create("afile.cpp", original, expected); QTest::newRow("onSimpleName") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- // Header File original = "namespace N { class Foo {}; }\n"; expected = original; testDocuments << CppTestDocument::create("afile.h", original, expected); // Source File original = "#include \"header.h\"\n" "\n" "void f()\n" "{\n" " N::Fo@o foo;\n" "}\n" ; expected = "#include \"afile.h\"\n" "#include \"header.h\"\n" "\n" "void f()\n" "{\n" " N::Foo foo;\n" "}\n" ; testDocuments << CppTestDocument::create("afile.cpp", original, expected); QTest::newRow("onNameOfQualifiedName") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- // Header File original = "namespace N { class Foo {}; }\n"; expected = original; testDocuments << CppTestDocument::create("afile.h", original, expected); // Source File original = "#include \"header.h\"\n" "\n" "void f()\n" "{\n" " @N::Foo foo;\n" "}\n" ; expected = "#include \"afile.h\"\n" "#include \"header.h\"\n" "\n" "void f()\n" "{\n" " N::Foo foo;\n" "}\n" ; testDocuments << CppTestDocument::create("afile.cpp", original, expected); QTest::newRow("onBaseOfQualifiedName") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- // Header File original = "class Foo { static void bar() {} };\n"; expected = original; testDocuments << CppTestDocument::create("afile.h", original, expected); // Source File original = "#include \"header.h\"\n" "\n" "void f()\n" "{\n" " @Foo::bar();\n" "}\n" ; expected = "#include \"afile.h\"\n" "#include \"header.h\"\n" "\n" "void f()\n" "{\n" " Foo::bar();\n" "}\n" ; testDocuments << CppTestDocument::create("afile.cpp", original, expected); QTest::newRow("onBaseOfQualifiedClassName") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- // Header File original = "template class Foo { static void bar() {} };\n"; expected = original; testDocuments << CppTestDocument::create("afile.h", original, expected); // Source File original = "#include \"header.h\"\n" "\n" "void f()\n" "{\n" " @Foo::bar();\n" "}\n" ; expected = "#include \"afile.h\"\n" "#include \"header.h\"\n" "\n" "void f()\n" "{\n" " Foo::bar();\n" "}\n" ; testDocuments << CppTestDocument::create("afile.cpp", original, expected); QTest::newRow("onBaseOfQualifiedTemplateClassName") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- // Header File original = "namespace N { template class Foo {}; }\n"; expected = original; testDocuments << CppTestDocument::create("afile.h", original, expected); // Source File original = "#include \"header.h\"\n" "\n" "void f()\n" "{\n" " @N::Foo foo;\n" "}\n" ; expected = "#include \"afile.h\"\n" "#include \"header.h\"\n" "\n" "void f()\n" "{\n" " N::Foo foo;\n" "}\n" ; testDocuments << CppTestDocument::create("afile.cpp", original, expected); QTest::newRow("onTemplateName") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- // Header File original = "namespace N { template class Foo {}; }\n"; expected = original; testDocuments << CppTestDocument::create("afile.h", original, expected); // Source File original = "#include \"header.h\"\n" "\n" "void f()\n" "{\n" " N::Bar<@Foo> foo;\n" "}\n" ; expected = "#include \"afile.h\"\n" "#include \"header.h\"\n" "\n" "void f()\n" "{\n" " N::Bar foo;\n" "}\n" ; testDocuments << CppTestDocument::create("afile.cpp", original, expected); QTest::newRow("onTemplateNameInsideArguments") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- // Header File original = "class Foo {};\n"; expected = original; testDocuments << CppTestDocument::create("afile.h", original, expected); // Source File original = "#include \"header.h\"\n" "\n" "class Foo;\n" "\n" "void f()\n" "{\n" " @Foo foo;\n" "}\n" ; expected = "#include \"afile.h\"\n" "#include \"header.h\"\n" "\n" "class Foo;\n" "\n" "void f()\n" "{\n" " Foo foo;\n" "}\n" ; testDocuments << CppTestDocument::create("afile.cpp", original, expected); QTest::newRow("withForwardDeclaration") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- // Header File original = "template class Foo {};\n"; expected = original; testDocuments << CppTestDocument::create("afile.h", original, expected); // Source File original = "#include \"header.h\"\n" "\n" "template class Foo;\n" "\n" "void f()\n" "{\n" " @Foo foo;\n" "}\n" ; expected = "#include \"afile.h\"\n" "#include \"header.h\"\n" "\n" "template class Foo;\n" "\n" "void f()\n" "{\n" " Foo foo;\n" "}\n" ; testDocuments << CppTestDocument::create("afile.cpp", original, expected); QTest::newRow("withForwardDeclaration2") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- // Header File original = "template class QMyClass {};\n"; expected = original; testDocuments << CppTestDocument::create("qmyclass.h", original, expected); // Forward Header File original = "#include \"qmyclass.h\"\n"; expected = original; testDocuments << CppTestDocument::create("QMyClass", original, expected); // Source File original = "#include \"header.h\"\n" "\n" "void f()\n" "{\n" " @QMyClass c;\n" "}\n" ; expected = "#include \"QMyClass\"\n" "#include \"header.h\"\n" "\n" "void f()\n" "{\n" " QMyClass c;\n" "}\n" ; testDocuments << CppTestDocument::create("afile.cpp", original, expected); QTest::newRow("withForwardHeader") << TestIncludePaths::globalIncludePath() << testDocuments << secondRefactoringOperation << ""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "void @f();\n" "#include \"file.moc\";\n" ; expected = "#include \"file.h\"\n" "\n" "void f();\n" "#include \"file.moc\";\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("insertingIgnoreMoc") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << "\"file.h\""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "#include \"y.h\"\n" "#include \"z.h\"\n" "\n@" ; expected = "#include \"file.h\"\n" "#include \"y.h\"\n" "#include \"z.h\"\n" "\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("insertingSortingTop") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << "\"file.h\""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "#include \"a.h\"\n" "#include \"z.h\"\n" "\n@" ; expected = "#include \"a.h\"\n" "#include \"file.h\"\n" "#include \"z.h\"\n" "\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("insertingSortingMiddle") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << "\"file.h\""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "#include \"a.h\"\n" "#include \"b.h\"\n" "\n@" ; expected = "#include \"a.h\"\n" "#include \"b.h\"\n" "#include \"file.h\"\n" "\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("insertingSortingBottom") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << "\"file.h\""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "#include \"b.h\"\n" "#include \"a.h\"\n" "\n@" ; expected = "#include \"b.h\"\n" "#include \"a.h\"\n" "#include \"file.h\"\n" "\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_appendToUnsorted") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << "\"file.h\""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "#include \n" "#include \n" "\n@" ; expected = "#include \"file.h\"\n" "\n" "#include \n" "#include \n" "\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_firstLocalIncludeAtFront") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << "\"file.h\""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "#include \"a.h\"\n" "#include \"b.h\"\n" "\n" "void @f();\n" ; expected = "#include \"a.h\"\n" "#include \"b.h\"\n" "\n" "#include \n" "\n" "void f();\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("firstGlobalIncludeAtBack") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "#include \"prefixa.h\"\n" "#include \"prefixb.h\"\n" "\n" "#include \"foo.h\"\n" "\n@" ; expected = "#include \"prefixa.h\"\n" "#include \"prefixb.h\"\n" "#include \"prefixc.h\"\n" "\n" "#include \"foo.h\"\n" "\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_preferGroupWithLongerMatchingPrefix") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << "\"prefixc.h\""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "#include \"lib/file.h\"\n" "#include \"lib/fileother.h\"\n" "\n@" ; expected = "#include \"lib/file.h\"\n" "#include \"lib/fileother.h\"\n" "\n" "#include \"file.h\"\n" "\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_newGroupIfOnlyDifferentIncludeDirs") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << "\"file.h\""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "#include \n" "#include \n" "#include \n" "\n@" ; expected = "#include \n" "#include \n" "#include \n" "#include \n" "\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_mixedDirsSorted") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "#include \n" "#include \n" "#include \n" "\n@" ; expected = "#include \n" "#include \n" "#include \n" "#include \n" "\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_mixedDirsUnsorted") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "#include \"a.h\"\n" "#include \n" "\n@" ; expected = "#include \"a.h\"\n" "#include \"z.h\"\n" "#include \n" "\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_mixedIncludeTypes1") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << "\"z.h\""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "#include \"z.h\"\n" "#include \n" "\n@" ; expected = "#include \"a.h\"\n" "#include \"z.h\"\n" "#include \n" "\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_mixedIncludeTypes2") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << "\"a.h\""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "#include \"z.h\"\n" "#include \n" "\n@" ; expected = "#include \"z.h\"\n" "#include \"lib/file.h\"\n" "#include \n" "\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_mixedIncludeTypes3") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << "\"lib/file.h\""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "#include \"z.h\"\n" "#include \n" "\n@" ; expected = "#include \"z.h\"\n" "#include \n" "#include \n" "\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_mixedIncludeTypes4") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "void @f();\n" ; expected = "#include \"file.h\"\n" "\n" "void f();\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_noinclude") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << "\"file.h\""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "#ifndef FOO_H\n" "#define FOO_H\n" "void @f();\n" "#endif\n" ; expected = "#ifndef FOO_H\n" "#define FOO_H\n" "\n" "#include \"file.h\"\n" "\n" "void f();\n" "#endif\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_onlyIncludeGuard") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << "\"file.h\""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "#pragma once\n" "void @f();\n" ; expected = "#pragma once\n" "\n" "#include \"file.h\"\n" "\n" "void f();\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_onlyPragmaOnce") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << "\"file.h\""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "\n" "// comment\n" "\n" "void @f();\n" ; expected = "\n" "// comment\n" "\n" "#include \"file.h\"\n" "\n" "void @f();\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_veryFirstIncludeCppStyleCommentOnTop") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << "\"file.h\""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "\n" "/*\n" " comment\n" " */\n" "\n" "void @f();\n" ; expected = "\n" "/*\n" " comment\n" " */\n" "\n" "#include \"file.h\"\n" "\n" "void @f();\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_veryFirstIncludeCStyleCommentOnTop") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << "\"file.h\""; testDocuments.clear(); // ------------------------------------------------------------------------------------------- original = "@QDir dir;\n" ; expected = "#include \n" "\n" "QDir dir;\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_checkQSomethingInQtIncludePaths") << TestIncludePaths::globalQtCoreIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); original = "std::s@tring s;\n" ; expected = "#include \n" "\n" "std::string s;\n" ; testDocuments << CppTestDocument::create("file.cpp", original, expected); QTest::newRow("inserting_std::string") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); original = "class A{};"; testDocuments << CppTestDocument::create("a.h", original, original); original = "class B{};"; testDocuments << CppTestDocument::create("b.h", original, original); original = "#include \"b.h\"\n" "@A a;\n" "B b;"; expected = "#include \"b.h\"\n\n" "#include \"a.h\"\n" "A a;\n" "B b;"; testDocuments << CppTestDocument::create("b.cpp", original, expected); QTest::newRow("preserve first header") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); original = "class C{};"; testDocuments << CppTestDocument::create("c.h", original, original); original = "class B{};"; testDocuments << CppTestDocument::create("b.h", original, original); original = "#include \"c.h\"\n" "C c;\n" "@B b;"; expected = "#include \"b.h\"\n" "#include \"c.h\"\n" "C c;\n" "B b;"; testDocuments << CppTestDocument::create("x.cpp", original, expected); QTest::newRow("do not preserve first header") << TestIncludePaths::globalIncludePath() << testDocuments << firstRefactoringOperation << ""; testDocuments.clear(); original = "static @C *c = nullptr;\n"; testDocuments << CppTestDocument::create("c.h", original, ""); original = "class C {};\n"; testDocuments << CppTestDocument::create("x.cpp", original, ""); QTest::newRow("do not include cpp file") << TestIncludePaths::globalIncludePath() << testDocuments << 0 << ""; testDocuments.clear(); } void testAddIncludeForUndefinedIdentifier() { QFETCH(FilePath, headerPath); QFETCH(QuickFixTestDocuments, testDocuments); QFETCH(int, refactoringOperationIndex); QFETCH(QString, includeForTestFactory); TemporaryDir temporaryDir; QVERIFY(temporaryDir.isValid()); for (const TestDocumentPtr &testDocument : std::as_const(testDocuments)) testDocument->setBaseDirectory(temporaryDir.path()); QScopedPointer factory; if (includeForTestFactory.isEmpty()) factory.reset(new AddIncludeForUndefinedIdentifier); else factory.reset(new AddIncludeForUndefinedIdentifierTestFactory(includeForTestFactory)); QuickFixOperationTest::run(testDocuments, factory.data(), headerPath, refactoringOperationIndex); } void testAddIncludeForUndefinedIdentifierNoDoubleQtHeaderInclude() { TemporaryDir temporaryDir; QVERIFY(temporaryDir.isValid()); QList testDocuments; QByteArray original; QByteArray expected; const QByteArray base = temporaryDir.path().toUtf8(); // This file makes the QDir definition available so that locator finds it. original = expected = "#include \n" "void avoidBeingRecognizedAsForwardingHeader();"; testDocuments << CppTestDocument::create(base + "/fileUsingQDir.cpp", original, expected); original = expected = "@QDir dir;\n"; testDocuments << CppTestDocument::create(base + "/fileWantsToUseQDir.cpp", original, expected); AddIncludeForUndefinedIdentifier factory; const QStringList expectedOperations = QStringList("Add #include "); QuickFixOfferedOperationsTest( testDocuments, &factory, {HeaderPath(TestIncludePaths::globalQtCoreIncludePath(), HeaderPathType::User)}, expectedOperations); } void testAddForwardDeclForUndefinedIdentifier_data() { QTest::addColumn("testDocuments"); QTest::addColumn("symbol"); QTest::addColumn("symbolPos"); QByteArray original; QByteArray expected; original = "#pragma once\n" "\n" "void f(const Blu@bb &b)\n" "{\n" "}\n" ; expected = "#pragma once\n" "\n" "\n" "class Blubb;\n" "void f(const Blubb &b)\n" "{\n" "}\n" ; QTest::newRow("unqualified symbol") << QuickFixTestDocuments{CppTestDocument::create("theheader.h", original, expected)} << "Blubb" << original.indexOf('@'); original = "#pragma once\n" "\n" "namespace NS {\n" "class C;\n" "}\n" "void f(const NS::Blu@bb &b)\n" "{\n" "}\n" ; expected = "#pragma once\n" "\n" "namespace NS {\n" "\n" "class Blubb;\n" "class C;\n" "}\n" "void f(const NS::Blubb &b)\n" "{\n" "}\n" ; QTest::newRow("qualified symbol, full namespace present") << QuickFixTestDocuments{CppTestDocument::create("theheader.h", original, expected)} << "NS::Blubb" << original.indexOf('@'); original = "#pragma once\n" "\n" "namespace NS {\n" "class C;\n" "}\n" "void f(const NS::NS2::Blu@bb &b)\n" "{\n" "}\n" ; expected = "#pragma once\n" "\n" "namespace NS {\n" "\n" "namespace NS2 { class Blubb; }\n" "class C;\n" "}\n" "void f(const NS::NS2::Blubb &b)\n" "{\n" "}\n" ; QTest::newRow("qualified symbol, partial namespace present") << QuickFixTestDocuments{CppTestDocument::create("theheader.h", original, expected)} << "NS::NS2::Blubb" << original.indexOf('@'); original = "#pragma once\n" "\n" "namespace NS {\n" "class C;\n" "}\n" "void f(const NS2::Blu@bb &b)\n" "{\n" "}\n" ; expected = "#pragma once\n" "\n" "\n" "namespace NS2 { class Blubb; }\n" "namespace NS {\n" "class C;\n" "}\n" "void f(const NS2::Blubb &b)\n" "{\n" "}\n" ; QTest::newRow("qualified symbol, other namespace present") << QuickFixTestDocuments{CppTestDocument::create("theheader.h", original, expected)} << "NS2::Blubb" << original.indexOf('@'); original = "#pragma once\n" "\n" "void f(const NS2::Blu@bb &b)\n" "{\n" "}\n" ; expected = "#pragma once\n" "\n" "\n" "namespace NS2 { class Blubb; }\n" "void f(const NS2::Blubb &b)\n" "{\n" "}\n" ; QTest::newRow("qualified symbol, no namespace present") << QuickFixTestDocuments{CppTestDocument::create("theheader.h", original, expected)} << "NS2::Blubb" << original.indexOf('@'); original = "#pragma once\n" "\n" "void f(const NS2::Blu@bb &b)\n" "{\n" "}\n" "namespace NS2 {}\n" ; expected = "#pragma once\n" "\n" "\n" "namespace NS2 { class Blubb; }\n" "void f(const NS2::Blubb &b)\n" "{\n" "}\n" "namespace NS2 {}\n" ; QTest::newRow("qualified symbol, existing namespace after symbol") << QuickFixTestDocuments{CppTestDocument::create("theheader.h", original, expected)} << "NS2::Blubb" << original.indexOf('@'); } void testAddForwardDeclForUndefinedIdentifier() { QFETCH(QuickFixTestDocuments, testDocuments); QFETCH(QString, symbol); QFETCH(int, symbolPos); TemporaryDir temporaryDir; QVERIFY(temporaryDir.isValid()); testDocuments.first()->setBaseDirectory(temporaryDir.path()); QScopedPointer factory( new AddForwardDeclForUndefinedIdentifierTestFactory(symbol, symbolPos)); QuickFixOperationTest::run({testDocuments}, factory.data(), ".", 0); } void testAddIncludeAndQtDependency() { // Set up project. Kit * const kit = Utils::findOr(KitManager::kits(), nullptr, [](const Kit *k) { return k->isValid() && !k->hasWarning() && k->value("QtSupport.QtInformation").isValid(); }); if (!kit) QSKIP("The test requires at least one valid kit with a valid Qt"); const auto projectDir = std::make_unique( ":/cppeditor/testcases/bring-identifier-into-scope/add-qt-dependency"); SourceFilesRefreshGuard refreshGuard; ProjectOpenerAndCloser projectMgr; QVERIFY(projectMgr.open(projectDir->absolutePath("add-qt-dependency.pro"), true, kit)); QVERIFY(refreshGuard.wait()); // Open source file and locate the use of Qt class. const FilePath sourceFilePath = projectDir->absolutePath("main.cpp"); QVERIFY2(sourceFilePath.exists(), qPrintable(sourceFilePath.toUserOutput())); const auto editor = qobject_cast( EditorManager::openEditor(sourceFilePath)); QVERIFY(editor); const auto doc = qobject_cast(editor->document()); QVERIFY(doc); QTextCursor classCursor = doc->document()->find("QApplication"); QVERIFY(!classCursor.isNull()); editor->setCursorPosition(classCursor.position()); const auto editorWidget = qobject_cast(editor->editorWidget()); QVERIFY(editorWidget); QVERIFY(TestCase::waitForRehighlightedSemanticDocument(editorWidget)); // Query factory. AddIncludeForUndefinedIdentifier factory; CppQuickFixInterface quickFixInterface(editorWidget, ExplicitlyInvoked); QuickFixOperations operations; factory.match(quickFixInterface, operations); QCOMPARE(operations.size(), qsizetype(1)); operations.first()->perform(); // Compare files. const FileFilter filter({"*_expected"}, QDir::Files); const FilePaths expectedDocuments = projectDir->filePath().dirEntries(filter); QVERIFY(!expectedDocuments.isEmpty()); for (const FilePath &expected : expectedDocuments) { static const QString suffix = "_expected"; const FilePath actual = expected.parentDir() .pathAppended(expected.fileName().chopped(suffix.size())); QVERIFY(actual.exists()); const auto actualContents = actual.fileContents(); QVERIFY(actualContents); const auto expectedContents = expected.fileContents(); const QByteArrayList actualLines = actualContents->split('\n'); const QByteArrayList expectedLines = expectedContents->split('\n'); if (actualLines.size() != expectedLines.size()) { qDebug().noquote().nospace() << "---\n" << *expectedContents << "EOF"; qDebug().noquote().nospace() << "+++\n" << *actualContents << "EOF"; } QCOMPARE(actualLines.size(), expectedLines.size()); for (int i = 0; i < actualLines.size(); ++i) { const QByteArray actualLine = actualLines.at(i); const QByteArray expectedLine = expectedLines.at(i); if (actualLine != expectedLine) qDebug() << "Unexpected content in line" << (i + 1) << "of file" << actual.fileName(); QCOMPARE(actualLine, expectedLine); } } } }; QObject *AddIncludeForUndefinedIdentifier::createTest() { return new BringIdentifierIntoScopeTest; } #endif // WITH_TESTS } // namespace void registerBringIdentifierIntoScopeQuickfixes() { CppQuickFixFactory::registerFactory(); } } // namespace CppEditor::Internal #ifdef WITH_TESTS #include #endif