diff --git a/kdevplatform/language/duchain/navigation/problemnavigationcontext.cpp b/kdevplatform/language/duchain/navigation/problemnavigationcontext.cpp index 1a2e19fe07..de5b99725b 100644 --- a/kdevplatform/language/duchain/navigation/problemnavigationcontext.cpp +++ b/kdevplatform/language/duchain/navigation/problemnavigationcontext.cpp @@ -1,273 +1,273 @@ /* Copyright 2009 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "problemnavigationcontext.h" #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace { QString KEY_INVOKE_ACTION(int num) { return QStringLiteral("invoke_action_%1").arg(num); } QString iconForSeverity(IProblem::Severity severity) { switch (severity) { case IProblem::Hint: return QStringLiteral("dialog-information"); case IProblem::Warning: return QStringLiteral("dialog-warning"); case IProblem::Error: return QStringLiteral("dialog-error"); case IProblem::NoSeverity: return {}; } Q_UNREACHABLE(); return {}; } QString htmlImg(const QString& iconName, KIconLoader::Group group) { auto* loader = KIconLoader::global(); const int size = loader->currentSize(group); return QStringLiteral("") .arg(size) .arg(loader->iconPath(iconName, group)); } } ProblemNavigationContext::ProblemNavigationContext(const QVector& problems, const Flags flags) : m_problems(problems) , m_flags(flags) , m_widget(nullptr) { // Sort problems vector: // 1) By severity // 2) By sourceString, if severities are equals std::sort(m_problems.begin(), m_problems.end(), [](const IProblem::Ptr& a, const IProblem::Ptr& b) { if (a->severity() < b->severity()) return true; if (a->severity() > b->severity()) return false; if (a->sourceString() < b->sourceString()) return true; return false; }); } ProblemNavigationContext::~ProblemNavigationContext() { delete m_widget; } QWidget* ProblemNavigationContext::widget() const { return m_widget; } bool ProblemNavigationContext::isWidgetMaximized() const { return false; } QString ProblemNavigationContext::name() const { return i18n("Problem"); } QString ProblemNavigationContext::escapedHtml(const QString& text) const { const QString htmlStart = QStringLiteral(""); const QString htmlEnd = QStringLiteral(""); QString result = text.trimmed(); if (!result.startsWith(htmlStart)) return result.toHtmlEscaped(); result.remove(htmlStart); result.remove(htmlEnd); return result; } void ProblemNavigationContext::html(IProblem::Ptr problem) { auto iconPath = iconForSeverity(problem->severity()); modifyHtml() += QStringLiteral(""); modifyHtml() += QStringLiteral("").arg(htmlImg(iconPath, KIconLoader::Panel)); // BEGIN: right column modifyHtml() += QStringLiteral(""); // END: right column modifyHtml() += QStringLiteral("
%1"); modifyHtml() += i18n("Problem in %1", problem->sourceString()); modifyHtml() += QStringLiteral("
"); if (m_flags & ShowLocation) { modifyHtml() += labelHighlight(i18n("Location: ")); makeLink(QStringLiteral("%1 :%2") .arg(problem->finalLocation().document.toUrl().fileName()) .arg(problem->finalLocation().start().line() + 1), QString(), NavigationAction(problem->finalLocation().document.toUrl(), problem->finalLocation().start()) ); modifyHtml() += QStringLiteral("
"); } QString description = escapedHtml(problem->description()); QString explanation = escapedHtml(problem->explanation()); modifyHtml() += description; // Add only non-empty explanation which differs from the problem description. // Skip this if we have more than one problem. if (m_problems.size() == 1 && !explanation.isEmpty() && explanation != description) modifyHtml() += QLatin1String("

") + explanation + QLatin1String("

"); modifyHtml() += QStringLiteral("
"); - auto diagnostics = problem->diagnostics(); + const auto diagnostics = problem->diagnostics(); if (!diagnostics.isEmpty()) { DUChainReadLocker lock; for (auto diagnostic : diagnostics) { modifyHtml() += QStringLiteral("

"); modifyHtml() += labelHighlight(QStringLiteral("%1: ").arg(diagnostic->severityString())); modifyHtml() += escapedHtml(diagnostic->description()); const DocumentRange range = diagnostic->finalLocation(); Declaration* declaration = DUChainUtils::itemUnderCursor(range.document.toUrl(), range.start()).declaration; if (declaration) { modifyHtml() += i18n("
See: "); makeLink(declaration->toString(), DeclarationPointer(declaration), NavigationAction::NavigateDeclaration); modifyHtml() += i18n(" in "); const auto url = declaration->url().toUrl(); makeLink(QStringLiteral("%1 :%2") .arg(url.fileName()) .arg(declaration->rangeInCurrentRevision().start().line() + 1), url.toDisplayString(QUrl::PreferLocalFile), NavigationAction(url, declaration->rangeInCurrentRevision().start())); } else if (range.start().isValid()) { modifyHtml() += i18n("
See: "); const auto url = range.document.toUrl(); makeLink(QStringLiteral("%1 :%2") .arg(url.fileName()) .arg(range.start().line() + 1), url.toDisplayString(QUrl::PreferLocalFile), NavigationAction(url, range.start())); } modifyHtml() += QStringLiteral("

"); } } auto assistant = problem->solutionAssistant(); if (assistant && !assistant->actions().isEmpty()) { modifyHtml() += QStringLiteral("").arg(QStringLiteral( "#b3d4ff")); modifyHtml() += QStringLiteral(""); modifyHtml() += QStringLiteral("
%1").arg(htmlImg(QStringLiteral( "dialog-ok-apply"), KIconLoader::Panel)); const int startIndex = m_assistantsActions.size(); int currentIndex = startIndex; const auto assistantActions = assistant->actions(); for (auto& assistantAction : assistantActions) { m_assistantsActions.append(assistantAction); if (currentIndex != startIndex) modifyHtml() += QStringLiteral("
"); makeLink(i18n("Solution (%1)", currentIndex + 1), KEY_INVOKE_ACTION(currentIndex), NavigationAction(KEY_INVOKE_ACTION(currentIndex))); modifyHtml() += QLatin1String(": ") + assistantAction->description().toHtmlEscaped(); ++currentIndex; } modifyHtml() += QStringLiteral("
"); } } QString ProblemNavigationContext::html(bool shorten) { AbstractNavigationContext::html(shorten); clear(); m_assistantsActions.clear(); int problemIndex = 0; for (auto& problem : qAsConst(m_problems)) { html(problem); if (++problemIndex != m_problems.size()) modifyHtml() += QStringLiteral("
"); } return currentHtml(); } NavigationContextPointer ProblemNavigationContext::executeKeyAction(const QString& key) { if (key.startsWith(QLatin1String("invoke_action_"))) { const int index = key.midRef(strlen("invoke_action_")).toInt(); executeAction(index); } return {}; } void ProblemNavigationContext::executeAction(int index) { if (index < 0 || index >= m_assistantsActions.size()) return; auto action = m_assistantsActions.at(index); Q_ASSERT(action); if (action) { action->execute(); if (topContext()) DUChain::self()->updateContextForUrl(topContext()->url(), TopDUContext::ForceUpdate); } else { qCWarning(LANGUAGE()) << "No such action"; return; } } diff --git a/plugins/clang/clangparsejob.cpp b/plugins/clang/clangparsejob.cpp index a0b0ca801b..5abadc833d 100644 --- a/plugins/clang/clangparsejob.cpp +++ b/plugins/clang/clangparsejob.cpp @@ -1,392 +1,392 @@ /* This file is part of KDevelop Copyright 2013 Olivier de Gaalon Copyright 2013 Milian Wolff This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "clangparsejob.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "clangsettings/clangsettingsmanager.h" #include "duchain/clanghelpers.h" #include "duchain/clangpch.h" #include "duchain/duchainutils.h" #include "duchain/parsesession.h" #include "duchain/clangindex.h" #include "duchain/clangparsingenvironmentfile.h" #include "util/clangdebug.h" #include "util/clangtypes.h" #include "util/clangutils.h" #include "clangsupport.h" #include "duchain/documentfinderhelpers.h" #include #include #include #include #include using namespace KDevelop; namespace { QString findConfigFile(const QString& forFile, const QString& configFileName) { QDir dir = QFileInfo(forFile).dir(); while (dir.exists()) { const QFileInfo customIncludePaths(dir, configFileName); if (customIncludePaths.exists()) { return customIncludePaths.absoluteFilePath(); } if (!dir.cdUp()) { break; } } return {}; } Path::List readPathListFile(const QString& filepath) { if (filepath.isEmpty()) { return {}; } QFile f(filepath); if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { return {}; } const QString text = QString::fromLocal8Bit(f.readAll()); const QStringList lines = text.split(QLatin1Char('\n'), QString::SkipEmptyParts); Path::List paths; paths.reserve(lines.length()); for (const auto& line : lines) { paths << Path(line); } return paths; } /** * File should contain the header to precompile and use while parsing * @returns the first path in the file */ Path userDefinedPchIncludeForFile(const QString& sourcefile) { const QString pchIncludeFilename = QStringLiteral(".kdev_pch_include"); const auto paths = readPathListFile(findConfigFile(sourcefile, pchIncludeFilename)); return paths.isEmpty() ? Path() : paths.first(); } ProjectFileItem* findProjectFileItem(const IndexedString& url, bool* hasBuildSystemInfo) { ProjectFileItem* file = nullptr; *hasBuildSystemInfo = false; const auto& projects = ICore::self()->projectController()->projects(); for (auto project : projects) { - auto files = project->filesForPath(url); + const auto files = project->filesForPath(url); if (files.isEmpty()) { continue; } file = files.last(); // A file might be defined in different targets. // Prefer file items defined inside a target with non-empty includes. for (auto f: files) { if (!dynamic_cast(f->parent())) { continue; } file = f; if (!IDefinesAndIncludesManager::manager()->includes(f, IDefinesAndIncludesManager::ProjectSpecific).isEmpty()) { break; } } } if (file && file->project()) { if (auto bsm = file->project()->buildSystemManager()) { *hasBuildSystemInfo = bsm->hasBuildInfo(file); } } return file; } ClangParsingEnvironmentFile* parsingEnvironmentFile(const TopDUContext* context) { return dynamic_cast(context->parsingEnvironmentFile().data()); } DocumentChangeTracker* trackerForUrl(const IndexedString& url) { return ICore::self()->languageController()->backgroundParser()->trackerForUrl(url); } } ClangParseJob::ClangParseJob(const IndexedString& url, ILanguageSupport* languageSupport) : ParseJob(url, languageSupport) { const auto tuUrl = clang()->index()->translationUnitForUrl(url); bool hasBuildSystemInfo; if (auto file = findProjectFileItem(tuUrl, &hasBuildSystemInfo)) { m_environment.addIncludes(IDefinesAndIncludesManager::manager()->includes(file)); m_environment.addFrameworkDirectories(IDefinesAndIncludesManager::manager()->frameworkDirectories(file)); m_environment.addDefines(IDefinesAndIncludesManager::manager()->defines(file)); m_environment.setParserSettings(ClangSettingsManager::self()->parserSettings(file)); if (hasBuildSystemInfo) { // Assume the builder invokes the compiler in the build directory. m_environment.setWorkingDirectory(file->project()->buildSystemManager()->buildDirectory(file)); } } else { m_environment.addIncludes(IDefinesAndIncludesManager::manager()->includes(tuUrl.str())); m_environment.addFrameworkDirectories(IDefinesAndIncludesManager::manager()->frameworkDirectories(tuUrl.str())); m_environment.addDefines(IDefinesAndIncludesManager::manager()->defines(tuUrl.str())); m_environment.setParserSettings(ClangSettingsManager::self()->parserSettings(tuUrl.str())); } const bool isSource = ClangHelpers::isSource(tuUrl.str()); m_environment.setQuality( isSource ? (hasBuildSystemInfo ? ClangParsingEnvironment::BuildSystem : ClangParsingEnvironment::Source) : ClangParsingEnvironment::Unknown ); m_environment.setTranslationUnitUrl(tuUrl); Path::List projectPaths; const auto& projects = ICore::self()->projectController()->projects(); projectPaths.reserve(projects.size()); for (auto project : projects) { projectPaths.append(project->path()); } m_environment.setProjectPaths(projectPaths); m_unsavedFiles = ClangUtils::unsavedFiles(); const auto documents = ICore::self()->documentController()->openDocuments(); for (auto* document : documents) { auto textDocument = document->textDocument(); if ( !textDocument ) { continue; } const IndexedString indexedUrl(textDocument->url()); if (indexedUrl == tuUrl) { m_tuDocumentIsUnsaved = true; } m_unsavedRevisions.insert(indexedUrl, ModificationRevision::revisionForFile(indexedUrl)); } if (auto tracker = trackerForUrl(url)) { tracker->reset(); } } ClangSupport* ClangParseJob::clang() const { return static_cast(languageSupport()); } void ClangParseJob::run(ThreadWeaver::JobPointer /*self*/, ThreadWeaver::Thread* /*thread*/) { QReadLocker parseLock(languageSupport()->parseLock()); if (abortRequested()) { return; } { const auto tuUrlStr = m_environment.translationUnitUrl().str(); if (!m_tuDocumentIsUnsaved && !QFile::exists(tuUrlStr)) { // maybe we requested a parse job some time ago but now the file // does not exist anymore. return early then clang()->index()->unpinTranslationUnitForUrl(document()); return; } m_environment.addIncludes(IDefinesAndIncludesManager::manager()->includesInBackground(tuUrlStr)); m_environment.addFrameworkDirectories(IDefinesAndIncludesManager::manager()->frameworkDirectoriesInBackground(tuUrlStr)); m_environment.addDefines(IDefinesAndIncludesManager::manager()->definesInBackground(tuUrlStr)); m_environment.addParserArguments(IDefinesAndIncludesManager::manager()->parserArgumentsInBackground(tuUrlStr)); m_environment.setPchInclude(userDefinedPchIncludeForFile(tuUrlStr)); } if (abortRequested()) { return; } // NOTE: we must have all declarations, contexts and uses available for files that are opened in the editor // it is very hard to check this for all included files of this TU, and previously lead to problems // when we tried to skip function bodies as an optimization for files that where not open in the editor. // now, we always build everything, which is correct but a tad bit slower. we can try to optimize later. setMinimumFeatures(static_cast(minimumFeatures() | TopDUContext::AllDeclarationsContextsAndUses)); if (minimumFeatures() & AttachASTWithoutUpdating) { // The context doesn't need to be updated, but has no AST attached (restored from disk), // so attach AST to it, without updating DUChain ParseSession session(createSessionData()); DUChainWriteLocker lock; auto ctx = DUChainUtils::standardContextForUrl(document().toUrl()); if (!ctx) { clangDebug() << "Lost context while attaching AST"; return; } ctx->setAst(IAstContainer::Ptr(session.data())); if (minimumFeatures() & UpdateHighlighting) { lock.unlock(); languageSupport()->codeHighlighting()->highlightDUChain(ctx); } return; } { UrlParseLock urlLock(document()); if (abortRequested() || !isUpdateRequired(ParseSession::languageString())) { return; } } ParseSession session(ClangIntegration::DUChainUtils::findParseSessionData(document(), m_environment.translationUnitUrl())); if (abortRequested()) { return; } if (!session.data() || !session.reparse(m_unsavedFiles, m_environment)) { session.setData(createSessionData()); } if (!session.unit()) { // failed to parse file, unpin and don't try again clang()->index()->unpinTranslationUnitForUrl(document()); return; } if (!clang_getFile(session.unit(), document().byteArray().constData())) { // this parse job's document does not exist in the pinned translation unit // so we need to unpin and re-add this document // Ideally we'd reset m_environment and session, but this is much simpler // and shouldn't be a common case clang()->index()->unpinTranslationUnitForUrl(document()); if (!(minimumFeatures() & Rescheduled)) { auto features = static_cast(minimumFeatures() | Rescheduled); ICore::self()->languageController()->backgroundParser()->addDocument(document(), features, priority()); } return; } Imports imports = ClangHelpers::tuImports(session.unit()); IncludeFileContexts includedFiles; if (auto pch = clang()->index()->pch(m_environment)) { auto pchFile = pch->mapFile(session.unit()); includedFiles = pch->mapIncludes(session.unit()); includedFiles.insert(pchFile, pch->context()); auto tuFile = clang_getFile(session.unit(), m_environment.translationUnitUrl().byteArray().constData()); imports.insert(tuFile, { pchFile, CursorInRevision(0, 0) } ); } if (abortRequested()) { return; } auto context = ClangHelpers::buildDUChain(session.mainFile(), imports, session, minimumFeatures(), includedFiles, clang()->index(), [this] { return abortRequested(); }); setDuChain(context); if (abortRequested()) { return; } if (context) { if (minimumFeatures() & TopDUContext::AST) { DUChainWriteLocker lock; context->setAst(IAstContainer::Ptr(session.data())); } #ifdef QT_DEBUG DUChainReadLocker lock; auto file = parsingEnvironmentFile(context); Q_ASSERT(file); // verify that features and environment where properly set in ClangHelpers::buildDUChain Q_ASSERT(file->featuresSatisfied(TopDUContext::Features(minimumFeatures() & ~TopDUContext::ForceUpdateRecursive))); if (trackerForUrl(context->url())) { Q_ASSERT(file->featuresSatisfied(TopDUContext::AllDeclarationsContextsAndUses)); } #endif } for (const auto& context : qAsConst(includedFiles)) { if (!context) { continue; } { // prefer the editor modification revision, instead of the on-disk revision auto it = m_unsavedRevisions.find(context->url()); if (it != m_unsavedRevisions.end()) { DUChainWriteLocker lock; auto file = parsingEnvironmentFile(context); Q_ASSERT(file); file->setModificationRevision(it.value()); } } if (trackerForUrl(context->url())) { if (clang()->index()->translationUnitForUrl(context->url()) == m_environment.translationUnitUrl()) { // cache the parse session and the contained translation unit for this chain // this then allows us to quickly reparse the document if it is changed by // the user // otherwise no editor component is open for this document and we can dispose // the TU to save memory // share the session data with all contexts that are pinned to this TU DUChainWriteLocker lock; context->setAst(IAstContainer::Ptr(session.data())); } languageSupport()->codeHighlighting()->highlightDUChain(context); } } } ParseSessionData::Ptr ClangParseJob::createSessionData() const { return ParseSessionData::Ptr(new ParseSessionData(m_unsavedFiles, clang()->index(), m_environment, ParseSessionData::NoOption)); } const ParsingEnvironment* ClangParseJob::environment() const { return &m_environment; } diff --git a/plugins/clang/codecompletion/context.cpp b/plugins/clang/codecompletion/context.cpp index 8ef06c769e..5565940b12 100644 --- a/plugins/clang/codecompletion/context.cpp +++ b/plugins/clang/codecompletion/context.cpp @@ -1,1395 +1,1395 @@ /* * This file is part of KDevelop * Copyright 2014 Milian Wolff * Copyright 2015 Sergey Kalinichev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "context.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../util/clangdebug.h" #include "../util/clangtypes.h" #include "../util/clangutils.h" #include "../duchain/clangdiagnosticevaluator.h" #include "../duchain/parsesession.h" #include "../duchain/duchainutils.h" #include "../duchain/navigationwidget.h" #include "../clangsettings/clangsettingsmanager.h" #include #include #include #include #include using namespace KDevelop; namespace { /// Maximum return-type string length in completion items const int MAX_RETURN_TYPE_STRING_LENGTH = 20; /// Priority of code-completion results. NOTE: Keep in sync with Clang code base. enum CodeCompletionPriority { /// Priority for the next initialization in a constructor initializer list. CCP_NextInitializer = 7, /// Priority for an enumeration constant inside a switch whose condition is of the enumeration type. CCP_EnumInCase = 7, CCP_LocalDeclarationMatch = 8, CCP_DeclarationMatch = 12, CCP_LocalDeclarationSimiliar = 17, /// Priority for a send-to-super completion. CCP_SuperCompletion = 20, CCP_DeclarationSimiliar = 25, /// Priority for a declaration that is in the local scope. CCP_LocalDeclaration = 34, /// Priority for a member declaration found from the current method or member function. CCP_MemberDeclaration = 35, /// Priority for a language keyword (that isn't any of the other categories). CCP_Keyword = 40, /// Priority for a code pattern. CCP_CodePattern = 40, /// Priority for a non-type declaration. CCP_Declaration = 50, /// Priority for a type. CCP_Type = CCP_Declaration, /// Priority for a constant value (e.g., enumerator). CCP_Constant = 65, /// Priority for a preprocessor macro. CCP_Macro = 70, /// Priority for a nested-name-specifier. CCP_NestedNameSpecifier = 75, /// Priority for a result that isn't likely to be what the user wants, but is included for completeness. CCP_Unlikely = 80 }; /** * Common base class for Clang code completion items. */ template class CompletionItem : public Base { public: CompletionItem(const QString& display, const QString& prefix) : Base() , m_display(display) , m_prefix(prefix) , m_unimportant(false) { } ~CompletionItem() override = default; QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* /*model*/) const override { if (role == Qt::DisplayRole) { if (index.column() == CodeCompletionModel::Prefix) { return m_prefix; } else if (index.column() == CodeCompletionModel::Name) { return m_display; } } return {}; } void markAsUnimportant() { m_unimportant = true; } protected: QString m_display; QString m_prefix; bool m_unimportant; }; class OverrideItem : public CompletionItem { public: OverrideItem(const QString& nameAndParams, const QString& returnType) : CompletionItem( nameAndParams, i18n("Override %1", returnType) ) , m_returnType(returnType) { } QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* model) const override { if (role == Qt::DecorationRole) { if (index.column() == KTextEditor::CodeCompletionModel::Icon) { return QIcon::fromTheme(QStringLiteral("CTparents")); } } return CompletionItem::data(index, role, model); } void execute(KTextEditor::View* view, const KTextEditor::Range& word) override { QString replacement = m_returnType + QLatin1Char(' ') + m_display.replace(QRegularExpression(QStringLiteral("\\s*=\\s*0")), QString()); bool appendSpecifer = true; if (const auto* project = KDevelop::ICore::self()->projectController()->findProjectForUrl(view->document()->url())) { const auto arguments = KDevelop::IDefinesAndIncludesManager::manager()->parserArguments( project->filesForPath(IndexedString(view->document()->url().path())).first()); const auto match = QRegularExpression(QStringLiteral(R"(-std=c\+\+(\w+))")).match(arguments); appendSpecifer = match.hasMatch(); // assume non-modern if no standard is specified if (appendSpecifer) { const auto standard = match.capturedRef(1); appendSpecifer = (standard != QLatin1String("98") && standard != QLatin1String("03")); } } if (appendSpecifer) { replacement.append(QLatin1String(" override;")); } else { replacement.append(QLatin1Char(';')); } DocumentChange overrideChange(IndexedString(view->document()->url()), word, QString{}, replacement); overrideChange.m_ignoreOldText = true; DocumentChangeSet changes; changes.addChange(overrideChange); changes.applyAllChanges(); } private: QString m_returnType; }; /** * Specialized completion item class for items which are represented by a Declaration */ class DeclarationItem : public CompletionItem { public: DeclarationItem(Declaration* dec, const QString& display, const QString& prefix, const QString& replacement) : CompletionItem(display, prefix) , m_replacement(replacement) { m_declaration = dec; } QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* model) const override { if (role == CodeCompletionModel::MatchQuality && m_matchQuality) { return m_matchQuality; } auto ret = CompletionItem::data(index, role, model); if (ret.isValid()) { return ret; } return NormalDeclarationCompletionItem::data(index, role, model); } void execute(KTextEditor::View* view, const KTextEditor::Range& word) override { QString repl = m_replacement; DUChainReadLocker lock; if(!m_declaration){ return; } if(m_declaration->isFunctionDeclaration()) { const auto functionType = m_declaration->type(); // protect against buggy code that created the m_declaration, // to mark it as a function but not assign a function type if (!functionType) return; auto doc = view->document(); // Function pointer? bool funcptr = false; const auto line = doc->line(word.start().line()); auto pos = word.end().column() - 1; while ( pos > 0 && (line.at(pos).isLetterOrNumber() || line.at(pos) == QLatin1Char(':')) ) { pos--; if ( line.at(pos) == QLatin1Char('&') ) { funcptr = true; break; } } auto restEmpty = doc->characterAt(word.end() + KTextEditor::Cursor{0, 1}) == QChar(); bool didAddParentheses = false; if ( !funcptr && doc->characterAt(word.end()) != QLatin1Char('(') ) { repl += QLatin1String("()"); didAddParentheses = true; } view->document()->replaceText(word, repl); if (functionType->indexedArgumentsSize() && didAddParentheses) { view->setCursorPosition(word.start() + KTextEditor::Cursor(0, repl.size() - 1)); } auto returnTypeIntegral = functionType->returnType().cast(); if ( restEmpty && !funcptr && returnTypeIntegral && returnTypeIntegral->dataType() == IntegralType::TypeVoid ) { // function returns void and rest of line is empty -- nothing can be done with the result if (functionType->indexedArgumentsSize() ) { // we placed the cursor inside the () view->document()->insertText(view->cursorPosition() + KTextEditor::Cursor(0, 1), QStringLiteral(";")); } else { // we placed the cursor after the () view->document()->insertText(view->cursorPosition(), QStringLiteral(";")); view->setCursorPosition(view->cursorPosition() + KTextEditor::Cursor{0, 1}); } } } else { view->document()->replaceText(word, repl); } } bool createsExpandingWidget() const override { return true; } QWidget* createExpandingWidget(const CodeCompletionModel* /*model*/) const override { return new ClangNavigationWidget(m_declaration, AbstractNavigationWidget::EmbeddableWidget); } int matchQuality() const { return m_matchQuality; } ///Sets match quality from 0 to 10. 10 is the best fit. void setMatchQuality(int value) { m_matchQuality = value; } void setInheritanceDepth(int depth) { m_inheritanceDepth = depth; } int argumentHintDepth() const override { return m_depth; } void setArgumentHintDepth(int depth) { m_depth = depth; } protected: int m_matchQuality = 0; int m_depth = 0; QString m_replacement; }; class ImplementsItem : public DeclarationItem { public: static QString replacement(const FuncImplementInfo& info) { QString replacement = info.templatePrefix; if (!info.isDestructor && !info.isConstructor) { replacement += info.returnType + QLatin1Char(' '); } replacement += info.prototype + QLatin1String("\n{\n}\n"); return replacement; } explicit ImplementsItem(const FuncImplementInfo& item) : DeclarationItem(item.declaration.data(), item.prototype, i18n("Implement %1", item.isConstructor ? QStringLiteral("") : item.isDestructor ? QStringLiteral("") : item.returnType), replacement(item) ) { } QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* model) const override { if (index.column() == CodeCompletionModel::Arguments) { // our display string already contains the arguments return {}; } return DeclarationItem::data(index, role, model); } void execute(KTextEditor::View* view, const KTextEditor::Range& word) override { auto* const document = view->document(); DocumentChangeSet changes; KTextEditor::Cursor rangeStart = word.start(); // try and replace leading typed text that match the proposed implementation const QString leading = document->line(word.end().line()).left(word.end().column()); const QString leadingNoSpace = removeWhitespace(leading); if (!leadingNoSpace.isEmpty() && (removeWhitespace(m_display).startsWith(leadingNoSpace) || removeWhitespace(m_replacement).startsWith(leadingNoSpace))) { const int removeSize = leading.end() - std::find_if_not(leading.begin(), leading.end(), [](QChar c){ return c.isSpace(); }); rangeStart = {word.end().line(), word.end().column() - removeSize}; } DocumentChange change(IndexedString(view->document()->url()), KTextEditor::Range(rangeStart, word.end()), QString(), m_replacement); change.m_ignoreOldText = true; changes.addChange(change); changes.applyAllChanges(); // Place cursor after the opening brace // arbitrarily chose 4, as it would accomodate the template and return types on their own line const auto searchRange = KTextEditor::Range(rangeStart, rangeStart.line() + 4, 0); const auto results = view->document()->searchText(searchRange, QStringLiteral("{")); if (!results.isEmpty()) { view->setCursorPosition(results.first().end()); } } }; class ArgumentHintItem : public DeclarationItem { public: struct CurrentArgumentRange { int start; int end; }; ArgumentHintItem(Declaration* decl, const QString& prefix, const QString& name, const QString& arguments, const CurrentArgumentRange& range) : DeclarationItem(decl, name, prefix, {}) , m_range(range) , m_arguments(arguments) {} QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* model) const override { if (role == CodeCompletionModel::CustomHighlight && index.column() == CodeCompletionModel::Arguments && argumentHintDepth()) { QTextCharFormat boldFormat; boldFormat.setFontWeight(QFont::Bold); const QList highlighting { QVariant(m_range.start), QVariant(m_range.end), boldFormat, }; return highlighting; } if (role == CodeCompletionModel::HighlightingMethod && index.column() == CodeCompletionModel::Arguments && argumentHintDepth()) { return QVariant(CodeCompletionModel::CustomHighlighting); } if (index.column() == CodeCompletionModel::Arguments) { return m_arguments; } return DeclarationItem::data(index, role, model); } private: CurrentArgumentRange m_range; QString m_arguments; }; /** * A minimalistic completion item for macros and such */ class SimpleItem : public CompletionItem { public: SimpleItem(const QString& display, const QString& prefix, const QString& replacement, const QIcon& icon = QIcon()) : CompletionItem(display, prefix) , m_replacement(replacement) , m_icon(icon) { } void execute(KTextEditor::View* view, const KTextEditor::Range& word) override { view->document()->replaceText(word, m_replacement); } QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* model) const override { if (role == Qt::DecorationRole && index.column() == KTextEditor::CodeCompletionModel::Icon) { return m_icon; } if (role == CodeCompletionModel::UnimportantItemRole) { return m_unimportant; } return CompletionItem::data(index, role, model); } private: QString m_replacement; QIcon m_icon; }; /** * Return true in case position @p position represents a cursor inside a comment */ bool isInsideComment(CXTranslationUnit unit, CXFile file, const KTextEditor::Cursor& position) { if (!position.isValid()) { return false; } // TODO: This may get very slow for a large TU, investigate if we can improve this function auto begin = clang_getLocation(unit, file, 1, 1); auto end = clang_getLocation(unit, file, position.line() + 1, position.column() + 1); CXSourceRange range = clang_getRange(begin, end); // tokenize the whole range from the start until 'position' // if we detect a comment token at this position, return true const ClangTokens tokens(unit, range); for (CXToken token : tokens) { CXTokenKind tokenKind = clang_getTokenKind(token); if (tokenKind != CXToken_Comment) { continue; } auto range = ClangRange(clang_getTokenExtent(unit, token)); if (range.toRange().contains(position)) { return true; } } return false; } QString& elideStringRight(QString& str, int length) { if (str.size() > length + 3) { return str.replace(length, str.size() - length, QStringLiteral("...")); } return str; } /** * @return Value suited for @ref CodeCompletionModel::MatchQuality in the range [0.0, 10.0] (the higher the better) * * See https://clang.llvm.org/doxygen/CodeCompleteConsumer_8h_source.html for list of priorities * They (currently) have a range from [-3, 80] (the lower, the better) */ int codeCompletionPriorityToMatchQuality(unsigned int completionPriority) { return 10u - qBound(0u, completionPriority, 80u) / 8; } int adjustPriorityForType(const AbstractType::Ptr& type, int completionPriority) { const auto modifier = 4; if (type) { const auto whichType = type->whichType(); if (whichType == AbstractType::TypePointer || whichType == AbstractType::TypeReference) { // Clang considers all pointers as similar, this is not what we want. completionPriority += modifier; } else if (whichType == AbstractType::TypeStructure) { // Clang considers all classes as similar too... completionPriority += modifier; } else if (whichType == AbstractType::TypeDelayed) { completionPriority += modifier; } else if (whichType == AbstractType::TypeAlias) { auto aliasedType = type.cast(); return adjustPriorityForType(aliasedType ? aliasedType->type() : AbstractType::Ptr(), completionPriority); } else if (whichType == AbstractType::TypeFunction) { auto functionType = type.cast(); return adjustPriorityForType(functionType ? functionType->returnType() : AbstractType::Ptr(), completionPriority); } } else { completionPriority += modifier; } return completionPriority; } /// Adjusts priority for the @p decl int adjustPriorityForDeclaration(Declaration* decl, unsigned int completionPriority) { if(completionPriority < CCP_LocalDeclarationSimiliar || completionPriority > CCP_SuperCompletion){ return completionPriority; } return adjustPriorityForType(decl->abstractType(), completionPriority); } /** * @return Whether the declaration represented by identifier @p identifier qualifies as completion result * * For example, we don't want to offer SomeClass::SomeClass as completion item to the user * (otherwise we'd end up generating code such as 's.SomeClass();') */ bool isValidCompletionIdentifier(const QualifiedIdentifier& identifier) { const int count = identifier.count(); if (identifier.count() < 2) { return true; } const Identifier scope = identifier.at(count-2); const Identifier id = identifier.last(); if (scope == id) { return false; // is constructor } const QString idString = id.toString(); if (idString.startsWith(QLatin1Char('~')) && scope.toString() == idString.midRef(1)) { return false; // is destructor } return true; } /** * @return Whether the declaration represented by identifier @p identifier qualifies as "special" completion result * * "Special" completion results are items that are likely not regularly used. * * Examples: * - 'SomeClass::operator=(const SomeClass&)' */ bool isValidSpecialCompletionIdentifier(const QualifiedIdentifier& identifier) { if (identifier.count() < 2) { return false; } const Identifier id = identifier.last(); const QString idString = id.toString(); if (idString.startsWith(QLatin1String("operator="))) { return true; // is assignment operator } return false; } Declaration* findDeclaration(const QualifiedIdentifier& qid, const DUContextPointer& ctx, const CursorInRevision& position, QSet& handled) { PersistentSymbolTable::Declarations decl = PersistentSymbolTable::self().declarations(qid); const auto top = ctx->topContext(); const auto& importedContexts = top->importedParentContexts(); for (auto it = decl.iterator(); it; ++it) { // if the context is not included, then this match is not correct for our consideration // this fixes issues where we used to include matches from files that did not have // anything to do with the current TU, e.g. the main from a different file or stuff like that // it also reduces the chance of us picking up a function of the same name from somewhere else // also, this makes sure the context has the correct language and we don't get confused by stuff // from other language plugins if (std::none_of(importedContexts.begin(), importedContexts.end(), [it] (const DUContext::Import& import) { return import.topContextIndex() == it->indexedTopContext().index(); })) { continue; } auto declaration = it->declaration(); if (!declaration) { // Mitigate problems such as: Cannot load a top-context from file "/home/kfunk/.cache/kdevduchain/kdevelop-{foo}/topcontexts/6085" // - the required language-support for handling ID 55 is probably not loaded qCWarning(KDEV_CLANG) << "Detected an invalid declaration for" << qid; continue; } if (declaration->kind() == Declaration::Instance && !declaration->isFunctionDeclaration()) { break; } if (!handled.contains(declaration)) { handled.insert(declaration); return declaration; } } const auto foundDeclarations = ctx->findDeclarations(qid, position); for (auto dec : foundDeclarations) { if (!handled.contains(dec)) { handled.insert(dec); return dec; } } return nullptr; } /// If any parent of this context is a class, the closest class declaration is returned, nullptr otherwise Declaration* classDeclarationForContext(const DUContextPointer& context, const CursorInRevision& position) { auto parent = context; while (parent) { if (parent->type() == DUContext::Class) { break; } if (auto owner = parent->owner()) { // Work-around for out-of-line methods. They have Helper context instead of Class context if (owner->context() && owner->context()->type() == DUContext::Helper) { auto qid = owner->qualifiedIdentifier(); qid.pop(); QSet tmp; auto decl = findDeclaration(qid, context, position, tmp); if (decl && decl->internalContext() && decl->internalContext()->type() == DUContext::Class) { parent = decl->internalContext(); break; } } } parent = parent->parentContext(); } return parent ? parent->owner() : nullptr; } class LookAheadItemMatcher { public: explicit LookAheadItemMatcher(const TopDUContextPointer& ctx) : m_topContext(ctx) , m_enabled(ClangSettingsManager::self()->codeCompletionSettings().lookAhead) {} /// Adds all local declarations for @p declaration into possible look-ahead items. void addDeclarations(Declaration* declaration) { if (!m_enabled) { return; } if (declaration->kind() != Declaration::Instance) { return; } auto type = typeForDeclaration(declaration); auto identifiedType = dynamic_cast(type.data()); if (!identifiedType) { return; } addDeclarationsForType(identifiedType, declaration); } /// Add type for matching. This type'll be used for filtering look-ahead items /// Only items with @p type will be returned through @sa matchedItems void addMatchedType(const IndexedType& type) { if (type.isValid()) { matchedTypes.insert(type); } } /// @return look-ahead items that math given types. @sa addMatchedType QList matchedItems() { QList lookAheadItems; for (const auto& pair: qAsConst(possibleLookAheadDeclarations)) { auto decl = pair.first; if (matchedTypes.contains(decl->indexedType())) { auto parent = pair.second; const QLatin1String access = (parent->abstractType()->whichType() == AbstractType::TypePointer) ? QLatin1String("->") : QLatin1String("."); const QString text = parent->identifier().toString() + access + decl->identifier().toString(); auto item = new DeclarationItem(decl, text, {}, text); item->setMatchQuality(8); lookAheadItems.append(CompletionTreeItemPointer(item)); } } return lookAheadItems; } private: AbstractType::Ptr typeForDeclaration(const Declaration* decl) { return TypeUtils::targetType(decl->abstractType(), m_topContext.data()); } void addDeclarationsForType(const IdentifiedType* identifiedType, Declaration* declaration) { if (auto typeDecl = identifiedType->declaration(m_topContext.data())) { if (dynamic_cast(typeDecl->logicalDeclaration(m_topContext.data()))) { if (!typeDecl->internalContext()) { return; } const auto& localDeclarations = typeDecl->internalContext()->localDeclarations(); for (auto localDecl : localDeclarations) { if(localDecl->identifier().isEmpty()){ continue; } if(auto classMember = dynamic_cast(localDecl)){ // TODO: Also add protected/private members if completion is inside this class context. if(classMember->accessPolicy() != Declaration::Public){ continue; } } if (!localDecl->abstractType()) { continue; } if (localDecl->abstractType()->whichType() == AbstractType::TypeIntegral) { if (auto integralType = declaration->abstractType().cast()) { if (integralType->dataType() == IntegralType::TypeVoid) { continue; } } } possibleLookAheadDeclarations.insert({localDecl, declaration}); } } } } // Declaration and it's context using DeclarationContext = QPair; /// Types of declarations that look-ahead completion items can have QSet matchedTypes; // List of declarations that can be added to the Look Ahead group // Second declaration represents context QSet possibleLookAheadDeclarations; TopDUContextPointer m_topContext; bool m_enabled; }; struct MemberAccessReplacer : public QObject { Q_OBJECT public: enum Type { None, DotToArrow, ArrowToDot }; public Q_SLOTS: void replaceCurrentAccess(MemberAccessReplacer::Type type) { if (auto document = ICore::self()->documentController()->activeDocument()) { if (auto textDocument = document->textDocument()) { auto activeView = document->activeTextView(); if (!activeView) { return; } auto cursor = activeView->cursorPosition(); QString oldAccess, newAccess; if (type == ArrowToDot) { oldAccess = QStringLiteral("->"); newAccess = QStringLiteral("."); } else { oldAccess = QStringLiteral("."); newAccess = QStringLiteral("->"); } auto oldRange = KTextEditor::Range(cursor - KTextEditor::Cursor(0, oldAccess.length()), cursor); // This code needed for testReplaceMemberAccess test // Maybe we should do a similar thing for '->' to '.' direction, but this is not so important while (textDocument->text(oldRange) == QLatin1String(" ") && oldRange.start().column() >= 0) { oldRange = KTextEditor::Range({oldRange.start().line(), oldRange.start().column() - 1}, {oldRange.end().line(), oldRange.end().column() - 1}); } if (oldRange.start().column() >= 0 && textDocument->text(oldRange) == oldAccess) { textDocument->replaceText(oldRange, newAccess); } } } } }; static MemberAccessReplacer s_memberAccessReplacer; } Q_DECLARE_METATYPE(MemberAccessReplacer::Type) ClangCodeCompletionContext::ClangCodeCompletionContext(const DUContextPointer& context, const ParseSessionData::Ptr& sessionData, const QUrl& url, const KTextEditor::Cursor& position, const QString& text, const QString& followingText ) : CodeCompletionContext(context, text + followingText, CursorInRevision::castFromSimpleCursor(position), 0) , m_results(nullptr, clang_disposeCodeCompleteResults) , m_parseSessionData(sessionData) { qRegisterMetaType(); const QByteArray file = url.toLocalFile().toUtf8(); ParseSession session(m_parseSessionData); QVector otherUnsavedFiles; { ForegroundLock lock; otherUnsavedFiles = ClangUtils::unsavedFiles(); } QVector allUnsaved; { const unsigned int completeOptions = clang_defaultCodeCompleteOptions(); CXUnsavedFile unsaved; unsaved.Filename = file.constData(); const QByteArray content = m_text.toUtf8(); unsaved.Contents = content.constData(); unsaved.Length = content.size(); allUnsaved.reserve(otherUnsavedFiles.size() + 1); for (const auto& f : qAsConst(otherUnsavedFiles)) { allUnsaved.append(f.toClangApi()); } allUnsaved.append(unsaved); m_results.reset(clang_codeCompleteAt(session.unit(), file.constData(), position.line() + 1, position.column() + 1, allUnsaved.data(), allUnsaved.size(), completeOptions)); if (!m_results) { qCWarning(KDEV_CLANG) << "Something went wrong during 'clang_codeCompleteAt' for file" << file; return; } auto numDiagnostics = clang_codeCompleteGetNumDiagnostics(m_results.get()); for (uint i = 0; i < numDiagnostics; i++) { auto diagnostic = clang_codeCompleteGetDiagnostic(m_results.get(), i); auto diagnosticType = ClangDiagnosticEvaluator::diagnosticType(diagnostic); clang_disposeDiagnostic(diagnostic); if (diagnosticType == ClangDiagnosticEvaluator::ReplaceWithArrowProblem || diagnosticType == ClangDiagnosticEvaluator::ReplaceWithDotProblem) { MemberAccessReplacer::Type replacementType; if (diagnosticType == ClangDiagnosticEvaluator::ReplaceWithDotProblem) { replacementType = MemberAccessReplacer::ArrowToDot; } else { replacementType = MemberAccessReplacer::DotToArrow; } QMetaObject::invokeMethod(&s_memberAccessReplacer, "replaceCurrentAccess", Qt::QueuedConnection, Q_ARG(MemberAccessReplacer::Type, replacementType)); m_valid = false; return; } } auto addMacros = ClangSettingsManager::self()->codeCompletionSettings().macros; if (!addMacros) { m_filters |= NoMacros; } } if (!m_results->NumResults) { const auto trimmedText = text.trimmed(); if (trimmedText.endsWith(QLatin1Char('.'))) { // TODO: This shouldn't be needed if Clang provided diagnostic. // But it doesn't always do it, so let's try to manually determine whether '.' is used instead of '->' m_text = trimmedText.leftRef(trimmedText.size() - 1) + QLatin1String("->"); CXUnsavedFile unsaved; unsaved.Filename = file.constData(); const QByteArray content = m_text.toUtf8(); unsaved.Contents = content.constData(); unsaved.Length = content.size(); allUnsaved[allUnsaved.size() - 1] = unsaved; m_results.reset(clang_codeCompleteAt(session.unit(), file.constData(), position.line() + 1, position.column() + 1 + 1, allUnsaved.data(), allUnsaved.size(), clang_defaultCodeCompleteOptions())); if (m_results && m_results->NumResults) { QMetaObject::invokeMethod(&s_memberAccessReplacer, "replaceCurrentAccess", Qt::QueuedConnection, Q_ARG(MemberAccessReplacer::Type, MemberAccessReplacer::DotToArrow)); } m_valid = false; return; } } // check 'isValidPosition' after parsing the new content auto clangFile = session.file(file); if (!isValidPosition(session.unit(), clangFile)) { m_valid = false; return; } m_completionHelper.computeCompletions(session, clangFile, position); } ClangCodeCompletionContext::~ClangCodeCompletionContext() { } bool ClangCodeCompletionContext::isValidPosition(CXTranslationUnit unit, CXFile file) const { if (isInsideComment(unit, file, m_position.castToSimpleCursor())) { clangDebug() << "Invalid completion context: Inside comment"; return false; } return true; } QList ClangCodeCompletionContext::completionItems(bool& abort, bool /*fullCompletion*/) { if (!m_valid || !m_duContext || !m_results) { return {}; } const auto ctx = DUContextPointer(m_duContext->findContextAt(m_position)); /// Normal completion items, such as 'void Foo::foo()' QList items; /// Stuff like 'Foo& Foo::operator=(const Foo&)', etc. Not regularly used by our users. QList specialItems; /// Macros from the current context QList macros; /// Builtins reported by Clang QList builtin; // two sets of handled declarations to prevent duplicates and make sure we show // all available overloads QSet handled; // this is only used for the CXCursor_OverloadCandidate completion items QSet overloadsHandled; LookAheadItemMatcher lookAheadMatcher(TopDUContextPointer(ctx->topContext())); // If ctx is/inside the Class context, this represents that context. const auto currentClassContext = classDeclarationForContext(ctx, m_position); // HACK: try to build a fallback parent ID from the USR // otherwise we won't identify typedefed anon structs correctly :( auto parentFromUSR = [this]() -> QString { const auto containerUSR = ClangString(clang_codeCompleteGetContainerUSR(m_results.get())).toString(); const auto lastAt = containerUSR.lastIndexOf(QLatin1Char('@')); if (lastAt <= 0 || containerUSR[lastAt - 1] != QLatin1Char('A')) // we use this hack only for _A_non stuff return {}; return containerUSR.mid(lastAt + 1); }; const auto fallbackParentFromUSR = parentFromUSR(); clangDebug() << "Clang found" << m_results->NumResults << "completion results"; for (uint i = 0; i < m_results->NumResults; ++i) { if (abort) { return {}; } auto result = m_results->Results[i]; #if CINDEX_VERSION_MINOR >= 30 const bool isOverloadCandidate = result.CursorKind == CXCursor_OverloadCandidate; #else const bool isOverloadCandidate = false; #endif const auto availability = clang_getCompletionAvailability(result.CompletionString); if (availability == CXAvailability_NotAvailable) { continue; } const bool isMacroDefinition = result.CursorKind == CXCursor_MacroDefinition; if (isMacroDefinition && m_filters & NoMacros) { continue; } const bool isBuiltin = (result.CursorKind == CXCursor_NotImplemented); if (isBuiltin && m_filters & NoBuiltins) { continue; } const bool isDeclaration = !isMacroDefinition && !isBuiltin; if (isDeclaration && m_filters & NoDeclarations) { continue; } if (availability == CXAvailability_NotAccessible && (!isDeclaration || !currentClassContext)) { continue; } // the string that would be needed to type, usually the identifier of something. Also we use it as name for code completion declaration items. QString typed; // the return type of a function e.g. QString resultType; // the replacement text when an item gets executed QString replacement; QString arguments; ArgumentHintItem::CurrentArgumentRange argumentRange; //BEGIN function signature parsing // nesting depth of parentheses int parenDepth = 0; enum FunctionSignatureState { // not yet inside the function signature Before, // any token is part of the function signature now Inside, // finished parsing the function signature After }; // current state FunctionSignatureState signatureState = Before; //END function signature parsing std::function processChunks = [&] (CXCompletionString completionString) { const uint chunks = clang_getNumCompletionChunks(completionString); for (uint j = 0; j < chunks; ++j) { const auto kind = clang_getCompletionChunkKind(completionString, j); if (kind == CXCompletionChunk_Optional) { completionString = clang_getCompletionChunkCompletionString(completionString, j); if (completionString) { processChunks(completionString); } continue; } // We don't need function signature for declaration items, we can get it directly from the declaration. Also adding the function signature to the "display" would break the "Detailed completion" option. if (isDeclaration && !typed.isEmpty()) { // TODO: When parent context for CXCursor_OverloadCandidate is fixed remove this check if (!isOverloadCandidate) { break; } } const QString string = ClangString(clang_getCompletionChunkText(completionString, j)).toString(); switch (kind) { case CXCompletionChunk_TypedText: typed = string; replacement += string; break; case CXCompletionChunk_ResultType: resultType = string; continue; case CXCompletionChunk_Placeholder: if (signatureState == Inside) { arguments += string; } continue; case CXCompletionChunk_LeftParen: if (signatureState == Before && !parenDepth) { signatureState = Inside; } parenDepth++; break; case CXCompletionChunk_RightParen: --parenDepth; if (signatureState == Inside && !parenDepth) { arguments += QLatin1Char(')'); signatureState = After; } break; case CXCompletionChunk_Text: if (isOverloadCandidate) { typed += string; } else if (result.CursorKind == CXCursor_EnumConstantDecl) { replacement += string; } else if (result.CursorKind == CXCursor_EnumConstantDecl) { replacement += string; } break; case CXCompletionChunk_CurrentParameter: argumentRange.start = arguments.size(); argumentRange.end = string.size(); break; default: break; } if (signatureState == Inside) { arguments += string; } } }; processChunks(result.CompletionString); // we have our own implementation of an override helper // TODO: use the clang-provided one, if available if (typed.endsWith(QLatin1String(" override"))) continue; // TODO: No closing paren if default parameters present if (isOverloadCandidate && !arguments.endsWith(QLatin1Char(')'))) { arguments += QLatin1Char(')'); } // ellide text to the right for overly long result types (templates especially) elideStringRight(resultType, MAX_RETURN_TYPE_STRING_LENGTH); static const auto noIcon = QIcon(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kdevelop/pics/namespace.png"))); if (isDeclaration) { const Identifier id(typed); QualifiedIdentifier qid; auto parent = ClangString(clang_getCompletionParent(result.CompletionString, nullptr)).toString(); if (parent.isEmpty() && !fallbackParentFromUSR.isEmpty()) { parent = fallbackParentFromUSR; } if (!parent.isEmpty()) { qid = QualifiedIdentifier(parent); } qid.push(id); if (!isValidCompletionIdentifier(qid)) { continue; } if (isOverloadCandidate && resultType.isEmpty() && parent.isEmpty()) { // workaround: find constructor calls for non-namespaced classes // TODO: return the namespaced class as parent in libclang qid.push(id); } auto found = findDeclaration(qid, ctx, m_position, isOverloadCandidate ? overloadsHandled : handled); CompletionTreeItemPointer item; if (found) { // TODO: Bug in Clang: protected members from base classes not accessible in derived classes. if (availability == CXAvailability_NotAccessible) { if (auto cl = dynamic_cast(found)) { if (cl->accessPolicy() != Declaration::Protected) { continue; } auto declarationClassContext = classDeclarationForContext(DUContextPointer(found->context()), m_position); uint steps = 10; auto inheriters = DUChainUtils::inheriters(declarationClassContext, steps); if(!inheriters.contains(currentClassContext)){ continue; } } else { continue; } } DeclarationItem* declarationItem = nullptr; if (isOverloadCandidate) { declarationItem = new ArgumentHintItem(found, resultType, typed, arguments, argumentRange); declarationItem->setArgumentHintDepth(1); } else { declarationItem = new DeclarationItem(found, typed, resultType, replacement); } const unsigned int completionPriority = adjustPriorityForDeclaration(found, clang_getCompletionPriority(result.CompletionString)); const bool bestMatch = completionPriority <= CCP_SuperCompletion; //don't set best match property for internal identifiers, also prefer declarations from current file const auto isInternal = found->indexedIdentifier().identifier().toString().startsWith(QLatin1String("__")); if (bestMatch && !isInternal) { const int matchQuality = codeCompletionPriorityToMatchQuality(completionPriority); declarationItem->setMatchQuality(matchQuality); // TODO: LibClang missing API to determine expected code completion type. if (auto functionType = found->type()) { lookAheadMatcher.addMatchedType(IndexedType(functionType->returnType())); } lookAheadMatcher.addMatchedType(found->indexedType()); } else { declarationItem->setInheritanceDepth(completionPriority); lookAheadMatcher.addDeclarations(found); } if ( isInternal ) { declarationItem->markAsUnimportant(); } item = declarationItem; } else { if (isOverloadCandidate) { // TODO: No parent context for CXCursor_OverloadCandidate items, hence qid is broken -> no declaration found auto ahi = new ArgumentHintItem({}, resultType, typed, arguments, argumentRange); ahi->setArgumentHintDepth(1); item = ahi; } else { // still, let's trust that Clang found something useful and put it into the completion result list clangDebug() << "Could not find declaration for" << qid; auto instance = new SimpleItem(typed + arguments, resultType, replacement, noIcon); instance->markAsUnimportant(); item = CompletionTreeItemPointer(instance); } } if (isValidSpecialCompletionIdentifier(qid)) { // If it's a special completion identifier e.g. "operator=(const&)" and we don't have a declaration for it, don't add it into completion list, as this item is completely useless and pollutes the test case. // This happens e.g. for "class A{}; a.|". At | we have "operator=(const A&)" as a special completion identifier without a declaration. if(item->declaration()){ specialItems.append(item); } } else { items.append(item); } continue; } if (result.CursorKind == CXCursor_MacroDefinition) { // TODO: grouping of macros and built-in stuff const auto text = QString(typed + arguments); auto instance = new SimpleItem(text, resultType, replacement, noIcon); auto item = CompletionTreeItemPointer(instance); if ( text.startsWith(QLatin1Char('_')) ) { instance->markAsUnimportant(); } macros.append(item); } else if (result.CursorKind == CXCursor_NotImplemented) { auto instance = new SimpleItem(typed, resultType, replacement, noIcon); auto item = CompletionTreeItemPointer(instance); builtin.append(item); } } if (abort) { return {}; } addImplementationHelperItems(); addOverwritableItems(); eventuallyAddGroup(i18n("Special"), 700, specialItems); eventuallyAddGroup(i18n("Look-ahead Matches"), 800, lookAheadMatcher.matchedItems()); eventuallyAddGroup(i18n("Builtin"), 900, builtin); eventuallyAddGroup(i18n("Macros"), 1000, macros); return items; } void ClangCodeCompletionContext::eventuallyAddGroup(const QString& name, int priority, const QList& items) { if (items.isEmpty()) { return; } auto* node = new CompletionCustomGroupNode(name, priority); node->appendChildren(items); m_ungrouped << CompletionTreeElementPointer(node); } void ClangCodeCompletionContext::addOverwritableItems() { - auto overrideList = m_completionHelper.overrides(); + const auto overrideList = m_completionHelper.overrides(); if (overrideList.isEmpty()) { return; } QList overrides; QList overridesAbstract; for (const auto& info : overrideList) { QStringList params; params.reserve(info.params.size()); for (const auto& param : info.params) { params << param.type + QLatin1Char(' ') + param.id; } QString nameAndParams = info.name + QLatin1Char('(') + params.join(QLatin1String(", ")) + QLatin1Char(')'); if(info.isConst) nameAndParams = nameAndParams + QLatin1String(" const"); if(info.isPureVirtual) nameAndParams = nameAndParams + QLatin1String(" = 0"); auto item = CompletionTreeItemPointer(new OverrideItem(nameAndParams, info.returnType)); if (info.isPureVirtual) overridesAbstract << item; else overrides << item; } eventuallyAddGroup(i18n("Abstract Override"), 0, overridesAbstract); eventuallyAddGroup(i18n("Virtual Override"), 0, overrides); } void ClangCodeCompletionContext::addImplementationHelperItems() { const auto implementsList = m_completionHelper.implements(); if (implementsList.isEmpty()) { return; } QList implements; implements.reserve(implementsList.size()); for (const auto& info : implementsList) { implements << CompletionTreeItemPointer(new ImplementsItem(info)); } eventuallyAddGroup(i18n("Implement Function"), 0, implements); } QList ClangCodeCompletionContext::ungroupedElements() { return m_ungrouped; } ClangCodeCompletionContext::ContextFilters ClangCodeCompletionContext::filters() const { return m_filters; } void ClangCodeCompletionContext::setFilters(const ClangCodeCompletionContext::ContextFilters& filters) { m_filters = filters; } #include "context.moc" diff --git a/plugins/clang/duchain/documentfinderhelpers.cpp b/plugins/clang/duchain/documentfinderhelpers.cpp index 34e58f3faf..dbb50ab0ce 100644 --- a/plugins/clang/duchain/documentfinderhelpers.cpp +++ b/plugins/clang/duchain/documentfinderhelpers.cpp @@ -1,284 +1,284 @@ /* * This file is part of KDevelop * * Copyright 2014 Sergey Kalinichev * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * */ #include "documentfinderhelpers.h" #include "duchain/clanghelpers.h" #include #include #include #include #include #include using namespace KDevelop; namespace { enum FileType { Unknown, ///< Doesn't belong to C++ Header, ///< Is a header file Source ///< Is a C(++) file }; class PotentialBuddyCollector : public DUChainUtils::DUChainItemFilter { public: enum BuddyMode { Header, Source }; explicit PotentialBuddyCollector(BuddyMode mode) : mode(mode) {} bool accept(Declaration* decl) override { if (decl->range().isEmpty()) return false; if (mode == Header && decl->isFunctionDeclaration()) { // Search for definitions of our declarations FunctionDefinition* def = FunctionDefinition::definition(decl); if (def) { vote(def->url().toUrl()); } return true; } else if (mode == Source && decl->isFunctionDeclaration()) { auto* fdef = dynamic_cast(decl); if (fdef) { Declaration* fdecl = fdef->declaration(); if (fdecl) { vote(fdecl->url().toUrl()); } } return true; } else { return false; } } bool accept(DUContext* ctx) override { if (ctx->type() == DUContext::Class || ctx->type() == DUContext::Namespace || ctx->type() == DUContext::Global || ctx->type() == DUContext::Other || ctx->type() == DUContext::Helper ) { return true; } else { return false; } } QUrl bestBuddy() const { QUrl ret; int bestCount = 0; for (auto it = m_buddyFiles.begin(); it != m_buddyFiles.end(); ++it) { if(it.value() > bestCount) { bestCount = it.value(); ret = it.key(); } } return ret; } private: BuddyMode mode; QHash m_buddyFiles; void vote(const QUrl& url) { m_buddyFiles[url]++; } }; /** * Tries to find a buddy file to the given file by looking at the DUChain. * * The project might keep source files separate from headers. To cover * this situation, we examine DUChain for the most probable buddy file. * This of course only works if we have parsed the buddy file, but it is * better than nothing. * * @param url url of the source/header file to find a buddy for * @param type type of the file @p url * * @returns QUrl of the most probable buddy file, or an empty url **/ QUrl duchainBuddyFile(const QUrl& url, FileType type) { DUChainReadLocker lock; auto ctx = DUChainUtils::standardContextForUrl(url); if (ctx) { PotentialBuddyCollector collector( type == Header ? PotentialBuddyCollector::Header : PotentialBuddyCollector::Source ); DUChainUtils::collectItems(ctx, collector); return collector.bestBuddy(); } return QUrl(); } /** * Generates the base path (without extension) and the file type * for the specified url. * * @returns pair of base path and file type which has been found for @p url. */ QPair basePathAndTypeForUrl(const QUrl &url) { QString path = url.toLocalFile(); int idxSlash = path.lastIndexOf(QLatin1Char('/')); int idxDot = path.lastIndexOf(QLatin1Char('.')); FileType fileType = Unknown; QString basePath; if (idxSlash >= 0 && idxDot >= 0 && idxDot > idxSlash) { basePath = path.left(idxDot); if (idxDot + 1 < path.length()) { QString extension = path.mid(idxDot + 1); if (ClangHelpers::isHeader(extension)) { fileType = Header; } else if (ClangHelpers::isSource(extension)) { fileType = Source; } } } else { basePath = path; } return qMakePair(basePath, fileType); } } namespace DocumentFinderHelpers { QStringList mimeTypesList() { static const QStringList mimeTypes = { QStringLiteral("text/x-chdr"), QStringLiteral("text/x-c++hdr"), QStringLiteral("text/vnd.nvidia.cuda.chdr"), QStringLiteral("text/x-csrc"), QStringLiteral("text/x-c++src"), QStringLiteral("text/vnd.nvidia.cuda.csrc"), QStringLiteral("text/x-objcsrc") }; return mimeTypes; } bool areBuddies(const QUrl &url1, const QUrl& url2) { auto type1 = basePathAndTypeForUrl(url1); auto type2 = basePathAndTypeForUrl(url2); QUrl headerPath; QUrl sourcePath; // Check that one file is a header, the other one is source if (type1.second == Header && type2.second == Source) { headerPath = url1; sourcePath = url2; } else if (type1.second == Source && type2.second == Header) { headerPath = url2; sourcePath = url1; } else { // Some other file constellation return false; } // The simplest directory layout is with header + source in one directory. // So check that first. if (type1.first == type2.first) { return true; } // Also check if the DUChain thinks this is likely if (duchainBuddyFile(sourcePath, Source) == headerPath) { return true; } return false; } bool buddyOrder(const QUrl &url1, const QUrl& url2) { auto type1 = basePathAndTypeForUrl(url1); auto type2 = basePathAndTypeForUrl(url2); // Precondition is that the two URLs are buddies, so don't check it return(type1.second == Header && type2.second == Source); } QVector potentialBuddies(const QUrl& url, bool checkDUChain) { auto type = basePathAndTypeForUrl(url); // Don't do anything for types we don't know if (type.second == Unknown) { return {}; } // Depending on the buddy's file type we either generate source extensions (for headers) // or header extensions (for sources) const auto& extensions = ( type.second == Header ? ClangHelpers::sourceExtensions() : ClangHelpers::headerExtensions() ); QVector< QUrl > buddies; buddies.reserve(extensions.size()); for(const QString& extension : extensions) { if (!extension.contains(QLatin1Char('.'))) { buddies.append(QUrl::fromLocalFile(type.first + QLatin1Char('.') + extension)); } else { buddies.append(QUrl::fromLocalFile(type.first + extension)); } } if (checkDUChain) { // Also ask DUChain for a guess QUrl bestBuddy = duchainBuddyFile(url, type.second); if (!buddies.contains(bestBuddy)) { buddies.append(bestBuddy); } } return buddies; } QString sourceForHeader(const QString& headerPath) { if (!ClangHelpers::isHeader(headerPath)) { return {}; } QString targetUrl; - auto buddies = DocumentFinderHelpers::potentialBuddies(QUrl::fromLocalFile(headerPath)); + const auto buddies = DocumentFinderHelpers::potentialBuddies(QUrl::fromLocalFile(headerPath)); for (const auto& buddy : buddies) { const auto local = buddy.toLocalFile(); if (QFileInfo::exists(local)) { targetUrl = local; break; } } return targetUrl; } } diff --git a/plugins/clang/duchain/parsesession.cpp b/plugins/clang/duchain/parsesession.cpp index b75d47460c..bb7872012f 100644 --- a/plugins/clang/duchain/parsesession.cpp +++ b/plugins/clang/duchain/parsesession.cpp @@ -1,622 +1,622 @@ /* This file is part of KDevelop Copyright 2013 Olivier de Gaalon Copyright 2013 Milian Wolff Copyright 2013 Kevin Funk This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "parsesession.h" #include #include "clangdiagnosticevaluator.h" #include "todoextractor.h" #include "clanghelpers.h" #include "clangindex.h" #include "clangparsingenvironment.h" #include "util/clangdebug.h" #include "util/clangtypes.h" #include "util/clangutils.h" #include "headerguardassistant.h" #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace { QVector extraArgs() { const auto extraArgsString = QString::fromLatin1(qgetenv("KDEV_CLANG_EXTRA_ARGUMENTS")); const auto extraArgs = KShell::splitArgs(extraArgsString); // transform to list of QByteArrays QVector result; result.reserve(extraArgs.size()); for (const QString& arg : extraArgs) { result << arg.toLatin1(); } clangDebug() << "Passing extra arguments to clang:" << result; return result; } void sanitizeArguments(QVector& arguments) { // We remove the -Werror flag, and replace -Werror=foo by -Wfoo. // Warning as error may cause problem to the clang parser. const auto asError = QByteArrayLiteral("-Werror="); const auto documentation = QByteArrayLiteral("-Wdocumentation"); for (auto& argument : arguments) { if (argument == "-Werror") { argument.clear(); } else if (argument.startsWith(asError)) { // replace -Werror=foo by -Wfoo argument.remove(2, asError.length() - 2); } #if CINDEX_VERSION_MINOR < 100 // FIXME https://bugs.llvm.org/show_bug.cgi?id=35333 if (argument == documentation) { argument.clear(); } #endif } } QVector argsForSession(const QString& path, ParseSessionData::Options options, const ParserSettings& parserSettings) { QMimeDatabase db; if (db.mimeTypeForFile(path).name() == QLatin1String("text/x-objcsrc")) { return {QByteArrayLiteral("-xobjective-c++")}; } // TODO: No proper mime type detection possible yet // cf. https://bugs.freedesktop.org/show_bug.cgi?id=26913 if (path.endsWith(QLatin1String(".cl"), Qt::CaseInsensitive)) { return {QByteArrayLiteral("-xcl")}; } // TODO: No proper mime type detection possible yet // cf. https://bugs.freedesktop.org/show_bug.cgi?id=23700 if (path.endsWith(QLatin1String(".cu"), Qt::CaseInsensitive) || path.endsWith(QLatin1String(".cuh"), Qt::CaseInsensitive)) { auto result = parserSettings.toClangAPI(); result.append(QByteArrayLiteral("-xcuda")); return result; } if (parserSettings.parserOptions.isEmpty()) { // The parserOptions can be empty for some unit tests that use ParseSession directly auto defaultArguments = ClangSettingsManager::self()->parserSettings(path).toClangAPI(); defaultArguments.append(QByteArrayLiteral("-nostdinc")); defaultArguments.append(QByteArrayLiteral("-nostdinc++")); defaultArguments.append(QByteArrayLiteral("-xc++")); sanitizeArguments(defaultArguments); return defaultArguments; } auto result = parserSettings.toClangAPI(); result.append(QByteArrayLiteral("-nostdinc")); if (parserSettings.isCpp()) { result.append(QByteArrayLiteral("-nostdinc++")); } if (options & ParseSessionData::PrecompiledHeader) { result.append(parserSettings.isCpp() ? QByteArrayLiteral("-xc++-header") : QByteArrayLiteral("-xc-header")); sanitizeArguments(result); return result; } result.append(parserSettings.isCpp() ? QByteArrayLiteral("-xc++") : QByteArrayLiteral("-xc")); sanitizeArguments(result); return result; } void addIncludes(QVector* args, QVector* otherArgs, const Path::List& includes, const char* cliSwitch) { for (const Path& url : includes) { if (url.isEmpty()) { continue; } QFileInfo info(url.toLocalFile()); QByteArray path = url.toLocalFile().toUtf8(); if (info.isFile()) { path.prepend("-include"); } else { path.prepend(cliSwitch); } otherArgs->append(path); args->append(path.constData()); } } void addFrameworkDirectories(QVector* args, QVector* otherArgs, const Path::List& frameworkDirectories, const char* cliSwitch) { for (const Path& url : frameworkDirectories) { if (url.isEmpty()) { continue; } QFileInfo info(url.toLocalFile()); if (!info.isDir()) { qCWarning(KDEV_CLANG) << "supposed framework directory is not a directory:" << url.pathOrUrl(); continue; } QByteArray path = url.toLocalFile().toUtf8(); otherArgs->append(cliSwitch); otherArgs->append(path); args->append(cliSwitch); args->append(path.constData()); } } QVector toClangApi(const QVector& unsavedFiles) { QVector unsaved; unsaved.reserve(unsavedFiles.size()); std::transform(unsavedFiles.begin(), unsavedFiles.end(), std::back_inserter(unsaved), [] (const UnsavedFile& file) { return file.toClangApi(); }); return unsaved; } bool hasQtIncludes(const Path::List& includePaths) { return std::find_if(includePaths.begin(), includePaths.end(), [] (const Path& path) { return path.lastPathSegment() == QLatin1String("QtCore"); }) != includePaths.end(); } } ParseSessionData::ParseSessionData(const QVector& unsavedFiles, ClangIndex* index, const ClangParsingEnvironment& environment, Options options) : m_file(nullptr) , m_unit(nullptr) { unsigned int flags = CXTranslationUnit_DetailedPreprocessingRecord #if CINDEX_VERSION_MINOR >= 34 | CXTranslationUnit_KeepGoing #endif ; if (options.testFlag(SkipFunctionBodies)) { flags |= CXTranslationUnit_SkipFunctionBodies; } if (options.testFlag(PrecompiledHeader)) { flags |= CXTranslationUnit_ForSerialization; } else { flags |= CXTranslationUnit_CacheCompletionResults #if CINDEX_VERSION_MINOR >= 32 | CXTranslationUnit_CreatePreambleOnFirstParse #endif | CXTranslationUnit_PrecompiledPreamble; if (environment.quality() == ClangParsingEnvironment::Unknown) { flags |= CXTranslationUnit_Incomplete; } } const auto tuUrl = environment.translationUnitUrl(); Q_ASSERT(!tuUrl.isEmpty()); const auto arguments = argsForSession(tuUrl.str(), options, environment.parserSettings()); QVector clangArguments; const auto& includes = environment.includes(); const auto& pchInclude = environment.pchInclude(); // uses QByteArray as smart-pointer for const char* ownership QVector smartArgs; smartArgs.reserve(includes.system.size() + includes.project.size() + pchInclude.isValid() + arguments.size() + 1); clangArguments.reserve(smartArgs.size()); std::transform(arguments.constBegin(), arguments.constEnd(), std::back_inserter(clangArguments), [] (const QByteArray &argument) { return argument.constData(); }); // NOTE: the PCH include must come before all other includes! if (pchInclude.isValid()) { clangArguments << "-include"; QByteArray pchFile = pchInclude.toLocalFile().toUtf8(); smartArgs << pchFile; clangArguments << pchFile.constData(); } if (hasQtIncludes(includes.system)) { const auto wrappedQtHeaders = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kdevclangsupport/wrappedQtHeaders"), QStandardPaths::LocateDirectory).toUtf8(); if (!wrappedQtHeaders.isEmpty()) { smartArgs << wrappedQtHeaders; clangArguments << "-isystem" << wrappedQtHeaders.constData(); const QByteArray qtCore = wrappedQtHeaders + "/QtCore"; smartArgs << qtCore; clangArguments << "-isystem" << qtCore.constData(); } } addIncludes(&clangArguments, &smartArgs, includes.system, "-isystem"); addIncludes(&clangArguments, &smartArgs, includes.project, "-I"); const auto& frameworkDirectories = environment.frameworkDirectories(); addFrameworkDirectories(&clangArguments, &smartArgs, frameworkDirectories.system, "-iframework"); addFrameworkDirectories(&clangArguments, &smartArgs, frameworkDirectories.project, "-F"); // libclang cannot find it's builtin dir automatically, we have to specify it manually smartArgs << ClangHelpers::clangBuiltinIncludePath().toUtf8(); clangArguments << "-isystem" << smartArgs.last().constData(); if (!environment.defines().isEmpty()) { smartArgs << writeDefinesFile(environment.defines()); clangArguments << "-imacros" << smartArgs.last().constData(); } if (!environment.workingDirectory().isEmpty()) { QByteArray workingDirectory = environment.workingDirectory().toLocalFile().toUtf8(); workingDirectory.prepend("-working-directory"); smartArgs << workingDirectory; clangArguments << workingDirectory.constData(); } // append extra args from environment variable static const auto extraArgs = ::extraArgs(); for (const QByteArray& arg : extraArgs) { clangArguments << arg.constData(); } QVector unsaved; //For PrecompiledHeader, we don't want unsaved contents (and contents.isEmpty()) if (!options.testFlag(PrecompiledHeader)) { unsaved = toClangApi(unsavedFiles); } // debugging: print hypothetical clang invocation including args (for easy c&p for local testing) if (qEnvironmentVariableIsSet("KDEV_CLANG_DISPLAY_ARGS")) { QTextStream out(stdout); out << "Invocation: clang"; for (const auto& arg : qAsConst(clangArguments)) { out << " " << arg; } out << " " << tuUrl.byteArray().constData() << "\n"; } const CXErrorCode code = clang_parseTranslationUnit2( index->index(), tuUrl.byteArray().constData(), clangArguments.constData(), clangArguments.size(), unsaved.data(), unsaved.size(), flags, &m_unit ); if (code != CXError_Success) { qCWarning(KDEV_CLANG) << "clang_parseTranslationUnit2 return with error code" << code; if (!qEnvironmentVariableIsSet("KDEV_CLANG_DISPLAY_DIAGS")) { qCWarning(KDEV_CLANG) << " (start KDevelop with `KDEV_CLANG_DISPLAY_DIAGS=1 kdevelop` to see more diagnostics)"; } } if (m_unit) { setUnit(m_unit); m_environment = environment; if (options.testFlag(PrecompiledHeader)) { clang_saveTranslationUnit(m_unit, QByteArray(tuUrl.byteArray() + ".pch").constData(), CXSaveTranslationUnit_None); } } else { qCWarning(KDEV_CLANG) << "Failed to parse translation unit:" << tuUrl; } } ParseSessionData::~ParseSessionData() { clang_disposeTranslationUnit(m_unit); } QByteArray ParseSessionData::writeDefinesFile(const QMap& defines) { m_definesFile.open(); Q_ASSERT(m_definesFile.isWritable()); { QTextStream definesStream(&m_definesFile); // don't show warnings about redefined macros definesStream << "#pragma clang system_header\n"; for (auto it = defines.begin(); it != defines.end(); ++it) { if (it.key().startsWith(QLatin1String("__has_include(")) || it.key().startsWith(QLatin1String("__has_include_next("))) { continue; } definesStream << QLatin1String("#define ") << it.key() << ' ' << it.value() << '\n'; } } m_definesFile.close(); if (qEnvironmentVariableIsSet("KDEV_CLANG_DISPLAY_DEFINES")) { QFile f(m_definesFile.fileName()); f.open(QIODevice::ReadOnly); Q_ASSERT(f.isReadable()); QTextStream out(stdout); out << "Defines file: " << f.fileName() << "\n" << f.readAll() << f.size() << "\n VS defines:" << defines.size() << "\n"; } return m_definesFile.fileName().toUtf8(); } void ParseSessionData::setUnit(CXTranslationUnit unit) { m_unit = unit; m_diagnosticsCache.clear(); if (m_unit) { const ClangString unitFile(clang_getTranslationUnitSpelling(unit)); m_file = clang_getFile(m_unit, unitFile.c_str()); } else { m_file = nullptr; } } ClangParsingEnvironment ParseSessionData::environment() const { return m_environment; } ParseSession::ParseSession(const ParseSessionData::Ptr& data) : d(data) { if (d) { ENSURE_CHAIN_NOT_LOCKED d->m_mutex.lock(); } } ParseSession::~ParseSession() { if (d) { d->m_mutex.unlock(); } } void ParseSession::setData(const ParseSessionData::Ptr& data) { if (data == d) { return; } if (d) { d->m_mutex.unlock(); } d = data; if (d) { ENSURE_CHAIN_NOT_LOCKED d->m_mutex.lock(); } } ParseSessionData::Ptr ParseSession::data() const { return d; } IndexedString ParseSession::languageString() { static const IndexedString lang("Clang"); return lang; } ClangProblem::Ptr ParseSession::getOrCreateProblem(int indexInTU, CXDiagnostic diagnostic) const { auto& problem = d->m_diagnosticsCache[indexInTU]; if (!problem) { problem = ClangDiagnosticEvaluator::createProblem(diagnostic, d->m_unit); } return problem; } ClangProblem::Ptr ParseSession::createExternalProblem(int indexInTU, CXDiagnostic diagnostic, const KLocalizedString& descriptionTemplate, int childProblemFinalLocationIndex) const { // Make a copy of the original (cached) problem since it is modified later auto problem = ClangProblem::Ptr(new ClangProblem(*getOrCreateProblem(indexInTU, diagnostic))); // Insert a copy of the parent problem (without child problems) as the first // child problem to preserve its location. auto* problemCopy = new ClangProblem(); problemCopy->setSource(problem->source()); problemCopy->setFinalLocation(problem->finalLocation()); problemCopy->setFinalLocationMode(problem->finalLocationMode()); problemCopy->setDescription(problem->description()); problemCopy->setExplanation(problem->explanation()); problemCopy->setSeverity(problem->severity()); auto childProblems = problem->diagnostics(); childProblems.prepend(IProblem::Ptr(problemCopy)); problem->setDiagnostics(childProblems); // Override the problem's finalLocation with that of the child problem in this document. // This is required to make the problem show up in the problem reporter for this // file, since it filters by finalLocation. It will also lead the user to the correct // location when clicking the problem and cause proper error highlighting. int index = (childProblemFinalLocationIndex >= 0) ? (1 + childProblemFinalLocationIndex) : (childProblems.size() - 1); problem->setFinalLocation(childProblems[index]->finalLocation()); problem->setDescription(descriptionTemplate.subs(problem->description()).toString()); return problem; } QList ParseSession::createRequestedHereProblems(int indexInTU, CXDiagnostic diagnostic, CXFile file) const { QList results; auto childDiagnostics = clang_getChildDiagnostics(diagnostic); auto numChildDiagnostics = clang_getNumDiagnosticsInSet(childDiagnostics); for (uint j = 0; j < numChildDiagnostics; ++j) { auto childDiagnostic = clang_getDiagnosticInSet(childDiagnostics, j); CXSourceLocation childLocation = clang_getDiagnosticLocation(childDiagnostic); CXFile childDiagnosticFile; clang_getFileLocation(childLocation, &childDiagnosticFile, nullptr, nullptr, nullptr); if (childDiagnosticFile == file) { QString description = ClangString(clang_getDiagnosticSpelling(childDiagnostic)).toString(); if (description.endsWith(QLatin1String("requested here"))) { // Note: Using the index j here assumes a 1:1 mapping from clang child diagnostics to KDevelop // problem diagnostics (i.e., child problems). If we wanted to avoid making this assumption, we'd have // to use ClangDiagnosticEvaluator::createProblem() first and then search within its // child problems to find the correct index. results << createExternalProblem(indexInTU, diagnostic, ki18n("Requested here: %1"), j); } } } return results; } QList ParseSession::problemsForFile(CXFile file) const { if (!d) { return {}; } QList problems; // extra clang diagnostics const uint numDiagnostics = clang_getNumDiagnostics(d->m_unit); problems.reserve(numDiagnostics); d->m_diagnosticsCache.resize(numDiagnostics); for (uint i = 0; i < numDiagnostics; ++i) { auto diagnostic = clang_getDiagnostic(d->m_unit, i); CXSourceLocation location = clang_getDiagnosticLocation(diagnostic); CXFile diagnosticFile; clang_getFileLocation(location, &diagnosticFile, nullptr, nullptr, nullptr); - auto requestedHereProblems = createRequestedHereProblems(i, diagnostic, file); + const auto requestedHereProblems = createRequestedHereProblems(i, diagnostic, file); for (const auto& ptr : requestedHereProblems) { problems.append(static_cast(ptr)); } // missing-include problems are so severe in clang that we always propagate // them to this document, to ensure that the user will see the error. if (diagnosticFile != file && ClangDiagnosticEvaluator::diagnosticType(diagnostic) != ClangDiagnosticEvaluator::IncludeFileNotFoundProblem) { continue; } problems << ((diagnosticFile == file) ? getOrCreateProblem(i, diagnostic) : createExternalProblem(i, diagnostic, ki18n("In included file: %1"))); clang_disposeDiagnostic(diagnostic); } // other problem sources TodoExtractor extractor(unit(), file); problems << extractor.problems(); #if CINDEX_VERSION_MINOR > 30 // note that the below warning is triggered on every reparse when there is a precompiled preamble // see also TestDUChain::testReparseIncludeGuard const QString path = QDir(ClangString(clang_getFileName(file)).toString()).canonicalPath(); const IndexedString indexedPath(path); if (ClangHelpers::isHeader(path) && !clang_isFileMultipleIncludeGuarded(unit(), file) && !clang_Location_isInSystemHeader(clang_getLocationForOffset(d->m_unit, file, 0))) { QExplicitlySharedDataPointer problem(new StaticAssistantProblem); problem->setSeverity(IProblem::Warning); problem->setDescription(i18n("Header is not guarded against multiple inclusions")); problem->setExplanation(i18n("The given header is not guarded against multiple inclusions, " "either with the conventional #ifndef/#define/#endif macro guards or with #pragma once.")); const KTextEditor::Range problemRange(0, 0, KDevelop::createCodeRepresentation(indexedPath)->lines(), 0); problem->setFinalLocation(DocumentRange{indexedPath, problemRange}); problem->setSource(IProblem::Preprocessor); problem->setSolutionAssistant(KDevelop::IAssistant::Ptr(new HeaderGuardAssistant(d->m_unit, file))); problems << problem; } #endif return problems; } CXTranslationUnit ParseSession::unit() const { return d ? d->m_unit : nullptr; } CXFile ParseSession::file(const QByteArray& path) const { return clang_getFile(unit(), path.constData()); } CXFile ParseSession::mainFile() const { return d ? d->m_file : nullptr; } bool ParseSession::reparse(const QVector& unsavedFiles, const ClangParsingEnvironment& environment) { if (!d || environment != d->m_environment) { return false; } auto unsaved = toClangApi(unsavedFiles); const auto code = clang_reparseTranslationUnit(d->m_unit, unsaved.size(), unsaved.data(), clang_defaultReparseOptions(d->m_unit)); if (code != CXError_Success) { qCWarning(KDEV_CLANG) << "clang_reparseTranslationUnit return with error code" << code; // if error code != 0 => clang_reparseTranslationUnit invalidates the old translation unit => clean up clang_disposeTranslationUnit(d->m_unit); d->setUnit(nullptr); return false; } // update state d->setUnit(d->m_unit); return true; } ClangParsingEnvironment ParseSession::environment() const { return d->m_environment; } diff --git a/plugins/clang/tests/test_duchain.cpp b/plugins/clang/tests/test_duchain.cpp index 57866bff69..1614ec753f 100644 --- a/plugins/clang/tests/test_duchain.cpp +++ b/plugins/clang/tests/test_duchain.cpp @@ -1,2228 +1,2228 @@ /* * Copyright 2014 Milian Wolff * Copyright 2014 Kevin Funk * Copyright 2015 Sergey Kalinichev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_duchain.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "duchain/clangparsingenvironmentfile.h" #include "duchain/clangparsingenvironment.h" #include "duchain/parsesession.h" #include "duchain/clanghelpers.h" #include "testprovider.h" #include #include #include #include #include #include QTEST_MAIN(TestDUChain) using namespace KDevelop; TestDUChain::~TestDUChain() = default; void TestDUChain::initTestCase() { QLoggingCategory::setFilterRules(QStringLiteral("*.debug=false\ndefault.debug=true\nkdevelop.plugins.clang.debug=true\n")); QVERIFY(qputenv("KDEV_CLANG_DISPLAY_DIAGS", "1")); AutoTestShell::init({QStringLiteral("kdevclangsupport")}); auto core = TestCore::initialize(); delete core->projectController(); m_projectController = new TestProjectController(core); core->setProjectController(m_projectController); } void TestDUChain::cleanupTestCase() { TestCore::shutdown(); } void TestDUChain::cleanup() { if (m_provider) { IDefinesAndIncludesManager::manager()->unregisterBackgroundProvider(m_provider.data()); } } void TestDUChain::init() { m_provider.reset(new TestEnvironmentProvider); IDefinesAndIncludesManager::manager()->registerBackgroundProvider(m_provider.data()); } struct ExpectedComment { QString identifier; QString comment; }; Q_DECLARE_METATYPE(ExpectedComment) Q_DECLARE_METATYPE(AbstractType::WhichType) void TestDUChain::testComments() { QFETCH(QString, code); QFETCH(ExpectedComment, expectedComment); TestFile file(code, QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; auto top = file.topContext(); QVERIFY(top); auto candidates = top->findDeclarations(QualifiedIdentifier(expectedComment.identifier)); QVERIFY(!candidates.isEmpty()); auto decl = candidates.first(); QString comment = QString::fromLocal8Bit(decl->comment()); const auto plainText = KDevelop::htmlToPlainText(comment, KDevelop::CompleteMode); // if comment is e.g. "("code"); QTest::addColumn("expectedComment"); // note: Clang only retrieves the comments when in doxygen-style format (i.e. '///', '/**', '///<') QTest::newRow("invalid1") << "//this is foo\nint foo;" << ExpectedComment{"foo", QString()}; QTest::newRow("invalid2") << "/*this is foo*/\nint foo;" << ExpectedComment{"foo", QString()}; QTest::newRow("basic1") << "///this is foo\nint foo;" << ExpectedComment{"foo", "this is foo"}; QTest::newRow("basic2") << "/**this is foo*/\nint foo;" << ExpectedComment{"foo", "this is foo"}; // as long as https://bugs.llvm.org/show_bug.cgi?id=35333 is not fixed, we don't fully parse and render // doxygen-style comments properly (cf. `makeComment` in builder.cpp) #define PARSE_COMMENTS 0 QTest::newRow("enumerator") << "enum Foo { bar1, ///localDeclarations().size(), 2); auto decl = file.topContext()->localDeclarations()[1]; QVERIFY(decl); auto function = dynamic_cast(decl); QVERIFY(function); auto functionType = function->type(); QVERIFY(functionType); #if CINDEX_VERSION_MINOR < 34 QEXPECT_FAIL("namespace", "The ElaboratedType is not exposed through the libclang interface, not much we can do here", Abort); #endif QVERIFY(functionType->returnType()->whichType() != AbstractType::TypeDelayed); #if CINDEX_VERSION_MINOR < 34 QEXPECT_FAIL("typedef", "After using clang_getCanonicalType on ElaboratedType all typedef information get's stripped away", Continue); #endif QCOMPARE(functionType->returnType()->whichType(), type); } void TestDUChain::testElaboratedType_data() { QTest::addColumn("code"); QTest::addColumn("type"); QTest::newRow("namespace") << "namespace NS{struct Type{};} struct NS::Type foo();" << AbstractType::TypeStructure; QTest::newRow("enum") << "enum Enum{}; enum Enum foo();" << AbstractType::TypeEnumeration; QTest::newRow("typedef") << "namespace NS{typedef int type;} NS::type foo();" << AbstractType::TypeAlias; } void TestDUChain::testInclude() { TestFile header(QStringLiteral("int foo() { return 42; }\n"), QStringLiteral("h")); // NOTE: header is _not_ explicitly being parsed, instead the impl job does that TestFile impl("#include \"" + header.url().str() + "\"\n" "int main() { return foo(); }", QStringLiteral("cpp"), &header); impl.parse(TopDUContext::AllDeclarationsContextsAndUses); auto implCtx = impl.topContext(); QVERIFY(implCtx); DUChainReadLocker lock; QCOMPARE(implCtx->localDeclarations().size(), 1); auto headerCtx = DUChain::self()->chainForDocument(header.url()); QVERIFY(headerCtx); QVERIFY(!headerCtx->parsingEnvironmentFile()->needsUpdate()); QCOMPARE(headerCtx->localDeclarations().size(), 1); QVERIFY(implCtx->imports(headerCtx, CursorInRevision(0, 10))); Declaration* foo = headerCtx->localDeclarations().first(); QCOMPARE(foo->uses().size(), 1); QCOMPARE(foo->uses().begin().key(), impl.url()); QCOMPARE(foo->uses().begin()->size(), 1); QCOMPARE(foo->uses().begin()->first(), RangeInRevision(1, 20, 1, 23)); } void TestDUChain::testMissingInclude() { auto code = R"( #pragma once #include "missing1.h" template class A { T a; }; #include "missing2.h" class B : public A { }; )"; TestFile header(code, QStringLiteral("h")); TestFile impl("#include \"" + header.url().str() + "\"\n", QStringLiteral("cpp"), &header); QVERIFY(impl.parseAndWait(TopDUContext::AllDeclarationsContextsAndUses)); DUChainReadLocker lock; auto top = impl.topContext(); QVERIFY(top); QCOMPARE(top->importedParentContexts().count(), 1); TopDUContext* headerCtx = dynamic_cast(top->importedParentContexts().first().context(top)); QVERIFY(headerCtx); QCOMPARE(headerCtx->url(), header.url()); #if CINDEX_VERSION_MINOR < 34 QEXPECT_FAIL("", "Second missing header isn't reported", Continue); #endif QCOMPARE(headerCtx->problems().count(), 2); QCOMPARE(headerCtx->localDeclarations().count(), 2); auto a = dynamic_cast(headerCtx->localDeclarations().first()); QVERIFY(a); auto b = dynamic_cast(headerCtx->localDeclarations().last()); QVERIFY(b); // NOTE: This fails and needs fixing. If the include of "missing2.h" // above is commented out, then it doesn't fail. Maybe // clang stops processing when it encounters the second missing // header, or similar. // XFAIL this check until https://bugs.llvm.org/show_bug.cgi?id=38155 is fixed if (QVersionNumber::fromString(ClangHelpers::clangVersion()) < QVersionNumber(9, 0, 0)) QEXPECT_FAIL("", "Base class isn't assigned correctly", Continue); QCOMPARE(b->baseClassesSize(), 1u); #if CINDEX_VERSION_MINOR < 34 // at least the one problem we have should have been propagated QCOMPARE(top->problems().count(), 1); #else // two errors: // /tmp/testfile_f32415.h:3:10: error: 'missing1.h' file not found // /tmp/testfile_f32415.h:11:10: error: 'missing2.h' file not found QCOMPARE(top->problems().count(), 2); #endif } QByteArray createCode(const QByteArray& prefix, const int functions) { QByteArray code; code += "#ifndef " + prefix + "_H\n"; code += "#define " + prefix + "_H\n"; for (int i = 0; i < functions; ++i) { code += "void myFunc_" + prefix + "(int arg1, char arg2, const char* arg3);\n"; } code += "#endif\n"; return code; } void TestDUChain::testIncludeLocking() { TestFile header1(createCode("Header1", 1000), QStringLiteral("h")); TestFile header2(createCode("Header2", 1000), QStringLiteral("h")); TestFile header3(createCode("Header3", 1000), QStringLiteral("h")); ICore::self()->languageController()->backgroundParser()->setThreadCount(3); TestFile impl1("#include \"" + header1.url().str() + "\"\n" "#include \"" + header2.url().str() + "\"\n" "#include \"" + header3.url().str() + "\"\n" "int main() { return 0; }", QStringLiteral("cpp")); TestFile impl2("#include \"" + header2.url().str() + "\"\n" "#include \"" + header1.url().str() + "\"\n" "#include \"" + header3.url().str() + "\"\n" "int main() { return 0; }", QStringLiteral("cpp")); TestFile impl3("#include \"" + header3.url().str() + "\"\n" "#include \"" + header1.url().str() + "\"\n" "#include \"" + header2.url().str() + "\"\n" "int main() { return 0; }", QStringLiteral("cpp")); impl1.parse(TopDUContext::AllDeclarationsContextsAndUses); impl2.parse(TopDUContext::AllDeclarationsContextsAndUses); impl3.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(impl1.waitForParsed(5000)); QVERIFY(impl2.waitForParsed(5000)); QVERIFY(impl3.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(DUChain::self()->chainForDocument(header1.url())); QVERIFY(DUChain::self()->chainForDocument(header2.url())); QVERIFY(DUChain::self()->chainForDocument(header3.url())); } void TestDUChain::testReparse() { TestFile file(QStringLiteral("int main() { int i = 42; return i; }"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); DeclarationPointer mainDecl; DeclarationPointer iDecl; for (int i = 0; i < 3; ++i) { QVERIFY(file.waitForParsed(500)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->childContexts().size(), 1); QCOMPARE(file.topContext()->localDeclarations().size(), 1); DUContext *exprContext = file.topContext()->childContexts().first()->childContexts().first(); QCOMPARE(exprContext->localDeclarations().size(), 1); if (i) { QVERIFY(mainDecl); QCOMPARE(mainDecl.data(), file.topContext()->localDeclarations().first()); QVERIFY(iDecl); QCOMPARE(iDecl.data(), exprContext->localDeclarations().first()); } mainDecl = file.topContext()->localDeclarations().first(); iDecl = exprContext->localDeclarations().first(); QVERIFY(mainDecl->uses().isEmpty()); QCOMPARE(iDecl->uses().size(), 1); QCOMPARE(iDecl->uses().begin()->size(), 1); if (i == 1) { file.setFileContents(QStringLiteral("int main()\n{\nfloat i = 13; return i - 5;\n}\n")); } file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdateRecursive)); } } void TestDUChain::testReparseError() { TestFile file(QStringLiteral("int i = 1 / 0;\n"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); for (int i = 0; i < 2; ++i) { QVERIFY(file.waitForParsed(500)); DUChainReadLocker lock; QVERIFY(file.topContext()); if (!i) { QCOMPARE(file.topContext()->problems().size(), 1); file.setFileContents(QStringLiteral("int i = 0;\n")); } else { QCOMPARE(file.topContext()->problems().size(), 0); } file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdateRecursive)); } } void TestDUChain::testTemplate() { TestFile file("template struct foo { T bar; };\n" "int main() { foo myFoo; return myFoo.bar; }\n", QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto fooDecl = file.topContext()->localDeclarations().first(); QVERIFY(fooDecl->internalContext()); QCOMPARE(fooDecl->internalContext()->localDeclarations().size(), 2); QCOMPARE(file.topContext()->findDeclarations(QualifiedIdentifier("foo< T >")).size(), 1); QCOMPARE(file.topContext()->findDeclarations(QualifiedIdentifier("foo< T >::bar")).size(), 1); auto mainCtx = file.topContext()->localDeclarations().last()->internalContext()->childContexts().first(); QVERIFY(mainCtx); auto myFoo = mainCtx->localDeclarations().first(); QVERIFY(myFoo); QCOMPARE(myFoo->abstractType()->toString().remove(' '), QStringLiteral("foo")); } void TestDUChain::testNamespace() { TestFile file("namespace foo { struct bar { int baz; }; }\n" "int main() { foo::bar myBar; }\n", QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto fooDecl = file.topContext()->localDeclarations().first(); QVERIFY(fooDecl->internalContext()); QCOMPARE(fooDecl->internalContext()->localDeclarations().size(), 1); DUContext* top = file.topContext().data(); DUContext* mainCtx = file.topContext()->childContexts().last(); auto foo = top->localDeclarations().first(); QCOMPARE(foo->qualifiedIdentifier().toString(), QString("foo")); DUContext* fooCtx = file.topContext()->childContexts().first(); QCOMPARE(fooCtx->localScopeIdentifier().toString(), QString("foo")); QCOMPARE(fooCtx->scopeIdentifier(true).toString(), QString("foo")); QCOMPARE(fooCtx->localDeclarations().size(), 1); auto bar = fooCtx->localDeclarations().first(); QCOMPARE(bar->qualifiedIdentifier().toString(), QString("foo::bar")); QCOMPARE(fooCtx->childContexts().size(), 1); DUContext* barCtx = fooCtx->childContexts().first(); QCOMPARE(barCtx->localScopeIdentifier().toString(), QString("bar")); QCOMPARE(barCtx->scopeIdentifier(true).toString(), QString("foo::bar")); QCOMPARE(barCtx->localDeclarations().size(), 1); auto baz = barCtx->localDeclarations().first(); QCOMPARE(baz->qualifiedIdentifier().toString(), QString("foo::bar::baz")); for (auto ctx : {top, mainCtx}) { QCOMPARE(ctx->findDeclarations(QualifiedIdentifier("foo")).size(), 1); QCOMPARE(ctx->findDeclarations(QualifiedIdentifier("foo::bar")).size(), 1); QCOMPARE(ctx->findDeclarations(QualifiedIdentifier("foo::bar::baz")).size(), 1); } } void TestDUChain::testAutoTypeDeduction() { TestFile file(QStringLiteral(R"( const volatile auto foo = 5; template struct myTemplate {}; myTemplate& > templRefParam; auto autoTemplRefParam = templRefParam; )"), QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; DUContext* ctx = file.topContext().data(); QVERIFY(ctx); QCOMPARE(ctx->localDeclarations().size(), 4); QCOMPARE(ctx->findDeclarations(QualifiedIdentifier("foo")).size(), 1); Declaration* decl = ctx->findDeclarations(QualifiedIdentifier(QStringLiteral("foo")))[0]; QCOMPARE(decl->identifier(), Identifier("foo")); #if CINDEX_VERSION_MINOR < 31 QEXPECT_FAIL("", "No type deduction here unfortunately, missing API in Clang", Continue); #endif QVERIFY(decl->type()); #if CINDEX_VERSION_MINOR < 31 QCOMPARE(decl->toString(), QStringLiteral("const volatile auto foo")); #else QCOMPARE(decl->toString(), QStringLiteral("const volatile int foo")); #endif decl = ctx->findDeclarations(QualifiedIdentifier(QStringLiteral("autoTemplRefParam")))[0]; QVERIFY(decl); QVERIFY(decl->abstractType()); #if CINDEX_VERSION_MINOR < 31 QEXPECT_FAIL("", "Auto type is not exposed via LibClang", Continue); #endif QCOMPARE(decl->abstractType()->toString(), QStringLiteral("myTemplate< myTemplate< int >& >")); } void TestDUChain::testTypeDeductionInTemplateInstantiation() { // see: http://clang-developers.42468.n3.nabble.com/RFC-missing-libclang-query-functions-features-td2504253.html TestFile file(QStringLiteral("template struct foo { T member; } foo f; auto i = f.member;"), QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; DUContext* ctx = file.topContext().data(); QVERIFY(ctx); QCOMPARE(ctx->localDeclarations().size(), 3); Declaration* decl = nullptr; // check 'foo' declaration decl = ctx->localDeclarations()[0]; QVERIFY(decl); QCOMPARE(decl->identifier(), Identifier("foo< T >")); // check type of 'member' inside declaration-scope QCOMPARE(ctx->childContexts().size(), 1); DUContext* fooCtx = ctx->childContexts().first(); QVERIFY(fooCtx); // Should there really be two declarations? QCOMPARE(fooCtx->localDeclarations().size(), 2); decl = fooCtx->localDeclarations()[1]; QCOMPARE(decl->identifier(), Identifier("member")); // check type of 'member' in definition of 'f' decl = ctx->localDeclarations()[1]; QCOMPARE(decl->identifier(), Identifier("f")); decl = ctx->localDeclarations()[2]; QCOMPARE(decl->identifier(), Identifier("i")); #if CINDEX_VERSION_MINOR < 31 QEXPECT_FAIL("", "No type deduction here unfortunately, missing API in Clang", Continue); #endif QVERIFY(decl->type()); } void TestDUChain::testVirtualMemberFunction() { //Forward-declarations with "struct" or "class" are considered equal, so make sure the override is detected correctly. TestFile file(QStringLiteral("struct S {}; struct A { virtual S* ret(); }; struct B : public A { virtual S* ret(); };"), QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; DUContext* top = file.topContext().data(); QVERIFY(top); QCOMPARE(top->childContexts().count(), 3); QCOMPARE(top->localDeclarations().count(), 3); QCOMPARE(top->childContexts()[2]->localDeclarations().count(), 1); Declaration* decl = top->childContexts()[2]->localDeclarations()[0]; QCOMPARE(decl->identifier(), Identifier("ret")); QVERIFY(DUChainUtils::overridden(decl)); } void TestDUChain::testBaseClasses() { TestFile file(QStringLiteral("class Base {}; class Inherited : public Base {};"), QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; DUContext* top = file.topContext().data(); QVERIFY(top); QCOMPARE(top->localDeclarations().count(), 2); Declaration* baseDecl = top->localDeclarations().first(); QCOMPARE(baseDecl->identifier(), Identifier("Base")); ClassDeclaration* inheritedDecl = dynamic_cast(top->localDeclarations()[1]); QCOMPARE(inheritedDecl->identifier(), Identifier("Inherited")); QVERIFY(inheritedDecl); QCOMPARE(inheritedDecl->baseClassesSize(), 1u); QCOMPARE(baseDecl->uses().count(), 1); QCOMPARE(baseDecl->uses().first().count(), 1); QCOMPARE(baseDecl->uses().first().first(), RangeInRevision(0, 40, 0, 44)); } void TestDUChain::testReparseBaseClasses() { TestFile file(QStringLiteral("struct a{}; struct b : a {};\n"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); for (int i = 0; i < 2; ++i) { qDebug() << "run: " << i; QVERIFY(file.waitForParsed(500)); DUChainWriteLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->childContexts().size(), 2); QCOMPARE(file.topContext()->childContexts().first()->importers().size(), 1); QCOMPARE(file.topContext()->childContexts().last()->importedParentContexts().size(), 1); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto aDecl = dynamic_cast(file.topContext()->localDeclarations().first()); QVERIFY(aDecl); QCOMPARE(aDecl->baseClassesSize(), 0u); auto bDecl = dynamic_cast(file.topContext()->localDeclarations().last()); QVERIFY(bDecl); QCOMPARE(bDecl->baseClassesSize(), 1u); int distance = 0; QVERIFY(bDecl->isPublicBaseClass(aDecl, file.topContext(), &distance)); QCOMPARE(distance, 1); file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdateRecursive)); } } void TestDUChain::testReparseBaseClassesTemplates() { TestFile file(QStringLiteral("template struct a{}; struct b : a {};\n"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); for (int i = 0; i < 2; ++i) { qDebug() << "run: " << i; QVERIFY(file.waitForParsed(500)); DUChainWriteLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->childContexts().size(), 2); QCOMPARE(file.topContext()->childContexts().first()->importers().size(), 1); QCOMPARE(file.topContext()->childContexts().last()->importedParentContexts().size(), 1); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto aDecl = dynamic_cast(file.topContext()->localDeclarations().first()); QVERIFY(aDecl); QCOMPARE(aDecl->baseClassesSize(), 0u); auto bDecl = dynamic_cast(file.topContext()->localDeclarations().last()); QVERIFY(bDecl); QCOMPARE(bDecl->baseClassesSize(), 1u); int distance = 0; QVERIFY(bDecl->isPublicBaseClass(aDecl, file.topContext(), &distance)); QCOMPARE(distance, 1); file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdateRecursive)); } } void TestDUChain::testGetInheriters_data() { QTest::addColumn("code"); QTest::newRow("inline") << "struct Base { struct Inner {}; }; struct Inherited : Base, Base::Inner {};"; QTest::newRow("outline") << "struct Base { struct Inner; }; struct Base::Inner {}; struct Inherited : Base, Base::Inner {};"; } void TestDUChain::testGetInheriters() { QFETCH(QString, code); TestFile file(code, QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; auto top = file.topContext(); QVERIFY(top); QVERIFY(top->problems().isEmpty()); QCOMPARE(top->localDeclarations().count(), 2); Declaration* baseDecl = top->localDeclarations().first(); QCOMPARE(baseDecl->identifier(), Identifier("Base")); DUContext* baseCtx = baseDecl->internalContext(); QVERIFY(baseCtx); QCOMPARE(baseCtx->localDeclarations().count(), 1); Declaration* innerDecl = baseCtx->localDeclarations().first(); QCOMPARE(innerDecl->identifier(), Identifier("Inner")); if (auto forward = dynamic_cast(innerDecl)) { innerDecl = forward->resolve(top); } QVERIFY(dynamic_cast(innerDecl)); Declaration* inheritedDecl = top->localDeclarations().last(); QVERIFY(inheritedDecl); QCOMPARE(inheritedDecl->identifier(), Identifier("Inherited")); uint maxAllowedSteps = uint(-1); auto baseInheriters = DUChainUtils::inheriters(baseDecl, maxAllowedSteps); QCOMPARE(baseInheriters, QList() << inheritedDecl); maxAllowedSteps = uint(-1); auto innerInheriters = DUChainUtils::inheriters(innerDecl, maxAllowedSteps); QCOMPARE(innerInheriters, QList() << inheritedDecl); maxAllowedSteps = uint(-1); auto inheritedInheriters = DUChainUtils::inheriters(inheritedDecl, maxAllowedSteps); QCOMPARE(inheritedInheriters.count(), 0); } void TestDUChain::testGlobalFunctionDeclaration() { TestFile file(QStringLiteral("void foo(int arg1, char arg2);\n"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); file.waitForParsed(); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 1); QCOMPARE(file.topContext()->childContexts().size(), 1); QVERIFY(!file.topContext()->childContexts().first()->inSymbolTable()); } void TestDUChain::testFunctionDefinitionVsDeclaration() { TestFile file(QStringLiteral("void func(); void func() {}\n"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed()); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto funcDecl = file.topContext()->localDeclarations()[0]; QVERIFY(!funcDecl->isDefinition()); QVERIFY(!dynamic_cast(funcDecl)); auto funcDef = file.topContext()->localDeclarations()[1]; QVERIFY(dynamic_cast(funcDef)); QVERIFY(funcDef->isDefinition()); } void TestDUChain::testEnsureNoDoubleVisit() { // On some language construct, we may up visiting the same cursor multiple times // Example: "struct SomeStruct {} s;" // decl: "SomeStruct SomeStruct " of kind StructDecl (2) in main.cpp@[(1,1),(1,17)] // decl: "struct SomeStruct s " of kind VarDecl (9) in main.cpp@[(1,1),(1,19)] // decl: "SomeStruct SomeStruct " of kind StructDecl (2) in main.cpp@[(1,1),(1,17)] // // => We end up visiting the StructDecl twice (or more) // That's because we use clang_visitChildren not just on the translation unit cursor. // Apparently just "recursing" vs. "visiting children explicitly" // results in a different AST traversal TestFile file(QStringLiteral("struct SomeStruct {} s;\n"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed()); DUChainReadLocker lock; auto top = file.topContext(); QVERIFY(top); // there should only be one declaration for "SomeStruct" auto candidates = top->findDeclarations(QualifiedIdentifier(QStringLiteral("SomeStruct"))); QCOMPARE(candidates.size(), 1); } void TestDUChain::testParsingEnvironment() { const TopDUContext::Features features = TopDUContext::AllDeclarationsContextsAndUses; IndexedTopDUContext indexed; ClangParsingEnvironment lastEnv; { TestFile file(QStringLiteral("int main() {}\n"), QStringLiteral("cpp")); auto astFeatures = static_cast(features | TopDUContext::AST); file.parse(astFeatures); file.setKeepDUChainData(true); QVERIFY(file.waitForParsed()); DUChainWriteLocker lock; auto top = file.topContext(); QVERIFY(top); auto sessionData = ParseSessionData::Ptr(dynamic_cast(top->ast().data())); lock.unlock(); ParseSession session(sessionData); lock.lock(); QVERIFY(session.data()); QVERIFY(top); auto envFile = QExplicitlySharedDataPointer( dynamic_cast(file.topContext()->parsingEnvironmentFile().data())); QCOMPARE(envFile->features(), astFeatures); QVERIFY(envFile->featuresSatisfied(astFeatures)); QCOMPARE(envFile->environmentQuality(), ClangParsingEnvironment::Source); // if no environment is given, no update should be triggered QVERIFY(!envFile->needsUpdate()); // same env should also not trigger a reparse ClangParsingEnvironment env = session.environment(); QCOMPARE(env.quality(), ClangParsingEnvironment::Source); QVERIFY(!envFile->needsUpdate(&env)); // but changing the environment should trigger an update env.addIncludes(Path::List() << Path(QStringLiteral("/foo/bar/baz"))); QVERIFY(envFile->needsUpdate(&env)); envFile->setEnvironment(env); QVERIFY(!envFile->needsUpdate(&env)); // setting the environment quality higher should require an update env.setQuality(ClangParsingEnvironment::BuildSystem); QVERIFY(envFile->needsUpdate(&env)); envFile->setEnvironment(env); QVERIFY(!envFile->needsUpdate(&env)); // changing defines requires an update env.addDefines(QHash{ { "foo", "bar" } }); QVERIFY(envFile->needsUpdate(&env)); // but only when changing the defines for the envFile's TU const auto barTU = IndexedString("bar.cpp"); const auto oldTU = env.translationUnitUrl(); env.setTranslationUnitUrl(barTU); QCOMPARE(env.translationUnitUrl(), barTU); QVERIFY(!envFile->needsUpdate(&env)); env.setTranslationUnitUrl(oldTU); QVERIFY(envFile->needsUpdate(&env)); // update it again envFile->setEnvironment(env); QVERIFY(!envFile->needsUpdate(&env)); lastEnv = env; // now compare against a lower quality environment // in such a case, we do not want to trigger an update env.setQuality(ClangParsingEnvironment::Unknown); env.setTranslationUnitUrl(barTU); QVERIFY(!envFile->needsUpdate(&env)); // even when the environment changes env.addIncludes(Path::List() << Path(QStringLiteral("/lalalala"))); QVERIFY(!envFile->needsUpdate(&env)); indexed = top->indexed(); } DUChain::self()->storeToDisk(); { DUChainWriteLocker lock; QVERIFY(!DUChain::self()->isInMemory(indexed.index())); QVERIFY(indexed.data()); QVERIFY(DUChain::self()->environmentFileForDocument(indexed)); auto envFile = QExplicitlySharedDataPointer( dynamic_cast(DUChain::self()->environmentFileForDocument(indexed).data())); QVERIFY(envFile); QCOMPARE(envFile->features(), features); QVERIFY(envFile->featuresSatisfied(features)); QVERIFY(!envFile->needsUpdate(&lastEnv)); DUChain::self()->removeDocumentChain(indexed.data()); } } void TestDUChain::testActiveDocumentHasASTAttached() { const TopDUContext::Features features = TopDUContext::AllDeclarationsContextsAndUses; IndexedTopDUContext indexed; ClangParsingEnvironment lastEnv; { TestFile file(QStringLiteral("int main() {}\n"), QStringLiteral("cpp")); auto astFeatures = static_cast(features | TopDUContext::AST); file.parse(astFeatures); file.setKeepDUChainData(true); QVERIFY(file.waitForParsed()); DUChainWriteLocker lock; auto top = file.topContext(); QVERIFY(top); auto sessionData = ParseSessionData::Ptr(dynamic_cast(top->ast().data())); lock.unlock(); ParseSession session(sessionData); lock.lock(); QVERIFY(session.data()); QVERIFY(top); QVERIFY(top->ast()); indexed = top->indexed(); } DUChain::self()->storeToDisk(); { DUChainWriteLocker lock; QVERIFY(!DUChain::self()->isInMemory(indexed.index())); QVERIFY(indexed.data()); } QUrl url; { DUChainReadLocker lock; auto ctx = indexed.data(); QVERIFY(ctx); QVERIFY(!ctx->ast()); url = ctx->url().toUrl(); } QVERIFY(!QFileInfo::exists(url.toLocalFile())); QFile file(url.toLocalFile()); file.open(QIODevice::WriteOnly); Q_ASSERT(file.isOpen()); auto document = ICore::self()->documentController()->openDocument(url); QVERIFY(document); ICore::self()->documentController()->activateDocument(document); QApplication::processEvents(); ICore::self()->languageController()->backgroundParser()->parseDocuments(); QThread::sleep(1); document->close(KDevelop::IDocument::Discard); { DUChainReadLocker lock; auto ctx = indexed.data(); QVERIFY(ctx); QVERIFY(ctx->ast()); } DUChainWriteLocker lock; DUChain::self()->removeDocumentChain(indexed.data()); } void TestDUChain::testActiveDocumentsGetBestPriority() { // note: this test would make more sense in kdevplatform, but we don't have a language plugin available there // (required for background parsing) // TODO: Create a fake-language plugin in kdevplatform for testing purposes, use that. TestFile file1(QStringLiteral("int main() {}\n"), QStringLiteral("cpp")); TestFile file2(QStringLiteral("int main() {}\n"), QStringLiteral("cpp")); TestFile file3(QStringLiteral("int main() {}\n"), QStringLiteral("cpp")); DUChain::self()->storeToDisk(); auto backgroundParser = ICore::self()->languageController()->backgroundParser(); QVERIFY(!backgroundParser->isQueued(file1.url())); auto documentController = ICore::self()->documentController(); // open first document (no activation) auto doc = documentController->openDocument(file1.url().toUrl(), KTextEditor::Range::invalid(), {IDocumentController::DoNotActivate}); QVERIFY(doc); QVERIFY(backgroundParser->isQueued(file1.url())); QCOMPARE(backgroundParser->priorityForDocument(file1.url()), (int)BackgroundParser::NormalPriority); // open second document, activate doc = documentController->openDocument(file2.url().toUrl()); QVERIFY(doc); QVERIFY(backgroundParser->isQueued(file2.url())); QCOMPARE(backgroundParser->priorityForDocument(file2.url()), (int)BackgroundParser::BestPriority); // open third document, activate, too doc = documentController->openDocument(file3.url().toUrl()); QVERIFY(doc); QVERIFY(backgroundParser->isQueued(file3.url())); QCOMPARE(backgroundParser->priorityForDocument(file3.url()), (int)BackgroundParser::BestPriority); } void TestDUChain::testSystemIncludes() { ClangParsingEnvironment env; Path::List projectIncludes = { Path("/projects/1"), Path("/projects/1/sub"), Path("/projects/2"), Path("/projects/2/sub") }; env.addIncludes(projectIncludes); auto includes = env.includes(); // no project paths set, so everything is considered a system include QCOMPARE(includes.system, projectIncludes); QVERIFY(includes.project.isEmpty()); Path::List systemIncludes = { Path("/sys"), Path("/sys/sub") }; env.addIncludes(systemIncludes); includes = env.includes(); QCOMPARE(includes.system, projectIncludes + systemIncludes); QVERIFY(includes.project.isEmpty()); Path::List projects = { Path("/projects/1"), Path("/projects/2") }; env.setProjectPaths(projects); // now the list should be properly separated QCOMPARE(env.projectPaths(), projects); includes = env.includes(); QCOMPARE(includes.system, systemIncludes); QCOMPARE(includes.project, projectIncludes); } void TestDUChain::testReparseWithAllDeclarationsContextsAndUses() { TestFile file(QStringLiteral("int foo() { return 0; } int main() { return foo(); }"), QStringLiteral("cpp")); file.parse(TopDUContext::VisibleDeclarationsAndContexts); QVERIFY(file.waitForParsed(1000)); { DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->childContexts().size(), 2); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto dec = file.topContext()->localDeclarations().at(0); QEXPECT_FAIL("", "Skipping of function bodies is disabled for now", Continue); QVERIFY(dec->uses().isEmpty()); } file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(500)); { DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->childContexts().size(), 2); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto mainDecl = file.topContext()->localDeclarations()[1]; QVERIFY(mainDecl->uses().isEmpty()); auto foo = file.topContext()->localDeclarations().first(); QCOMPARE(foo->uses().size(), 1); } } void TestDUChain::testReparseOnDocumentActivated() { TestFile file(QStringLiteral("int foo() { return 0; } int main() { return foo(); }"), QStringLiteral("cpp")); file.parse(TopDUContext::VisibleDeclarationsAndContexts); QVERIFY(file.waitForParsed(1000)); { DUChainReadLocker lock; auto ctx = file.topContext(); QVERIFY(ctx); QCOMPARE(ctx->childContexts().size(), 2); QCOMPARE(ctx->localDeclarations().size(), 2); auto dec = ctx->localDeclarations().at(0); QEXPECT_FAIL("", "Skipping of function bodies was disabled for now", Continue); QVERIFY(dec->uses().isEmpty()); QVERIFY(!ctx->ast()); } auto backgroundParser = ICore::self()->languageController()->backgroundParser(); QVERIFY(!backgroundParser->isQueued(file.url())); auto doc = ICore::self()->documentController()->openDocument(file.url().toUrl()); QVERIFY(doc); QVERIFY(backgroundParser->isQueued(file.url())); QSignalSpy spy(backgroundParser, &BackgroundParser::parseJobFinished); spy.wait(); doc->close(KDevelop::IDocument::Discard); { DUChainReadLocker lock; auto ctx = file.topContext(); QCOMPARE(ctx->features() & TopDUContext::AllDeclarationsContextsAndUses, static_cast(TopDUContext::AllDeclarationsContextsAndUses)); QVERIFY(ctx->topContext()->ast()); } } void TestDUChain::testReparseInclude() { TestFile header(QStringLiteral("int foo() { return 42; }\n"), QStringLiteral("h")); TestFile impl("#include \"" + header.url().str() + "\"\n" "int main() { return foo(); }", QStringLiteral("cpp"), &header); // Use TopDUContext::AST to imitate that document is opened in the editor, so that ClangParseJob can store translation unit, that'll be used for reparsing. impl.parse(TopDUContext::Features(TopDUContext::AllDeclarationsAndContexts|TopDUContext::AST)); QVERIFY(impl.waitForParsed(5000)); { DUChainReadLocker lock; auto implCtx = impl.topContext(); QVERIFY(implCtx); QCOMPARE(implCtx->importedParentContexts().size(), 1); } impl.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST)); QVERIFY(impl.waitForParsed(5000)); DUChainReadLocker lock; auto implCtx = impl.topContext(); QVERIFY(implCtx); QCOMPARE(implCtx->localDeclarations().size(), 1); QCOMPARE(implCtx->importedParentContexts().size(), 1); auto headerCtx = DUChain::self()->chainForDocument(header.url()); QVERIFY(headerCtx); QVERIFY(!headerCtx->parsingEnvironmentFile()->needsUpdate()); QCOMPARE(headerCtx->localDeclarations().size(), 1); QVERIFY(implCtx->imports(headerCtx, CursorInRevision(0, 10))); Declaration* foo = headerCtx->localDeclarations().first(); QCOMPARE(foo->uses().size(), 1); QCOMPARE(foo->uses().begin().key(), impl.url()); QCOMPARE(foo->uses().begin()->size(), 1); QCOMPARE(foo->uses().begin()->first(), RangeInRevision(1, 20, 1, 23)); QCOMPARE(DUChain::self()->allEnvironmentFiles(header.url()).size(), 1); QCOMPARE(DUChain::self()->allEnvironmentFiles(impl.url()).size(), 1); QCOMPARE(DUChain::self()->chainsForDocument(header.url()).size(), 1); QCOMPARE(DUChain::self()->chainsForDocument(impl.url()).size(), 1); } void TestDUChain::testReparseChangeEnvironment() { TestFile header(QStringLiteral("int foo() { return 42; }\n"), QStringLiteral("h")); TestFile impl("#include \"" + header.url().str() + "\"\n" "int main() { return foo(); }", QStringLiteral("cpp"), &header); uint hashes[3] = {0, 0, 0}; for (int i = 0; i < 3; ++i) { impl.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST|TopDUContext::ForceUpdate)); QVERIFY(impl.waitForParsed(5000)); { DUChainReadLocker lock; QVERIFY(impl.topContext()); auto env = dynamic_cast(impl.topContext()->parsingEnvironmentFile().data()); QVERIFY(env); QCOMPARE(env->environmentQuality(), ClangParsingEnvironment::Source); hashes[i] = env->environmentHash(); QVERIFY(hashes[i]); // we should never end up with multiple env files or chains in memory for these files QCOMPARE(DUChain::self()->allEnvironmentFiles(impl.url()).size(), 1); QCOMPARE(DUChain::self()->chainsForDocument(impl.url()).size(), 1); QCOMPARE(DUChain::self()->allEnvironmentFiles(header.url()).size(), 1); QCOMPARE(DUChain::self()->chainsForDocument(header.url()).size(), 1); } // in every run, we expect the environment to have changed for (int j = 0; j < i; ++j) { QVERIFY(hashes[i] != hashes[j]); } if (i == 0) { // 1) change defines m_provider->defines.insert(QStringLiteral("foooooooo"), QStringLiteral("baaar!")); } else if (i == 1) { // 2) change includes m_provider->includes.append(Path(QStringLiteral("/foo/bar/asdf/lalala"))); } // 3) stop } } void TestDUChain::testMacroDependentHeader() { TestFile header(QStringLiteral("struct MY_CLASS { struct Q{Q(); int m;}; int m; };\n"), QStringLiteral("h")); TestFile impl("#define MY_CLASS A\n" "#include \"" + header.url().str() + "\"\n" "#undef MY_CLASS\n" "#define MY_CLASS B\n" "#include \"" + header.url().str() + "\"\n" "#undef MY_CLASS\n" "A a;\n" "const A::Q aq;\n" "B b;\n" "const B::Q bq;\n" "int am = a.m;\n" "int aqm = aq.m;\n" "int bm = b.m;\n" "int bqm = bq.m;\n" , QStringLiteral("cpp"), &header); impl.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST|TopDUContext::ForceUpdate)); QVERIFY(impl.waitForParsed(500000)); DUChainReadLocker lock; TopDUContext* top = impl.topContext().data(); QVERIFY(top); QCOMPARE(top->localDeclarations().size(), 10); // 2x macro, then a, aq, b, bq QCOMPARE(top->importedParentContexts().size(), 1); AbstractType::Ptr type = top->localDeclarations()[2]->abstractType(); auto* sType = dynamic_cast(type.data()); QVERIFY(sType); QCOMPARE(sType->toString(), QString("A")); Declaration* decl = sType->declaration(top); QVERIFY(decl); AbstractType::Ptr type2 = top->localDeclarations()[4]->abstractType(); auto* sType2 = dynamic_cast(type2.data()); QVERIFY(sType2); QCOMPARE(sType2->toString(), QString("B")); Declaration* decl2 = sType2->declaration(top); QVERIFY(decl2); TopDUContext* top2 = dynamic_cast(top->importedParentContexts()[0].context(top)); QVERIFY(top2); QCOMPARE(top2->localDeclarations().size(), 2); QCOMPARE(top2->localDeclarations()[0], decl); QCOMPARE(top2->localDeclarations()[1], decl2); qDebug() << "DECL RANGE:" << top2->localDeclarations()[0]->range().castToSimpleRange(); qDebug() << "CTX RANGE:" << top2->localDeclarations()[0]->internalContext()->range().castToSimpleRange(); // validate uses: QCOMPARE(top->usesCount(), 14); QCOMPARE(top->uses()[0].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("A")); QCOMPARE(top->uses()[1].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("A")); QCOMPARE(top->uses()[2].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("A::Q")); QCOMPARE(top->uses()[3].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("B")); QCOMPARE(top->uses()[4].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("B")); QCOMPARE(top->uses()[5].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("B::Q")); QCOMPARE(top->uses()[6].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("a")); QCOMPARE(top->uses()[7].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("A::m")); QCOMPARE(top->uses()[8].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("aq")); QCOMPARE(top->uses()[9].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("A::Q::m")); QCOMPARE(top->uses()[10].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("b")); QCOMPARE(top->uses()[11].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("B::m")); QCOMPARE(top->uses()[12].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("bq")); QCOMPARE(top->uses()[13].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("B::Q::m")); } void TestDUChain::testHeaderParsingOrder1() { TestFile header(QStringLiteral("typedef const A B;\n"), QStringLiteral("h")); TestFile impl("template class A{};\n" "#include \"" + header.url().str() + "\"\n" "B c;", QStringLiteral("cpp"), &header); impl.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST|TopDUContext::ForceUpdate)); QVERIFY(impl.waitForParsed(500000)); DUChainReadLocker lock; TopDUContext* top = impl.topContext().data(); QVERIFY(top); QCOMPARE(top->localDeclarations().size(), 2); QCOMPARE(top->importedParentContexts().size(), 1); AbstractType::Ptr type = top->localDeclarations()[1]->abstractType(); auto* aType = dynamic_cast(type.data()); QVERIFY(aType); AbstractType::Ptr targetType = aType->type(); QVERIFY(targetType); auto *idType = dynamic_cast(targetType.data()); QVERIFY(idType); // this declaration could be resolved, because it was created with an // indirect DeclarationId that is resolved from the perspective of 'top' Declaration* decl = idType->declaration(top); // NOTE: the decl. doesn't know (yet) about the template insantiation QVERIFY(decl); QCOMPARE(decl, top->localDeclarations()[0]); // now ensure that a use was build for 'A' in header1 TopDUContext* top2 = dynamic_cast(top->importedParentContexts()[0].context(top)); QVERIFY(top2); QEXPECT_FAIL("", "the use could not be created because the corresponding declaration didn't exist yet", Continue); QCOMPARE(top2->usesCount(), 1); // Declaration* decl2 = top2->uses()[0].usedDeclaration(top2); // QVERIFY(decl2); // QCOMPARE(decl, decl2); } void TestDUChain::testHeaderParsingOrder2() { TestFile header(QStringLiteral("template class A{};\n"), QStringLiteral("h")); TestFile header2(QStringLiteral("typedef const A B;\n"), QStringLiteral("h")); TestFile impl("#include \"" + header.url().str() + "\"\n" "#include \"" + header2.url().str() + "\"\n" "B c;", QStringLiteral("cpp"), &header); impl.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST|TopDUContext::ForceUpdate)); QVERIFY(impl.waitForParsed(500000)); DUChainReadLocker lock; TopDUContext* top = impl.topContext().data(); QVERIFY(top); QCOMPARE(top->localDeclarations().size(), 1); QCOMPARE(top->importedParentContexts().size(), 2); AbstractType::Ptr type = top->localDeclarations()[0]->abstractType(); auto* aType = dynamic_cast(type.data()); QVERIFY(aType); AbstractType::Ptr targetType = aType->type(); QVERIFY(targetType); auto *idType = dynamic_cast(targetType.data()); QVERIFY(idType); Declaration* decl = idType->declaration(top); // NOTE: the decl. doesn't know (yet) about the template insantiation QVERIFY(decl); // now ensure that a use was build for 'A' in header2 TopDUContext* top2 = dynamic_cast(top->importedParentContexts()[1].context(top)); QVERIFY(top2); QCOMPARE(top2->usesCount(), 1); Declaration* decl2 = top2->uses()[0].usedDeclaration(top2); QCOMPARE(decl, decl2); } void TestDUChain::testMacrosRanges() { TestFile file(QStringLiteral("#define FUNC_MACROS(x) struct str##x{};\nFUNC_MACROS(x);"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto macroDefinition = file.topContext()->localDeclarations()[0]; QVERIFY(macroDefinition); QCOMPARE(macroDefinition->range(), RangeInRevision(0,8,0,19)); auto structDeclaration = file.topContext()->localDeclarations()[1]; QVERIFY(structDeclaration); QCOMPARE(structDeclaration->range(), RangeInRevision(1,0,1,0)); QCOMPARE(macroDefinition->uses().size(), 1); QCOMPARE(macroDefinition->uses().begin()->first(), RangeInRevision(1,0,1,11)); } void TestDUChain::testMacroUses() { TestFile file(QStringLiteral("#define USER(x) x\n#define USED\nUSER(USED)"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto macroDefinition1 = file.topContext()->localDeclarations()[0]; auto macroDefinition2 = file.topContext()->localDeclarations()[1]; QCOMPARE(macroDefinition1->uses().size(), 1); QCOMPARE(macroDefinition1->uses().begin()->first(), RangeInRevision(2,0,2,4)); #if CINDEX_VERSION_MINOR < 32 QEXPECT_FAIL("", "This appears to be a clang bug, the AST doesn't contain the macro use", Continue); #endif QCOMPARE(macroDefinition2->uses().size(), 1); if (macroDefinition2->uses().size()) { QCOMPARE(macroDefinition2->uses().begin()->first(), RangeInRevision(2,5,2,9)); } } void TestDUChain::testMultiLineMacroRanges() { TestFile file(QStringLiteral("#define FUNC_MACROS(x) struct str##x{};\nFUNC_MACROS(x\n);"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto macroDefinition = file.topContext()->localDeclarations()[0]; QVERIFY(macroDefinition); QCOMPARE(macroDefinition->range(), RangeInRevision(0,8,0,19)); auto structDeclaration = file.topContext()->localDeclarations()[1]; QVERIFY(structDeclaration); QCOMPARE(structDeclaration->range(), RangeInRevision(1,0,1,0)); QCOMPARE(macroDefinition->uses().size(), 1); QCOMPARE(macroDefinition->uses().begin()->first(), RangeInRevision(1,0,1,11)); } void TestDUChain::testNestedMacroRanges() { TestFile file(QStringLiteral("#define INNER int var; var = 0;\n#define MACRO() INNER\nint main(){MACRO(\n);}"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 3); auto main = file.topContext()->localDeclarations()[2]; QVERIFY(main); auto mainCtx = main->internalContext()->childContexts().first(); QVERIFY(mainCtx); QCOMPARE(mainCtx->localDeclarations().size(), 1); auto var = mainCtx->localDeclarations().first(); QVERIFY(var); QCOMPARE(var->range(), RangeInRevision(2,11,2,11)); QCOMPARE(var->uses().size(), 1); QCOMPARE(var->uses().begin()->first(), RangeInRevision(2,11,2,11)); } void TestDUChain::testNestedImports() { TestFile B(QStringLiteral("#pragma once\nint B();\n"), QStringLiteral("h")); TestFile C("#pragma once\n#include \"" + B.url().str() + "\"\nint C();\n", QStringLiteral("h")); TestFile A("#include \"" + B.url().str() + "\"\n" + "#include \"" + C.url().str() + "\"\nint A();\n", QStringLiteral("cpp")); A.parse(); QVERIFY(A.waitForParsed(5000)); DUChainReadLocker lock; auto BCtx = DUChain::self()->chainForDocument(B.url().toUrl()); QVERIFY(BCtx); QVERIFY(BCtx->importedParentContexts().isEmpty()); auto CCtx = DUChain::self()->chainForDocument(C.url().toUrl()); QVERIFY(CCtx); QCOMPARE(CCtx->importedParentContexts().size(), 1); QVERIFY(CCtx->imports(BCtx, CursorInRevision(1, 10))); auto ACtx = A.topContext(); QVERIFY(ACtx); QCOMPARE(ACtx->importedParentContexts().size(), 2); QVERIFY(ACtx->imports(BCtx, CursorInRevision(0, 10))); QVERIFY(ACtx->imports(CCtx, CursorInRevision(1, 10))); } void TestDUChain::testEnvironmentWithDifferentOrderOfElements() { TestFile file(QStringLiteral("int main();\n"), QStringLiteral("cpp")); m_provider->includes.clear(); m_provider->includes.append(Path(QStringLiteral("/path1"))); m_provider->includes.append(Path(QStringLiteral("/path2"))); m_provider->defines.clear(); m_provider->defines.insert(QStringLiteral("key1"), QStringLiteral("value1")); m_provider->defines.insert(QStringLiteral("key2"), QStringLiteral("value2")); m_provider->defines.insert(QStringLiteral("key3"), QStringLiteral("value3")); uint previousHash = 0; for (int i: {0, 1, 2, 3}) { file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST|TopDUContext::ForceUpdate)); QVERIFY(file.waitForParsed(5000)); { DUChainReadLocker lock; QVERIFY(file.topContext()); auto env = dynamic_cast(file.topContext()->parsingEnvironmentFile().data()); QVERIFY(env); QCOMPARE(env->environmentQuality(), ClangParsingEnvironment::Source); if (previousHash) { if (i == 3) { QVERIFY(previousHash != env->environmentHash()); } else { QCOMPARE(previousHash, env->environmentHash()); } } previousHash = env->environmentHash(); QVERIFY(previousHash); } if (i == 0) { //Change order of defines. Hash of the environment should stay the same. m_provider->defines.clear(); m_provider->defines.insert(QStringLiteral("key3"), QStringLiteral("value3")); m_provider->defines.insert(QStringLiteral("key1"), QStringLiteral("value1")); m_provider->defines.insert(QStringLiteral("key2"), QStringLiteral("value2")); } else if (i == 1) { //Add the same macros twice. Hash of the environment should stay the same. m_provider->defines.clear(); m_provider->defines.insert(QStringLiteral("key2"), QStringLiteral("value2")); m_provider->defines.insert(QStringLiteral("key3"), QStringLiteral("value3")); m_provider->defines.insert(QStringLiteral("key3"), QStringLiteral("value3")); m_provider->defines.insert(QStringLiteral("key1"), QStringLiteral("value1")); } else if (i == 2) { //OTOH order of includes should change hash of the environment. m_provider->includes.clear(); m_provider->includes.append(Path(QStringLiteral("/path2"))); m_provider->includes.append(Path(QStringLiteral("/path1"))); } } } void TestDUChain::testReparseMacro() { TestFile file(QStringLiteral("#define DECLARE(a) typedef struct a##_ {} *a;\nDECLARE(D);\nD d;"), QStringLiteral("cpp")); file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST)); QVERIFY(file.waitForParsed(5000)); { DUChainReadLocker lock; QVERIFY(file.topContext()); } file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST|TopDUContext::ForceUpdate)); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 5); auto macroDefinition = file.topContext()->localDeclarations()[0]; QVERIFY(macroDefinition); QCOMPARE(macroDefinition->range(), RangeInRevision(0,8,0,15)); QCOMPARE(macroDefinition->uses().size(), 1); QCOMPARE(macroDefinition->uses().begin()->first(), RangeInRevision(1,0,1,7)); auto structDeclaration = file.topContext()->localDeclarations()[1]; QVERIFY(structDeclaration); QCOMPARE(structDeclaration->range(), RangeInRevision(1,0,1,0)); auto structTypedef = file.topContext()->localDeclarations()[3]; QVERIFY(structTypedef); QCOMPARE(structTypedef->range(), RangeInRevision(1,8,1,9)); QCOMPARE(structTypedef->uses().size(), 1); QCOMPARE(structTypedef->uses().begin()->first(), RangeInRevision(2,0,2,1)); } void TestDUChain::testGotoStatement() { TestFile file(QStringLiteral("int main() {\ngoto label;\ngoto label;\nlabel: return 0;}"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 1); auto main = file.topContext()->localDeclarations()[0]; QVERIFY(main); auto mainCtx = main->internalContext()->childContexts().first(); QVERIFY(mainCtx); QCOMPARE(mainCtx->localDeclarations().size(), 1); auto label = mainCtx->localDeclarations().first(); QVERIFY(label); QCOMPARE(label->range(), RangeInRevision(3,0,3,5)); QCOMPARE(label->uses().size(), 1); QCOMPARE(label->uses().begin()->first(), RangeInRevision(1,5,1,10)); QCOMPARE(label->uses().begin()->last(), RangeInRevision(2,5,2,10)); } void TestDUChain::testRangesOfOperatorsInsideMacro() { TestFile file(QStringLiteral("class Test{public: Test& operator++(int);};\n#define MACRO(var) var++;\nint main(){\nTest tst; MACRO(tst)}"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 3); auto testClass = file.topContext()->localDeclarations()[0]; QVERIFY(testClass); auto operatorPlusPlus = testClass->internalContext()->localDeclarations().first(); QVERIFY(operatorPlusPlus); QCOMPARE(operatorPlusPlus->uses().size(), 1); QCOMPARE(operatorPlusPlus->uses().begin()->first(), RangeInRevision(3,10,3,10)); } void TestDUChain::testUsesCreatedForDeclarations() { auto code = R"(template void functionTemplate(T); template void functionTemplate(U) {} namespace NS { class Class{}; } using NS::Class; Class function(); NS::Class function() { return {}; } int main () { functionTemplate(int()); function(); } )"; TestFile file(code, QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed()); DUChainReadLocker lock; QVERIFY(file.topContext()); auto functionTemplate = file.topContext()->findDeclarations(QualifiedIdentifier(QStringLiteral("functionTemplate"))); QVERIFY(!functionTemplate.isEmpty()); auto functionTemplateDeclaration = DUChainUtils::declarationForDefinition(functionTemplate.first()); QVERIFY(!functionTemplateDeclaration->isDefinition()); #if CINDEX_VERSION_MINOR < 29 QEXPECT_FAIL("", "No API in LibClang to determine function template type", Continue); #endif QCOMPARE(functionTemplateDeclaration->uses().count(), 1); auto function = file.topContext()->findDeclarations(QualifiedIdentifier(QStringLiteral("function"))); QVERIFY(!function.isEmpty()); auto functionDeclaration = DUChainUtils::declarationForDefinition(function.first()); QVERIFY(!functionDeclaration->isDefinition()); QCOMPARE(functionDeclaration->uses().count(), 1); } void TestDUChain::testReparseIncludeGuard() { TestFile header(QStringLiteral("#ifndef GUARD\n#define GUARD\nint something;\n#endif\n"), QStringLiteral("h")); TestFile impl("#include \"" + header.url().str() + "\"\n", QStringLiteral("cpp"), &header); QVERIFY(impl.parseAndWait(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::AST ))); { DUChainReadLocker lock; QCOMPARE(static_cast(impl.topContext()-> importedParentContexts().first().context(impl.topContext()))->problems().size(), 0); } QVERIFY(impl.parseAndWait(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdateRecursive))); { DUChainReadLocker lock; QCOMPARE(static_cast(impl.topContext()-> importedParentContexts().first().context(impl.topContext()))->problems().size(), 0); } } void TestDUChain::testExternC() { auto code = R"(extern "C" { void foo(); })"; TestFile file(code, QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed()); DUChainReadLocker lock; auto top = file.topContext(); QVERIFY(top); QVERIFY(!top->findDeclarations(QualifiedIdentifier("foo")).isEmpty()); } void TestDUChain::testReparseUnchanged_data() { QTest::addColumn("headerCode"); QTest::addColumn("implCode"); QTest::newRow("include-guards") << R"( #ifndef GUARD #define GUARD int something; #endif )" << R"( #include "%1" )"; QTest::newRow("template-default-parameters") << R"( #ifndef TEST_H #define TEST_H template class dummy; template class dummy { int field[T]; }; #endif )" << R"( #include "%1" int main(int, char **) { dummy<> x; (void)x; } )"; } void TestDUChain::testReparseUnchanged() { QFETCH(QString, headerCode); QFETCH(QString, implCode); TestFile header(headerCode, QStringLiteral("h")); TestFile impl(implCode.arg(header.url().str()), QStringLiteral("cpp"), &header); auto checkProblems = [&] (bool reparsed) { DUChainReadLocker lock; auto headerCtx = DUChain::self()->chainForDocument(header.url()); QVERIFY(headerCtx); QVERIFY(headerCtx->problems().isEmpty()); auto implCtx = DUChain::self()->chainForDocument(impl.url()); QVERIFY(implCtx); if (reparsed && CINDEX_VERSION_MINOR > 29 && CINDEX_VERSION_MINOR < 33) { QEXPECT_FAIL("template-default-parameters", "the precompiled preamble messes the default template parameters up in clang 3.7", Continue); } QVERIFY(implCtx->problems().isEmpty()); }; impl.parseAndWait(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::AST )); checkProblems(false); impl.parseAndWait(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdateRecursive)); checkProblems(true); } void TestDUChain::testTypeAliasTemplate() { TestFile file(QStringLiteral("template using Alias = T; using Foo = Alias;"), QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto templateAlias = file.topContext()->localDeclarations().first(); QVERIFY(templateAlias); #if CINDEX_VERSION_MINOR < 31 QEXPECT_FAIL("", "TypeAliasTemplate is not exposed via LibClang", Abort); #endif QVERIFY(templateAlias->isTypeAlias()); QVERIFY(templateAlias->abstractType()); QCOMPARE(templateAlias->abstractType()->toString(), QStringLiteral("Alias")); QCOMPARE(templateAlias->uses().size(), 1); QCOMPARE(templateAlias->uses().first().size(), 1); QCOMPARE(templateAlias->uses().first().first(), RangeInRevision(0, 51, 0, 56)); } void TestDUChain::testDeclarationsInsideMacroExpansion() { TestFile header(QStringLiteral("#define DECLARE(a) typedef struct a##__ {int var;} *a\nDECLARE(D);\n"), QStringLiteral("h")); TestFile file("#include \"" + header.url().str() + "\"\nint main(){\nD d; d->var;}\n", QStringLiteral("cpp")); file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST)); QVERIFY(file.waitForParsed(5000)); { DUChainReadLocker lock; QVERIFY(file.topContext()); } file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST|TopDUContext::ForceUpdate)); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 1); auto context = file.topContext()->childContexts().first()->childContexts().first(); QVERIFY(context); QCOMPARE(context->localDeclarations().size(), 1); QCOMPARE(context->usesCount(), 3); QCOMPARE(context->uses()[0].m_range, RangeInRevision({2, 0}, {2, 1})); QCOMPARE(context->uses()[1].m_range, RangeInRevision({2, 5}, {2, 6})); QCOMPARE(context->uses()[2].m_range, RangeInRevision({2, 8}, {2, 11})); } // see also: https://bugs.kde.org/show_bug.cgi?id=368067 void TestDUChain::testForwardTemplateTypeParameterContext() { TestFile file(QStringLiteral(R"( template class Foo; class MatchingName { void bar(); }; void MatchingName::bar() { } )"), QStringLiteral("cpp")); file.parse(); QVERIFY(file.waitForParsed(500)); DUChainReadLocker lock; const auto top = file.topContext(); QVERIFY(top); DUChainDumper dumper(DUChainDumper::Features(DUChainDumper::DumpContext | DUChainDumper::DumpProblems)); dumper.dump(top); auto declarations = top->localDeclarations(); QCOMPARE(declarations.size(), 2); } // see also: https://bugs.kde.org/show_bug.cgi?id=368460 void TestDUChain::testTemplateFunctionParameterName() { TestFile file(QStringLiteral(R"( template void foo(int name); void bar(int name); )"), QStringLiteral("cpp")); file.parse(); QVERIFY(file.waitForParsed(500)); DUChainReadLocker lock; const auto top = file.topContext(); QVERIFY(top); DUChainDumper dumper(DUChainDumper::Features(DUChainDumper::DumpContext | DUChainDumper::DumpProblems)); dumper.dump(top); - auto declarations = top->localDeclarations(); + const auto declarations = top->localDeclarations(); QCOMPARE(declarations.size(), 2); for (auto decl : declarations) { auto ctx = DUChainUtils::argumentContext(decl); QVERIFY(ctx); auto args = ctx->localDeclarations(); if (decl == declarations.first()) QEXPECT_FAIL("", "We get two declarations, for both template and args :(", Continue); QCOMPARE(args.size(), 1); if (decl == declarations.first()) QEXPECT_FAIL("", "see above, this then triggers T T here", Continue); QCOMPARE(args.first()->toString(), QStringLiteral("int name")); } } static bool containsErrors(const QList& problems) { auto it = std::find_if(problems.begin(), problems.end(), [] (const Problem::Ptr& problem) { return problem->severity() == Problem::Error; }); return it != problems.end(); } static bool expectedXmmintrinErrors(const QList& problems) { for (const auto& problem : problems) { if (problem->severity() == Problem::Error && !problem->description().contains(QLatin1String("Cannot initialize a parameter of type"))) { return false; } } return true; } static void verifyNoErrors(TopDUContext* top, QSet& checked) { const auto problems = top->problems(); if (containsErrors(problems)) { qDebug() << top->url() << top->problems(); if (top->url().str().endsWith(QLatin1String("xmmintrin.h")) && expectedXmmintrinErrors(problems)) { QEXPECT_FAIL("", "there are still some errors in xmmintrin.h b/c some clang provided intrinsincs are more strict than the GCC ones.", Continue); QVERIFY(false); } else { QFAIL("parse error detected"); } } const auto imports = top->importedParentContexts(); for (const auto& import : imports) { auto ctx = import.context(top); QVERIFY(ctx); auto importedTop = ctx->topContext(); if (checked.contains(importedTop)) { continue; } checked.insert(importedTop); verifyNoErrors(importedTop, checked); } } void TestDUChain::testFriendDeclaration() { TestFile file(QStringLiteral(R"( struct FriendFoo { friend class FriendBar; }; class FriendBar{}; FriendBar friendBar; )"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(1000)); { DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 3); auto friendBar = file.topContext()->localDeclarations()[1]; if (CINDEX_VERSION_MINOR < 37) { QEXPECT_FAIL("", "Your clang version is too old", Abort); } QCOMPARE(friendBar->uses().size(), 1); QCOMPARE(friendBar->uses().begin()->first(), RangeInRevision(3,25,3,34)); QCOMPARE(friendBar->uses().begin()->last(), RangeInRevision(8,8,8,17)); } } void TestDUChain::testVariadicTemplateArguments() { TestFile file(QStringLiteral(R"( template class VariadicTemplate {}; VariadicTemplate variadic; )"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(1000)); { DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto decl = file.topContext()->localDeclarations()[1]; QVERIFY(decl); if (CINDEX_VERSION_MINOR < 37) { QEXPECT_FAIL("", "Your clang version is too old", Abort); } QCOMPARE(decl->toString(), QStringLiteral("VariadicTemplate< int, double, bool > variadic")); QVERIFY(decl->abstractType()); QCOMPARE(decl->abstractType()->toString(), QStringLiteral("VariadicTemplate< int, double, bool >")); } } void TestDUChain::testProblemRequestedHere() { auto headerCode = QStringLiteral(R"( #pragma once template T AddObjects(const T& a, const T& b) { return a + b; } )"); TestFile header(headerCode, QStringLiteral("h")); QString sourceCode = QStringLiteral(R"( #include "%1" struct A {}; int main() { A a, b; AddObjects(a, b); return 0; } )").arg(header.url().str()); TestFile impl(sourceCode, QStringLiteral("cpp"), &header); QVERIFY(impl.parseAndWait(TopDUContext::AllDeclarationsContextsAndUses)); DUChainReadLocker lock; auto top = impl.topContext(); QVERIFY(top); auto* headerCtx = dynamic_cast(top->importedParentContexts().first().context(top)); QVERIFY(headerCtx); QCOMPARE(headerCtx->url(), header.url()); QCOMPARE(headerCtx->problems().count(), 1); QCOMPARE(headerCtx->localDeclarations().count(), 1); // Verify that the problem is reported for the source file. QCOMPARE(top->problems().count(), 1); QCOMPARE(top->localDeclarations().count(), 2); } void TestDUChain::testProblemRequestedHereSameFile() { auto sourceCode = QStringLiteral(R"( struct A {}; template T AddObjects(const T& a, const T& b) { return a + b; } int main() { A a, b; AddObjects(a, b); return 0; } )"); TestFile impl(sourceCode, QStringLiteral("cpp")); QVERIFY(impl.parseAndWait(TopDUContext::AllDeclarationsContextsAndUses)); DUChainReadLocker lock; auto top = impl.topContext(); QVERIFY(top); QCOMPARE(top->problems().count(), 2); } void TestDUChain::testProblemRequestedHereChain() { auto headerCode = QStringLiteral(R"( #pragma once template T AddObjects(const T& a, const T& b) { return a + b; } )"); TestFile header(headerCode, QStringLiteral("h")); QString sourceCode = QStringLiteral(R"( #include "%1" struct A {}; template T AddObjects2(const T& a, const T& b) { return AddObjects(a, b); } int main() { A a, b; AddObjects2(a, b); return 0; } )").arg(header.url().str()); TestFile impl(sourceCode, QStringLiteral("cpp"), &header); QVERIFY(impl.parseAndWait(TopDUContext::AllDeclarationsContextsAndUses)); DUChainReadLocker lock; auto top = impl.topContext(); QVERIFY(top); auto* headerCtx = dynamic_cast(top->importedParentContexts().first().context(top)); QVERIFY(headerCtx); QCOMPARE(headerCtx->url(), header.url()); QCOMPARE(headerCtx->problems().count(), 1); QCOMPARE(headerCtx->localDeclarations().count(), 1); // Verify that the problem is reported for the source file. QCOMPARE(top->problems().count(), 2); QCOMPARE(top->localDeclarations().count(), 3); } void TestDUChain::testGccCompatibility() { // TODO: make it easier to change the compiler provider for testing purposes QTemporaryDir dir; auto project = new TestProject(Path(dir.path()), this); auto definesAndIncludesConfig = project->projectConfiguration()->group("CustomDefinesAndIncludes"); auto pathConfig = definesAndIncludesConfig.group("ProjectPath0"); pathConfig.writeEntry("Path", "."); pathConfig.group("Compiler").writeEntry("Name", "GCC"); m_projectController->addProject(project); { // TODO: Also test in C mode. Currently it doesn't work (some intrinsics missing?) TestFile file(QStringLiteral(R"( #include int main() { return 0; } )"), QStringLiteral("cpp"), project, dir.path()); file.parse(); QVERIFY(file.waitForParsed(50000)); DUChainReadLocker lock; QSet checked; verifyNoErrors(file.topContext(), checked); } m_projectController->closeAllProjects(); } void TestDUChain::testLambda() { TestFile file(QStringLiteral("auto lambda = [](int p1, int p2, int p3) { int var1, var2; };"), QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); { DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 1); QCOMPARE(file.topContext()->childContexts().size(), 1); auto lambdaContext = file.topContext()->childContexts().first(); QCOMPARE(lambdaContext->type(), DUContext::Function); QCOMPARE(lambdaContext->localDeclarations().size(), 3); QCOMPARE(lambdaContext->childContexts().size(), 1); QCOMPARE(lambdaContext->childContexts().first()->type(), DUContext::Other); QCOMPARE(lambdaContext->childContexts().first()->localDeclarations().size(), 2); } } void TestDUChain::testQtIntegration() { QTemporaryDir includeDir; { QDir dir(includeDir.path()); dir.mkdir(QStringLiteral("QtCore")); // create the file but don't put anything in it QFile header(includeDir.path() + "/QtCore/qobjectdefs.h"); QVERIFY(header.open(QIODevice::WriteOnly | QIODevice::Text)); } QTemporaryDir dir; auto project = new TestProject(Path(dir.path()), this); m_provider->defines.clear(); m_provider->includes = {Path(includeDir.path() + "/QtCore")}; m_projectController->addProject(project); { TestFile file(QStringLiteral(R"( #define slots #define signals #define Q_SLOTS #define Q_SIGNALS #include struct MyObject { public: void other1(); public slots: void slot1(); signals: void signal1(); private Q_SLOTS: void slot2(); Q_SIGNALS: void signal2(); public: void other2(); }; )"), QStringLiteral("cpp"), project, dir.path()); file.parse(); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; auto top = file.topContext(); QVERIFY(top); QVERIFY(top->problems().isEmpty()); const auto methods = top->childContexts().last()->localDeclarations(); QCOMPARE(methods.size(), 6); for (auto method : methods) { auto classFunction = dynamic_cast(method); QVERIFY(classFunction); auto id = classFunction->identifier().toString(); QCOMPARE(classFunction->isSignal(), id.startsWith(QLatin1String("signal"))); QCOMPARE(classFunction->isSlot(), id.startsWith(QLatin1String("slot"))); } } m_projectController->closeAllProjects(); } void TestDUChain::testHasInclude() { TestFile header(QStringLiteral(R"( #pragma once #if __has_include_next() // good #else #error broken c++11 setup (__has_include_next) #endif )"), QStringLiteral("h")); // NOTE: header is _not_ explicitly being parsed, instead the impl job does that TestFile file(QStringLiteral(R"( #if __has_include() // good #else #error broken c++11 setup (__has_include) #endif #include "%1" )").arg(header.url().str()), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(1000)); { DUChainDumper dumper{DUChainDumper::DumpProblems}; DUChainReadLocker lock; QVERIFY(file.topContext()); dumper.dump(file.topContext()); QVERIFY(file.topContext()->problems().isEmpty()); auto headerCtx = DUChain::self()->chainForDocument(header.url()); QVERIFY(headerCtx); dumper.dump(headerCtx); QVERIFY(headerCtx->problems().count() <= 1); const auto& headerProblems = headerCtx->problems(); for (const auto& problem : headerProblems) { // ignore the following error: "#include_next with absolute path [-Winclude-next-absolute-path]" "" [ (2, 12) -> (2, 30) ] QVERIFY(problem->description().contains(QLatin1String("-Winclude-next-absolute-path"))); } } } void TestDUChain::testSameFunctionDefinition() { QString source(QStringLiteral(R"( #include #include void configure() { printf("do stuff\n"); } )")); QTemporaryDir dir; auto project = new TestProject(Path(dir.path()), this); m_projectController->addProject(project); TestFile file1(source, QStringLiteral("c"), project); TestFile file2(source, QStringLiteral("c"), project); TestFile file3(source, QStringLiteral("c"), project); file1.parse(TopDUContext::AllDeclarationsContextsAndUses); file2.parse(TopDUContext::AllDeclarationsContextsAndUses); file3.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file1.waitForParsed(1000)); QVERIFY(file2.waitForParsed(1000)); QVERIFY(file3.waitForParsed(1000)); auto checkFunctionDefinition = [] (TestFile & file) { DUChainReadLocker lock; QCOMPARE(file.topContext()->localDeclarations().count(), 1); auto configureFunc = file.topContext()->localDeclarations().first(); QCOMPARE(FunctionDefinition::definition(configureFunc), configureFunc); }; checkFunctionDefinition(file1); checkFunctionDefinition(file2); checkFunctionDefinition(file3); m_projectController->closeAllProjects(); } diff --git a/plugins/clazy/utils.cpp b/plugins/clazy/utils.cpp index 1785038ffb..ba2b3897d4 100644 --- a/plugins/clazy/utils.cpp +++ b/plugins/clazy/utils.cpp @@ -1,201 +1,201 @@ /* This file is part of KDevelop Copyright 2018 Anton Anikin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "utils.h" // KDevPlatform #include #include #include // KF #include // Qt #include #include #include namespace Clazy { QString prettyPathName(const QUrl& path) { return KDevelop::ICore::self()->projectController()->prettyFileName(path, KDevelop::IProjectController::FormatPlain); } // Very simple Markdown parser/converter. Does not provide full Markdown language support and // was tested only with Clazy documentation. class MarkdownConverter { public: MarkdownConverter() { tagStart.resize(STATE_COUNT); tagEnd.resize(STATE_COUNT); tagStart[EMPTY].clear(); tagEnd [EMPTY].clear(); tagStart[HEADING] = QStringLiteral(""); tagEnd [HEADING] = QStringLiteral(""); tagStart[PARAGRAPH] = QStringLiteral("

"); tagEnd [PARAGRAPH] = QStringLiteral("

"); tagStart[PREFORMATTED] = QStringLiteral("
");
         tagEnd  [PREFORMATTED] = QStringLiteral("
"); tagStart[LIST] = QStringLiteral("
  • "); tagEnd [LIST] = QStringLiteral("
"); } ~MarkdownConverter() = default; QString toHtml(const QString& markdown) { const QRegularExpression hRE(QStringLiteral("(#+) (.+)")); QRegularExpressionMatch match; state = EMPTY; html.clear(); html += QStringLiteral(""); - auto lines = markdown.split(QLatin1Char('\n')); + const auto lines = markdown.split(QLatin1Char('\n')); for (auto line : lines) { if (line.isEmpty()) { setState(EMPTY); continue; } if (line.startsWith(QLatin1Char('#'))) { auto match = hRE.match(line); if (match.hasMatch()) { setState(HEADING); html += match.captured(2); setState(EMPTY); if (match.capturedRef(1).size() == 1) { html += QStringLiteral("
"); } } continue; } if (line.startsWith(QLatin1String("```"))) { setState((state == PREFORMATTED) ? EMPTY : PREFORMATTED); continue; } if (line.startsWith(QLatin1String(" "))) { if (state == EMPTY) { setState(PREFORMATTED); } } else if ( line.startsWith(QLatin1String("- ")) || line.startsWith(QLatin1String("* "))) { // force close and reopen list - this fixes cases when we don't have // separator line between items setState(EMPTY); setState(LIST); line.remove(0, 2); } if (state == EMPTY) { setState(PARAGRAPH); } processLine(line); } setState(EMPTY); html += QStringLiteral(""); return html.join(QLatin1Char('\n')); } private: enum STATE { EMPTY, HEADING, PARAGRAPH, PREFORMATTED, LIST, STATE_COUNT }; void setState(int newState) { if (state == newState) { return; } if (state != EMPTY) { html += tagEnd[state]; } if (newState != EMPTY) { html += tagStart[newState]; } state = newState; } void processLine(QString& line) { static const QRegularExpression ttRE(QStringLiteral("`([^`]+)`")); static const QRegularExpression bdRE(QStringLiteral("\\*\\*([^\\*]+)\\*\\*")); static const QRegularExpression itRE(QStringLiteral("[^\\*]\\*([^\\*]+)\\*[^\\*]")); static auto applyRE = [](const QRegularExpression& re, QString& line, const QString& tag) { auto i = re.globalMatch(line); while (i.hasNext()) { auto match = i.next(); line.replace(match.captured(0), QStringLiteral("<%1>%2").arg(tag, match.captured(1))); } }; if (state != PREFORMATTED) { line.replace(QLatin1Char('&'), QLatin1String("&")); line.replace(QLatin1Char('<'), QLatin1String("<")); line.replace(QLatin1Char('>'), QLatin1String(">")); line.replace(QLatin1Char('\"'), QLatin1String(""")); line.replace(QLatin1Char('\''), QLatin1String("'")); applyRE(ttRE, line, QStringLiteral("tt")); applyRE(bdRE, line, QStringLiteral("b")); applyRE(itRE, line, QStringLiteral("i")); } html += line; } private: int state; QVector tagStart; QVector tagEnd; QStringList html; }; QString markdown2html(const QByteArray& markdown) { MarkdownConverter converter; return converter.toHtml(QString::fromUtf8(markdown)); } } diff --git a/plugins/custom-definesandincludes/compilerprovider/compilerprovider.cpp b/plugins/custom-definesandincludes/compilerprovider/compilerprovider.cpp index 05fc6c9dee..e1e74816b6 100644 --- a/plugins/custom-definesandincludes/compilerprovider/compilerprovider.cpp +++ b/plugins/custom-definesandincludes/compilerprovider/compilerprovider.cpp @@ -1,330 +1,330 @@ /* * This file is part of KDevelop * * Copyright 2014 Sergey Kalinichev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "compilerprovider.h" #include "debug.h" #include "compilerfactories.h" #include "settingsmanager.h" #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace { class NoCompiler : public ICompiler { public: NoCompiler(): ICompiler(i18n("None"), QString(), QString(), false) {} QHash< QString, QString > defines(Utils::LanguageType, const QString&) const override { return {}; } Path::List includes(Utils::LanguageType, const QString&) const override { return {}; } }; static CompilerPointer createDummyCompiler() { static CompilerPointer compiler(new NoCompiler()); return compiler; } ConfigEntry configForItem(KDevelop::ProjectBaseItem* item) { if(!item){ return ConfigEntry(); } const Path itemPath = item->path(); const Path rootDirectory = item->project()->path(); - auto paths = SettingsManager::globalInstance()->readPaths(item->project()->projectConfiguration().data()); + const auto paths = SettingsManager::globalInstance()->readPaths(item->project()->projectConfiguration().data()); ConfigEntry config; Path closestPath; // find config entry closest to the requested item for (const auto& entry : paths) { auto configEntry = entry; Path targetDirectory = rootDirectory; targetDirectory.addPath(entry.path); if (targetDirectory == itemPath) { return configEntry; } if (targetDirectory.isParentOf(itemPath)) { if (config.path.isEmpty() || targetDirectory.segments().size() > closestPath.segments().size()) { config = configEntry; closestPath = targetDirectory; } } } return config; } } ProjectTargetItem* findCompiledTarget(ProjectBaseItem* item) { const auto targets = item->targetList(); for (auto* item : targets) { if (item->type() == ProjectBaseItem::ExecutableTarget || item->type() == ProjectBaseItem::LibraryTarget) { return item; } } const auto folders = item->folderList(); for (auto* folder: folders) { auto target = findCompiledTarget(folder); if (target) return target; } return nullptr; } CompilerProvider::CompilerProvider( SettingsManager* settings, QObject* parent ) : QObject( parent ) , m_settings(settings) { m_factories = { CompilerFactoryPointer(new GccFactory()), CompilerFactoryPointer(new ClangFactory()), #ifdef _WIN32 CompilerFactoryPointer(new MsvcFactory()), #endif }; if (!QStandardPaths::findExecutable( QStringLiteral("clang") ).isEmpty()) { m_factories[1]->registerDefaultCompilers(this); } if (!QStandardPaths::findExecutable( QStringLiteral("gcc") ).isEmpty()) { m_factories[0]->registerDefaultCompilers(this); } #ifdef _WIN32 if (!QStandardPaths::findExecutable(QStringLiteral("cl.exe")).isEmpty()) { m_factories[2]->registerDefaultCompilers(this); } #endif registerCompiler(createDummyCompiler()); retrieveUserDefinedCompilers(); connect(ICore::self()->runtimeController(), &IRuntimeController::currentRuntimeChanged, this, [this]() { m_defaultProvider.clear(); }); connect(ICore::self()->projectController(), &IProjectController::projectConfigurationChanged, this, &CompilerProvider::projectChanged); connect(ICore::self()->projectController(), &IProjectController::projectOpened, this, &CompilerProvider::projectChanged); } CompilerProvider::~CompilerProvider() = default; void CompilerProvider::projectChanged(KDevelop::IProject* p) { const auto target = findCompiledTarget(p->projectItem()); if (!target) return; auto path = p->buildSystemManager()->compiler(target); qCDebug(DEFINESANDINCLUDES) << "found compiler" << path; if (path.isEmpty()) return; Q_ASSERT(QDir::isAbsolutePath(path.toLocalFile())); const auto pathString = path.toLocalFile(); auto it = std::find_if(m_compilers.begin(), m_compilers.end(), [&pathString](const CompilerPointer& compiler) { return compiler->path() == pathString; }); if (it != m_compilers.end()) { m_defaultProvider = *it; return; } //we need to search, sdk compiler names are weird: arm-linux-androideabi-g++ for (auto& factory : qAsConst(m_factories)) { if (factory->isSupported(path)) { auto compiler = factory->createCompiler(path.lastPathSegment(), pathString); registerCompiler(compiler); m_defaultProvider = compiler; } } qCDebug(DEFINESANDINCLUDES) << "using compiler" << m_defaultProvider << path; } QHash CompilerProvider::defines( const QString& path ) const { auto config = configForItem(nullptr); auto languageType = Utils::languageType(path, config.parserArguments.parseAmbiguousAsCPP); // If called on files that we can't compile, return an empty set of defines. if (languageType == Utils::Other) { return {}; } return config.compiler->defines(languageType, config.parserArguments[languageType]); } QHash CompilerProvider::defines( ProjectBaseItem* item ) const { auto config = configForItem(item); auto languageType = Utils::Cpp; if (item) { languageType = Utils::languageType(item->path().path(), config.parserArguments.parseAmbiguousAsCPP); } // If called on files that we can't compile, return an empty set of defines. if (languageType == Utils::Other) { return {}; } return config.compiler->defines(languageType, config.parserArguments[languageType]); } Path::List CompilerProvider::includes( const QString& path ) const { auto config = configForItem(nullptr); auto languageType = Utils::languageType(path, config.parserArguments.parseAmbiguousAsCPP); // If called on files that we can't compile, return an empty set of includes. if (languageType == Utils::Other) { return {}; } return config.compiler->includes(languageType, config.parserArguments[languageType]); } Path::List CompilerProvider::includes( ProjectBaseItem* item ) const { auto config = configForItem(item); auto languageType = Utils::Cpp; if (item) { languageType = Utils::languageType(item->path().path(), config.parserArguments.parseAmbiguousAsCPP); } // If called on files that we can't compile, return an empty set of includes. if (languageType == Utils::Other) { return {}; } return config.compiler->includes(languageType, config.parserArguments[languageType]); } Path::List CompilerProvider::frameworkDirectories( const QString& /* path */ ) const { return {}; } Path::List CompilerProvider::frameworkDirectories( ProjectBaseItem* /* item */ ) const { return {}; } IDefinesAndIncludesManager::Type CompilerProvider::type() const { return IDefinesAndIncludesManager::CompilerSpecific; } CompilerPointer CompilerProvider::defaultCompiler() const { if (m_defaultProvider) return m_defaultProvider; auto rt = ICore::self()->runtimeController()->currentRuntime(); for ( const CompilerPointer& compiler : m_compilers ) { if (rt->findExecutable(compiler->path()).isEmpty()) continue; m_defaultProvider = compiler; break; } if (!m_defaultProvider) m_defaultProvider = createDummyCompiler(); qCDebug(DEFINESANDINCLUDES) << "new default compiler" << rt->name() << m_defaultProvider->name() << m_defaultProvider->path(); return m_defaultProvider; } QVector< CompilerPointer > CompilerProvider::compilers() const { return m_compilers; } CompilerPointer CompilerProvider::compilerForItem( KDevelop::ProjectBaseItem* item ) const { auto compiler = configForItem(item).compiler; Q_ASSERT(compiler); return compiler; } bool CompilerProvider::registerCompiler(const CompilerPointer& compiler) { if (!compiler) { return false; } for (auto& c : qAsConst(m_compilers)) { if (c->name() == compiler->name()) { return false; } } m_compilers.append(compiler); return true; } void CompilerProvider::unregisterCompiler(const CompilerPointer& compiler) { if (!compiler->editable()) { return; } for (int i = 0; i < m_compilers.count(); i++) { if (m_compilers[i]->name() == compiler->name()) { m_compilers.remove(i); break; } } } QVector< CompilerFactoryPointer > CompilerProvider::compilerFactories() const { return m_factories; } void CompilerProvider::retrieveUserDefinedCompilers() { const auto compilers = m_settings->userDefinedCompilers(); for (auto& c : compilers) { registerCompiler(c); } } diff --git a/plugins/custom-definesandincludes/includepathsconverter.cpp b/plugins/custom-definesandincludes/includepathsconverter.cpp index 2be3d71eca..7cca756d0f 100644 --- a/plugins/custom-definesandincludes/includepathsconverter.cpp +++ b/plugins/custom-definesandincludes/includepathsconverter.cpp @@ -1,246 +1,246 @@ /* * This file is part of KDevelop * * Copyright 2014 Sergey Kalinichev * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * */ #include "includepathsconverter.h" #include #include #include #include #include #include #include #include "settingsmanager.h" using namespace KDevelop; namespace { KSharedConfigPtr openConfigFile(const QString& configFile) { return KSharedConfig::openConfig(configFile, KConfig::SimpleConfig); } QString findconfigFile(const QString& projectDir) { QDirIterator dirIterator(projectDir + QLatin1String("/.kdev4")); while (dirIterator.hasNext()) { dirIterator.next(); if (dirIterator.fileName().endsWith(QLatin1String(".kdev4"))) { return dirIterator.fileInfo().canonicalFilePath(); } } return {}; } QString findProject(const QString& subdirectory) { QDir project(subdirectory); do { if (project.exists(QStringLiteral(".kdev4"))) { return project.path(); } } while(project.cdUp()); return {}; } } IncludePathsConverter::IncludePathsConverter() { } bool IncludePathsConverter::addIncludePaths(const QStringList& includeDirectories, const QString& projectConfigFile, const QString& subdirectory) { auto configFile = openConfigFile(projectConfigFile); if (!configFile) { return false; } auto configEntries = SettingsManager::globalInstance()->readPaths(configFile.data()); QString path = subdirectory.isEmpty() ? QStringLiteral(".") : subdirectory; ConfigEntry config; for (auto& entry: configEntries) { if (path == entry.path) { config = entry; config.includes += includeDirectories; config.includes.removeDuplicates(); entry = config; break; } } if (config.path.isEmpty()) { config.path = path; config.includes = includeDirectories; configEntries.append(config); } SettingsManager::globalInstance()->writePaths(configFile.data(), configEntries); return true; } bool IncludePathsConverter::removeIncludePaths(const QStringList& includeDirectories, const QString& projectConfigFile, const QString& subdirectory) { auto configFile = openConfigFile(projectConfigFile); if (!configFile) { return false; } auto configEntries = SettingsManager::globalInstance()->readPaths(configFile.data()); QString path = subdirectory.isEmpty() ? QStringLiteral(".") : subdirectory; for (auto& entry: configEntries) { if (path == entry.path) { for(const auto& include: includeDirectories) { entry.includes.removeAll(include); } SettingsManager::globalInstance()->writePaths(configFile.data(), configEntries); return true; } } return true; } QStringList IncludePathsConverter::readIncludePaths(const QString& projectConfigFile, const QString& subdirectory) const { auto configFile = openConfigFile(projectConfigFile); if (!configFile) { return {}; } QString path = subdirectory.isEmpty() ? QStringLiteral(".") : subdirectory; - auto configEntries = SettingsManager::globalInstance()->readPaths(configFile.data()); + const auto configEntries = SettingsManager::globalInstance()->readPaths(configFile.data()); for (const auto& entry: configEntries) { if (path == entry.path) { return entry.includes; } } return {}; } int main(int argc, char** argv) { QCoreApplication app(argc, argv); QCoreApplication::setApplicationName(QStringLiteral("kdev_includepathsconverter")); QCommandLineParser parser; parser.setApplicationDescription(QStringLiteral("\nAdds, removes or shows include directories of a project. Also it can be used as a tool to convert include directories from .kdev_include_paths file to the new format.\n\n" "Examples:\ncat /project/path/.kdev_include_paths | xargs -d '\\n' kdev_includepathsconverter -a /project/path/\n\n" "kdev_includepathsconverter -r /project/path/subdirectory/ \"/some/include/dir\" \"/another/include/dir\" \n\n" "kdev_includepathsconverter -l /project/path/another/subdirectory/")); parser.addHelpOption(); QCommandLineOption listOption(QStringLiteral("l"), QCoreApplication::translate("main", "Shows include directories used by the project"), QCoreApplication::translate("main", "project")); parser.addOption(listOption); QCommandLineOption addOption(QStringLiteral("a"), QCoreApplication::translate("main", "Adds include directories to the project"), QCoreApplication::translate("main", "project")); parser.addOption(addOption); QCommandLineOption removeOption(QStringLiteral("r"), QCoreApplication::translate("main", "Removes include directories from the project"), QCoreApplication::translate("main", "project")); parser.addOption(removeOption); parser.process(app); QString projectDir; QStringList includeDirectories(parser.positionalArguments()); std::transform(includeDirectories.begin(), includeDirectories.end(), includeDirectories.begin(), [](const QString& path) { return path.trimmed(); } ); includeDirectories.erase(std::remove_if(includeDirectories.begin(), includeDirectories.end(), [](const QString& path) { return path.isEmpty(); } ), includeDirectories.end()); bool show = parser.isSet(listOption); bool add = parser.isSet(addOption); bool remove = parser.isSet(removeOption); if (show) { projectDir = parser.value(listOption); } else if(add) { projectDir = parser.value(addOption); } else if(remove) { projectDir = parser.value(removeOption); } if (projectDir.isEmpty()) { parser.showHelp(-1); } QString subdirectory = projectDir; projectDir = findProject(projectDir); QString configFile = findconfigFile(projectDir); QTextStream out(stdout); if (configFile.isEmpty()) { out << QCoreApplication::translate("main", "No project found for: ") << subdirectory; return -1; } if (add || remove) { if (includeDirectories.isEmpty()) { parser.showHelp(-1); } } { auto subdirCanonical = QFileInfo(subdirectory).canonicalFilePath(); auto projectCanonical = QFileInfo(projectDir).canonicalFilePath(); if (subdirCanonical != projectCanonical) { subdirectory = subdirCanonical.mid(projectCanonical.size()); if (subdirectory.startsWith(QLatin1Char('/'))) { subdirectory.remove(0,1); } } else { subdirectory.clear(); } } IncludePathsConverter converter; if (remove) { if (!converter.removeIncludePaths(includeDirectories, configFile, subdirectory)) { out << QCoreApplication::translate("main", "Can't remove include paths"); } } if (add) { if (!converter.addIncludePaths(includeDirectories, configFile, subdirectory)) { out << QCoreApplication::translate("main", "Can't add include paths"); } } if (show) { const auto& includes = converter.readIncludePaths(configFile, subdirectory); for (const auto& include : includes) { out << include << "\n"; } } } diff --git a/plugins/custom-definesandincludes/noprojectincludesanddefines/noprojectincludepathsmanager.cpp b/plugins/custom-definesandincludes/noprojectincludesanddefines/noprojectincludepathsmanager.cpp index e72d06f2cd..e0e6741f76 100644 --- a/plugins/custom-definesandincludes/noprojectincludesanddefines/noprojectincludepathsmanager.cpp +++ b/plugins/custom-definesandincludes/noprojectincludesanddefines/noprojectincludepathsmanager.cpp @@ -1,157 +1,157 @@ /* * This file is part of KDevelop * * Copyright 2014 Sergey Kalinichev * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "noprojectincludepathsmanager.h" #include #include #include #include #include #include #include #include #include "noprojectcustomincludepaths.h" namespace { inline QString includePathsFile() { return QStringLiteral(".kdev_include_paths"); } bool removeSettings(const QString& storageDirectory) { const QString file = storageDirectory + QDir::separator() + includePathsFile(); return QFile::remove(file); } QStringList pathListToStringList(const Path::List& paths) { QStringList sl; sl.reserve(paths.size()); for (const auto& p : paths) { sl << p.path(); } return sl; } } QString NoProjectIncludePathsManager::findConfigurationFile(const QString& path) { QDir dir(path); while (dir.exists()) { QFileInfo customIncludePathsFile(dir, includePathsFile()); if (customIncludePathsFile.exists()) { return customIncludePathsFile.absoluteFilePath(); } if (!dir.cdUp()) { break; } } return {}; } std::pair> NoProjectIncludePathsManager::includesAndDefines(const QString& path) { QFileInfo fi(path); auto pathToFile = findConfigurationFile(fi.absoluteDir().absolutePath()); if (pathToFile.isEmpty()) { return {}; } Path::List includes; QHash defines; QFile f(pathToFile); if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { - auto lines = QString::fromLocal8Bit(f.readAll()).split(QLatin1Char('\n'), QString::SkipEmptyParts); + const auto lines = QString::fromLocal8Bit(f.readAll()).split(QLatin1Char('\n'), QString::SkipEmptyParts); QFileInfo dir(pathToFile); const QChar dirSeparator = QDir::separator(); for (const auto& line : lines) { auto textLine = line.trimmed(); if (textLine.startsWith(QLatin1String("#define "))) { QStringList items = textLine.split(QLatin1Char(' ')); if (items.length() > 1) { defines[items[1]] = QStringList(items.mid(2)).join(QLatin1Char(' ')); }else{ qWarning() << i18n("Bad #define directive in %1: %2", pathToFile, textLine); } continue; } if (!textLine.isEmpty()) { QFileInfo pathInfo(textLine); if (pathInfo.isRelative()) { includes << Path(dir.canonicalPath() + dirSeparator + textLine); } else { includes << Path(textLine); } } } f.close(); } return std::make_pair(includes, defines); } bool NoProjectIncludePathsManager::writeIncludePaths(const QString& storageDirectory, const QStringList& includePaths) { QDir dir(storageDirectory); QFileInfo customIncludePaths(dir, includePathsFile()); QFile f(customIncludePaths.filePath()); if (f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { QTextStream out(&f); for (const auto& customPath : includePaths) { out << customPath << QLatin1Char('\n'); } if (includePaths.isEmpty()) { removeSettings(storageDirectory); } return true; } else { return false; } } void NoProjectIncludePathsManager::openConfigurationDialog(const QString& path) { auto cip = new NoProjectCustomIncludePaths; cip->setAttribute(Qt::WA_DeleteOnClose); cip->setModal(true); QFileInfo fi(path); auto dir = fi.absoluteDir().absolutePath(); cip->setStorageDirectory(dir); auto paths = includesAndDefines(path).first; cip->setCustomIncludePaths(pathListToStringList(paths)); QObject::connect(cip, &QDialog::accepted, cip, [this, cip, &path]() { if (!writeIncludePaths(cip->storageDirectory(), cip->customIncludePaths())) { qWarning() << i18n("Failed to save custom include paths in directory: %1", cip->storageDirectory()); } KDevelop::ICore::self()->languageController()->backgroundParser()->addDocument(KDevelop::IndexedString(path)); }); } diff --git a/plugins/flatpak/flatpakruntime.cpp b/plugins/flatpak/flatpakruntime.cpp index 4c21aa14a7..c33525b952 100644 --- a/plugins/flatpak/flatpakruntime.cpp +++ b/plugins/flatpak/flatpakruntime.cpp @@ -1,269 +1,271 @@ /* Copyright 2017 Aleix Pol Gonzalez This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "flatpakruntime.h" #include "flatpakplugin.h" #include "debug_flatpak.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; template static T kTransform(const Q& list, W func) { T ret; ret.reserve(list.size()); for (auto it = list.constBegin(), itEnd = list.constEnd(); it!=itEnd; ++it) ret += func(*it); return ret; } static KJob* createExecuteJob(const QStringList &program, const QString &title, const QUrl &wd = {}, bool checkExitCode = true) { auto* process = new OutputExecuteJob; process->setProperties(OutputExecuteJob::DisplayStdout | OutputExecuteJob::DisplayStderr); process->setExecuteOnHost(true); process->setJobName(title); process->setWorkingDirectory(wd); process->setCheckExitCode(checkExitCode); *process << program; return process; } KJob* FlatpakRuntime::createBuildDirectory(const KDevelop::Path &buildDirectory, const KDevelop::Path &file, const QString &arch) { return createExecuteJob(QStringList{QStringLiteral("flatpak-builder"), QLatin1String("--arch=")+arch, QStringLiteral("--build-only"), buildDirectory.toLocalFile(), file.toLocalFile() }, i18n("Flatpak"), file.parent().toUrl()); } FlatpakRuntime::FlatpakRuntime(const KDevelop::Path &buildDirectory, const KDevelop::Path &file, const QString &arch) : KDevelop::IRuntime() , m_file(file) , m_buildDirectory(buildDirectory) , m_arch(arch) { refreshJson(); } FlatpakRuntime::~FlatpakRuntime() { } void FlatpakRuntime::refreshJson() { const auto doc = config(); const QString sdkName = doc[QLatin1String("sdk")].toString(); const QString runtimeVersion = doc.value(QLatin1String("runtime-version")).toString(); const QString usedRuntime = sdkName + QLatin1Char('/') + m_arch + QLatin1Char('/') + runtimeVersion; m_sdkPath = KDevelop::Path(QLatin1String("/var/lib/flatpak/runtime/") + usedRuntime + QLatin1String("/active/files")); qCDebug(FLATPAK) << "flatpak runtime path..." << name() << m_sdkPath; Q_ASSERT(QFile::exists(m_sdkPath.toLocalFile())); m_finishArgs = kTransform(doc[QLatin1String("finish-args")].toArray(), [](const QJsonValue& val){ return val.toString(); }); } void FlatpakRuntime::setEnabled(bool /*enable*/) { } void FlatpakRuntime::startProcess(QProcess* process) const { //Take any environment variables specified in process and pass through to flatpak. QStringList env_args; - for(QString env_var: process->processEnvironment().toStringList()) { + const QStringList env_vars = process->processEnvironment().toStringList(); + for (const QString& env_var : env_vars) { env_args << QLatin1String("--env=") + env_var; } const QStringList args = m_finishArgs + env_args + QStringList{QStringLiteral("build"), QStringLiteral("--talk-name=org.freedesktop.DBus"), m_buildDirectory.toLocalFile(), process->program()} << process->arguments(); process->setProgram(QStringLiteral("flatpak")); process->setArguments(args); qCDebug(FLATPAK) << "starting qprocess" << process->program() << process->arguments(); process->start(); } void FlatpakRuntime::startProcess(KProcess* process) const { //Take any environment variables specified in process and pass through to flatpak. QStringList env_args; - for(QString env_var: process->processEnvironment().toStringList()) { + const QStringList env_vars = process->processEnvironment().toStringList(); + for (const QString& env_var : env_vars) { env_args << QLatin1String("--env=") + env_var; } process->setProgram(QStringList{QStringLiteral("flatpak")} << m_finishArgs << env_args << QStringList{QStringLiteral("build"), QStringLiteral("--talk-name=org.freedesktop.DBus"), m_buildDirectory.toLocalFile() } << process->program()); qCDebug(FLATPAK) << "starting kprocess" << process->program().join(QLatin1Char(' ')); process->start(); } KJob* FlatpakRuntime::rebuild() { QDir(m_buildDirectory.toLocalFile()).removeRecursively(); auto job = createBuildDirectory(m_buildDirectory, m_file, m_arch); refreshJson(); return job; } QList FlatpakRuntime::exportBundle(const QString &path) const { const auto doc = config(); QTemporaryDir* dir = new QTemporaryDir(QDir::tempPath()+QLatin1String("/flatpak-tmp-repo")); if (!dir->isValid() || doc.isEmpty()) { qCWarning(FLATPAK) << "Couldn't export:" << path << dir->isValid() << dir->path() << doc.isEmpty(); return {}; } const QString name = doc[QLatin1String("id")].toString(); QStringList args = m_finishArgs; if (doc.contains(QLatin1String("command"))) args << QLatin1String("--command=")+doc[QLatin1String("command")].toString(); const QString title = i18n("Bundling"); const QList jobs = { createExecuteJob(QStringList{QStringLiteral("flatpak"), QStringLiteral("build-finish"), m_buildDirectory.toLocalFile()} << args, title, {}, false), createExecuteJob(QStringList{QStringLiteral("flatpak"), QStringLiteral("build-export"), QLatin1String("--arch=")+m_arch, dir->path(), m_buildDirectory.toLocalFile()}, title), createExecuteJob(QStringList{QStringLiteral("flatpak"), QStringLiteral("build-bundle"), QLatin1String("--arch=")+m_arch, dir->path(), path, name }, title) }; connect(jobs.last(), &QObject::destroyed, jobs.last(), [dir]() { delete dir; }); return jobs; } QString FlatpakRuntime::name() const { return QStringLiteral("%1 - %2").arg(m_arch, m_file.lastPathSegment()); } KJob * FlatpakRuntime::executeOnDevice(const QString& host, const QString &path) const { const QString name = config()[QLatin1String("id")].toString(); const QString destPath = QStringLiteral("/tmp/kdevelop-test-app.flatpak"); const QString replicatePath = QStringLiteral("/tmp/replicate.sh"); const QString localReplicatePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("kdevflatpak/replicate.sh")); const QString title = i18n("Run on Device"); const QList jobs = exportBundle(path) << QList { createExecuteJob({QStringLiteral("scp"), path, host+QLatin1Char(':')+destPath}, title), createExecuteJob({QStringLiteral("scp"), localReplicatePath, host+QLatin1Char(':')+replicatePath}, title), createExecuteJob({QStringLiteral("ssh"), host, QStringLiteral("flatpak"), QStringLiteral("install"), QStringLiteral("--user"), QStringLiteral("--bundle"), QStringLiteral("-y"), destPath}, title), createExecuteJob({QStringLiteral("ssh"), host, QStringLiteral("bash"), replicatePath, QStringLiteral("plasmashell"), QStringLiteral("flatpak"), QStringLiteral("run"), name }, title), }; return new KDevelop::ExecuteCompositeJob( parent(), jobs ); } QJsonObject FlatpakRuntime::config(const KDevelop::Path& path) { QFile f(path.toLocalFile()); if (!f.open(QIODevice::ReadOnly)) { qCWarning(FLATPAK) << "couldn't open" << path; return {}; } QJsonParseError error; auto doc = QJsonDocument::fromJson(f.readAll(), &error); if (error.error) { qCWarning(FLATPAK) << "couldn't parse" << path << error.errorString(); return {}; } return doc.object(); } QJsonObject FlatpakRuntime::config() const { return config(m_file); } Path FlatpakRuntime::pathInHost(const KDevelop::Path& runtimePath) const { KDevelop::Path ret = runtimePath; if (!runtimePath.isLocalFile()) { return ret; } const auto prefix = runtimePath.segments().at(0); if (prefix == QLatin1String("usr")) { const auto relpath = KDevelop::Path(QStringLiteral("/usr")).relativePath(runtimePath); ret = Path(m_sdkPath, relpath); } else if (prefix == QLatin1String("app")) { const auto relpath = KDevelop::Path(QStringLiteral("/app")).relativePath(runtimePath); ret = Path(m_buildDirectory, QLatin1String("/active/files/") + relpath); } qCDebug(FLATPAK) << "path in host" << runtimePath << ret; return ret; } Path FlatpakRuntime::pathInRuntime(const KDevelop::Path& localPath) const { KDevelop::Path ret = localPath; if (m_sdkPath.isParentOf(localPath)) { const auto relpath = m_sdkPath.relativePath(localPath); ret = Path(Path(QStringLiteral("/usr")), relpath); } else { const Path bdfiles(m_buildDirectory, QStringLiteral("/active/files")); if (bdfiles.isParentOf(localPath)) { const auto relpath = bdfiles.relativePath(localPath); ret = Path(Path(QStringLiteral("/app")), relpath); } } qCDebug(FLATPAK) << "path in runtime" << localPath << ret; return ret; } QString FlatpakRuntime::findExecutable(const QString& executableName) const { QStringList rtPaths; auto envPaths = getenv(QByteArrayLiteral("PATH")).split(':'); std::transform(envPaths.begin(), envPaths.end(), std::back_inserter(rtPaths), [this](QByteArray p) { return pathInHost(Path(QString::fromLocal8Bit(p))).toLocalFile(); }); return QStandardPaths::findExecutable(executableName, rtPaths); } QByteArray FlatpakRuntime::getenv(const QByteArray& varname) const { if (varname == "KDEV_DEFAULT_INSTALL_PREFIX") return "/app"; return qgetenv(varname.constData()); } KDevelop::Path FlatpakRuntime::buildPath() const { auto file = m_file; file.setLastPathSegment(QStringLiteral(".flatpak-builder")); file.addPath(QStringLiteral("kdevelop")); return file; } diff --git a/plugins/gdb/unittests/test_gdb.cpp b/plugins/gdb/unittests/test_gdb.cpp index 6f5173fc41..7b8590f061 100644 --- a/plugins/gdb/unittests/test_gdb.cpp +++ b/plugins/gdb/unittests/test_gdb.cpp @@ -1,2122 +1,2122 @@ /* Copyright 2009 Niko Sams Copyright 2013 Vlas Puhov This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "test_gdb.h" #include "debugsession.h" #include "gdbframestackmodel.h" #include "mi/micommand.h" #include "mi/milexer.h" #include "mi/miparser.h" #include "tests/debuggers-tests-config.h" #include "tests/testhelper.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define SKIP_IF_ATTACH_FORBIDDEN() \ do { \ if (KDevMI::isAttachForbidden(__FILE__, __LINE__)) \ return; \ } while(0) using KDevelop::AutoTestShell; using KDevMI::findExecutable; using KDevMI::findSourceFile; using KDevMI::findFile; namespace KDevMI { namespace GDB { void GdbTest::initTestCase() { AutoTestShell::init(); KDevelop::TestCore::initialize(KDevelop::Core::NoUi); m_iface = KDevelop::ICore::self()->pluginController()->pluginForExtension(QStringLiteral("org.kdevelop.IExecutePlugin"), QStringLiteral("kdevexecute"))->extension(); Q_ASSERT(m_iface); } void GdbTest::cleanupTestCase() { KDevelop::TestCore::shutdown(); } void GdbTest::init() { //remove all breakpoints - so we can set our own in the test KConfigGroup breakpoints = KSharedConfig::openConfig()->group("breakpoints"); breakpoints.writeEntry("number", 0); breakpoints.sync(); KDevelop::BreakpointModel* m = KDevelop::ICore::self()->debugController()->breakpointModel(); m->removeRows(0, m->rowCount()); KDevelop::VariableCollection *vc = KDevelop::ICore::self()->debugController()->variableCollection(); for (int i=0; i < vc->watches()->childCount(); ++i) { delete vc->watches()->child(i); } vc->watches()->clear(); } class WritableEnvironmentProfileList : public KDevelop::EnvironmentProfileList { public: explicit WritableEnvironmentProfileList(KConfig* config) : EnvironmentProfileList(config) {} using EnvironmentProfileList::variables; using EnvironmentProfileList::saveSettings; using EnvironmentProfileList::removeProfile; }; class TestLaunchConfiguration : public KDevelop::ILaunchConfiguration { public: explicit TestLaunchConfiguration(const QUrl& executable = findExecutable(QStringLiteral("debuggee_debugee")), const QUrl& workingDirectory = QUrl()) { qDebug() << "FIND" << executable; c = KSharedConfig::openConfig(); c->deleteGroup("launch"); cfg = c->group("launch"); cfg.writeEntry("isExecutable", true); cfg.writeEntry("Executable", executable); cfg.writeEntry("Working Directory", workingDirectory); } const KConfigGroup config() const override { return cfg; } KConfigGroup config() override { return cfg; }; QString name() const override { return QStringLiteral("Test-Launch"); } KDevelop::IProject* project() const override { return nullptr; } KDevelop::LaunchConfigurationType* type() const override { return nullptr; } KConfig* rootConfig() { return c.data(); } private: KConfigGroup cfg; KSharedConfigPtr c; }; class TestFrameStackModel : public GdbFrameStackModel { Q_OBJECT public: explicit TestFrameStackModel(DebugSession* session) : GdbFrameStackModel(session), fetchFramesCalled(0), fetchThreadsCalled(0) {} int fetchFramesCalled; int fetchThreadsCalled; void fetchFrames(int threadNumber, int from, int to) override { fetchFramesCalled++; GdbFrameStackModel::fetchFrames(threadNumber, from, to); } void fetchThreads() override { fetchThreadsCalled++; GdbFrameStackModel::fetchThreads(); } }; class TestDebugSession : public DebugSession { Q_OBJECT public: TestDebugSession() : DebugSession() { setSourceInitFile(false); setAutoDisableASLR(false); m_frameStackModel = new TestFrameStackModel(this); KDevelop::ICore::self()->debugController()->addSession(this); } QUrl url() { return currentUrl(); } int line() { return currentLine(); } TestFrameStackModel* frameStackModel() const override { return m_frameStackModel; } private: TestFrameStackModel* m_frameStackModel; }; class TestWaiter { public: TestWaiter(DebugSession * session_, const char * condition_, const char * file_, int line_) : session(session_) , condition(condition_) , file(file_) , line(line_) { stopWatch.start(); } bool waitUnless(bool ok) { if (ok) { qDebug() << "Condition " << condition << " reached in " << file << ':' << line; return false; } if (stopWatch.elapsed() > 5000) { QTest::qFail(qPrintable(QString("Timeout before reaching condition %0").arg(condition)), file, line); return false; } QTest::qWait(100); if (!session) { QTest::qFail(qPrintable(QString("Session ended without reaching condition %0").arg(condition)), file, line); return false; } return true; } private: QTime stopWatch; QPointer session; const char * condition; const char * file; int line; }; #define WAIT_FOR_STATE(session, state) \ do { if (!waitForState((session), (state), __FILE__, __LINE__)) return; } while (0) #define WAIT_FOR_STATE_AND_IDLE(session, state) \ do { if (!waitForState((session), (state), __FILE__, __LINE__, true)) return; } while (0) #define WAIT_FOR(session, condition) \ do { \ TestWaiter w((session), #condition, __FILE__, __LINE__); \ while (w.waitUnless((condition))) /* nothing */ ; \ } while(0) #define COMPARE_DATA(index, expected) \ do { if(!compareData((index), (expected), __FILE__, __LINE__)) return; } while (0) bool compareData(const QModelIndex& index, const QString& expected, const char *file, int line) { QString s = index.model()->data(index, Qt::DisplayRole).toString(); if (s != expected) { QTest::qFail(qPrintable(QString("'%0' didn't match expected '%1' in %2:%3") .arg(s, expected, file).arg(line)), file, line); return false; } return true; } static const QString debugeeFileName = findSourceFile(QStringLiteral("debugee.cpp")); KDevelop::BreakpointModel* breakpoints() { return KDevelop::ICore::self()->debugController()->breakpointModel(); } void GdbTest::testStdOut() { auto *session = new TestDebugSession; QSignalSpy outputSpy(session, &TestDebugSession::inferiorStdoutLines); TestLaunchConfiguration cfg; session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE(session, KDevelop::IDebugSession::EndedState); { QCOMPARE(outputSpy.count(), 1); QList arguments = outputSpy.takeFirst(); QCOMPARE(arguments.count(), 1); QCOMPARE(arguments.first().toStringList(), QStringList() << "Hello, world!" << "Hello"); } } void GdbTest::testEnvironmentSet() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeeechoenv"))); cfg.config().writeEntry("EnvironmentGroup", "GdbTestGroup"); WritableEnvironmentProfileList envProfiles(cfg.rootConfig()); envProfiles.removeProfile(QStringLiteral("GdbTestGroup")); auto &envs = envProfiles.variables(QStringLiteral("GdbTestGroup")); envs[QStringLiteral("VariableA")] = QStringLiteral("-A' \" complex --value"); envs[QStringLiteral("VariableB")] = QStringLiteral("-B' \" complex --value"); envProfiles.saveSettings(cfg.rootConfig()); QSignalSpy outputSpy(session, &TestDebugSession::inferiorStdoutLines); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, KDevelop::IDebugSession::EndedState); QVERIFY(outputSpy.count() > 0); QStringList outputLines; while (outputSpy.count() > 0) { - QList arguments = outputSpy.takeFirst(); + const QList arguments = outputSpy.takeFirst(); for (const auto &item : arguments) { outputLines.append(item.toStringList()); } } QCOMPARE(outputLines, QStringList() << "-A' \" complex --value" << "-B' \" complex --value"); } void GdbTest::testBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; KDevelop::Breakpoint * b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 28); QCOMPARE(b->state(), KDevelop::Breakpoint::NotStartedState); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(b->state(), KDevelop::Breakpoint::CleanState); session->stepInto(); WAIT_FOR_STATE(session, DebugSession::PausedState); session->stepInto(); WAIT_FOR_STATE(session, DebugSession::PausedState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); QCOMPARE(b->state(), KDevelop::Breakpoint::NotStartedState); } void GdbTest::testDisableBreakpoint() { //Description: We must stop only on the third breakpoint int firstBreakLine=28; int secondBreakLine=23; int thirdBreakLine=24; int fourthBreakLine=31; auto *session = new TestDebugSession; TestLaunchConfiguration cfg; KDevelop::Breakpoint *b; b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), firstBreakLine); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); //this is needed to emulate debug from GUI. If we are in edit mode, the debugSession doesn't exist. KDevelop::ICore::self()->debugController()->breakpointModel()->blockSignals(true); b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), secondBreakLine); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); //all disabled breakpoints were added KDevelop::Breakpoint * thirdBreak = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), thirdBreakLine); KDevelop::ICore::self()->debugController()->breakpointModel()->blockSignals(false); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), thirdBreak->line()); //disable existing breakpoint thirdBreak->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); //add another disabled breakpoint b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), fourthBreakLine); QTest::qWait(300); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); QTest::qWait(300); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testChangeLocationBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; KDevelop::Breakpoint *b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 27); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->line(), 27); QTest::qWait(100); b->setLine(28); QTest::qWait(100); session->run(); QTest::qWait(100); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 28); QTest::qWait(500); breakpoints()->setData(breakpoints()->index(0, KDevelop::Breakpoint::LocationColumn), QString(debugeeFileName+":30")); QCOMPARE(b->line(), 29); QTest::qWait(100); QCOMPARE(b->line(), 29); session->run(); QTest::qWait(100); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 29); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testDeleteBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; QCOMPARE(breakpoints()->rowCount(), 0); //add breakpoint before startDebugging breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 21); QCOMPARE(breakpoints()->rowCount(), 1); breakpoints()->removeRow(0); QCOMPARE(breakpoints()->rowCount(), 0); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 22); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); breakpoints()->removeRow(0); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testPendingBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 28); KDevelop::Breakpoint * b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile(QStringLiteral("debugeeqt.cpp"))), 10); QCOMPARE(b->state(), KDevelop::Breakpoint::NotStartedState); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(b->state(), KDevelop::Breakpoint::PendingState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testUpdateBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; // breakpoint 1: real line 29: foo(); KDevelop::Breakpoint * b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 28); QCOMPARE(breakpoints()->rowCount(), 1); session->startDebugging(&cfg, m_iface); // breakpoint 2: real line 32: const char *x = "Hello"; //insert custom command as user might do it using GDB console session->addCommand(new MI::UserCommand(MI::NonMI, "break "+debugeeFileName+":32")); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); // stop at breakpoint 1, with custom command handled QCOMPARE(session->currentLine(), 28); // check breakpoint 2 got picked up QCOMPARE(breakpoints()->rowCount(), 2); b = breakpoints()->breakpoint(1); QCOMPARE(b->url(), QUrl::fromLocalFile(debugeeFileName)); QCOMPARE(b->line(), 31); session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); // stop at breakpoint 2 QCOMPARE(session->currentLine(), 31); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testIgnoreHitsBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; KDevelop::Breakpoint * b1 = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 21); b1->setIgnoreHits(1); KDevelop::Breakpoint * b2 = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 22); session->startDebugging(&cfg, m_iface); //WAIT_FOR_STATE(session, DebugSession::PausedState); WAIT_FOR(session, session->state() == DebugSession::PausedState && b2->hitCount() == 1); b2->setIgnoreHits(1); session->run(); //WAIT_FOR_STATE(session, DebugSession::PausedState); WAIT_FOR(session, session->state() == DebugSession::PausedState && b1->hitCount() == 1); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testConditionBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; KDevelop::Breakpoint * b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 39); b->setCondition(QStringLiteral("x[0] == 'H'")); b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 23); b->setCondition(QStringLiteral("i==2")); b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 24); session->startDebugging(&cfg, m_iface); WAIT_FOR(session, session->state() == DebugSession::PausedState && session->line() == 24); b->setCondition(QStringLiteral("i == 0")); QTest::qWait(100); session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 23); session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 39); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testBreakOnWriteBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 24); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->line(), 24); breakpoints()->addWatchpoint(QStringLiteral("i")); QTest::qWait(100); session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 22); // line 23: ++i; int j = i; session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 24); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testBreakOnWriteWithConditionBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 24); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->line(), 24); KDevelop::Breakpoint *b = breakpoints()->addWatchpoint(QStringLiteral("i")); b->setCondition(QStringLiteral("i==2")); QTest::qWait(100); session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 22); // line 23: ++i; int j = i; session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 24); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testBreakOnReadBreakpoint() { /* test disabled because of gdb bug: http://sourceware.org/bugzilla/show_bug.cgi?id=10136 TestDebugSession *session = new TestDebugSession; TestLaunchConfiguration cfg; KDevelop::Breakpoint *b = breakpoints()->addReadWatchpoint("foo::i"); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 23); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); */ } void GdbTest::testBreakOnReadBreakpoint2() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 24); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->line(), 24); breakpoints()->addReadWatchpoint(QStringLiteral("i")); session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 22); // ++i session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 22); // int j = i session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); if(session->line() == 22) { // some GDB versions break 3 times on this line session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); } QCOMPARE(session->line(), 24); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testBreakOnAccessBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 24); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->line(), 24); breakpoints()->addAccessWatchpoint(QStringLiteral("i")); session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 22); // line 23: ++i (read) session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 22); // line 23: ++i (write) session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 22); // line 23: int j = i (read) session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 24); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testInsertBreakpointWhileRunning() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeeslow"))); QString fileName = findSourceFile(QStringLiteral("debugeeslow.cpp")); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE(session, DebugSession::ActiveState); QTest::qWait(2000); qDebug() << "adding breakpoint"; KDevelop::Breakpoint *b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 30); // ++i; QTest::qWait(500); WAIT_FOR_STATE(session, DebugSession::PausedState); QTest::qWait(500); QCOMPARE(session->line(), 30); // ++i; b->setDeleted(); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testInsertBreakpointWhileRunningMultiple() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeeslow"))); QString fileName = findSourceFile(QStringLiteral("debugeeslow.cpp")); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE(session, DebugSession::ActiveState); QTest::qWait(2000); qDebug() << "adding breakpoint"; KDevelop::Breakpoint *b1 = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 29); // static int i=0; KDevelop::Breakpoint *b2 = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 30); // ++i; QTest::qWait(500); WAIT_FOR_STATE(session, DebugSession::PausedState); QTest::qWait(500); QCOMPARE(session->line(), 29); // static int i=0; session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QTest::qWait(500); QCOMPARE(session->line(), 30); // ++i; b1->setDeleted(); b2->setDeleted(); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testInsertBreakpointFunctionName() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QStringLiteral("main")); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->line(), 27); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testManualBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QStringLiteral("main")); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->line(), 27); breakpoints()->removeRows(0, 1); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(breakpoints()->rowCount(), 0); session->addCommand(MI::NonMI, QStringLiteral("break debugee.cpp:23")); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(breakpoints()->rowCount(), 1); KDevelop::Breakpoint* b = breakpoints()->breakpoint(0); QCOMPARE(b->line(), 22); session->addCommand(MI::NonMI, QStringLiteral("disable 2")); session->addCommand(MI::NonMI, QStringLiteral("condition 2 i == 1")); session->addCommand(MI::NonMI, QStringLiteral("ignore 2 1")); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(b->enabled(), false); QCOMPARE(b->condition(), QString("i == 1")); QCOMPARE(b->ignoreHits(), 1); session->addCommand(MI::NonMI, QStringLiteral("delete 2")); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(breakpoints()->rowCount(), 0); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testShowStepInSource() { auto *session = new TestDebugSession; QSignalSpy showStepInSourceSpy(session, &TestDebugSession::showStepInSource); TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 29); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE(session, DebugSession::PausedState); session->stepInto(); WAIT_FOR_STATE(session, DebugSession::PausedState); session->stepInto(); WAIT_FOR_STATE(session, DebugSession::PausedState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); { QCOMPARE(showStepInSourceSpy.count(), 3); QList arguments = showStepInSourceSpy.takeFirst(); QCOMPARE(arguments.first().toUrl(), QUrl::fromLocalFile(debugeeFileName)); QCOMPARE(arguments.at(1).toInt(), 29); arguments = showStepInSourceSpy.takeFirst(); QCOMPARE(arguments.first().toUrl(), QUrl::fromLocalFile(debugeeFileName)); QCOMPARE(arguments.at(1).toInt(), 22); arguments = showStepInSourceSpy.takeFirst(); QCOMPARE(arguments.first().toUrl(), QUrl::fromLocalFile(debugeeFileName)); QCOMPARE(arguments.at(1).toInt(), 23); } } void GdbTest::testStack() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; TestFrameStackModel *stackModel = session->frameStackModel(); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 21); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QModelIndex tIdx = stackModel->index(0,0); QCOMPARE(stackModel->rowCount(QModelIndex()), 1); QCOMPARE(stackModel->columnCount(QModelIndex()), 3); COMPARE_DATA(tIdx, "#1 at foo"); QCOMPARE(stackModel->rowCount(tIdx), 2); QCOMPARE(stackModel->columnCount(tIdx), 3); COMPARE_DATA(stackModel->index(0, 0, tIdx), "0"); COMPARE_DATA(stackModel->index(0, 1, tIdx), "foo"); COMPARE_DATA(stackModel->index(0, 2, tIdx), debugeeFileName+":23"); COMPARE_DATA(stackModel->index(1, 0, tIdx), "1"); COMPARE_DATA(stackModel->index(1, 1, tIdx), "main"); COMPARE_DATA(stackModel->index(1, 2, tIdx), debugeeFileName+":29"); session->stepOut(); WAIT_FOR_STATE(session, DebugSession::PausedState); COMPARE_DATA(tIdx, "#1 at main"); QCOMPARE(stackModel->rowCount(tIdx), 1); COMPARE_DATA(stackModel->index(0, 0, tIdx), "0"); COMPARE_DATA(stackModel->index(0, 1, tIdx), "main"); COMPARE_DATA(stackModel->index(0, 2, tIdx), debugeeFileName+":30"); session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testStackFetchMore() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeerecursion"))); QString fileName = findSourceFile(QStringLiteral("debugeerecursion.cpp")); TestFrameStackModel *stackModel = session->frameStackModel(); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 25); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->frameStackModel()->fetchFramesCalled, 1); QModelIndex tIdx = stackModel->index(0,0); QCOMPARE(stackModel->rowCount(QModelIndex()), 1); QCOMPARE(stackModel->columnCount(QModelIndex()), 3); COMPARE_DATA(tIdx, "#1 at foo"); QCOMPARE(stackModel->rowCount(tIdx), 21); COMPARE_DATA(stackModel->index(0, 0, tIdx), "0"); COMPARE_DATA(stackModel->index(0, 1, tIdx), "foo"); COMPARE_DATA(stackModel->index(0, 2, tIdx), fileName+":26"); COMPARE_DATA(stackModel->index(1, 0, tIdx), "1"); COMPARE_DATA(stackModel->index(1, 1, tIdx), "foo"); COMPARE_DATA(stackModel->index(1, 2, tIdx), fileName+":24"); COMPARE_DATA(stackModel->index(2, 0, tIdx), "2"); COMPARE_DATA(stackModel->index(2, 1, tIdx), "foo"); COMPARE_DATA(stackModel->index(2, 2, tIdx), fileName+":24"); COMPARE_DATA(stackModel->index(19, 0, tIdx), "19"); COMPARE_DATA(stackModel->index(20, 0, tIdx), "20"); stackModel->fetchMoreFrames(); QTest::qWait(200); QCOMPARE(stackModel->fetchFramesCalled, 2); QCOMPARE(stackModel->rowCount(tIdx), 41); COMPARE_DATA(stackModel->index(20, 0, tIdx), "20"); COMPARE_DATA(stackModel->index(21, 0, tIdx), "21"); COMPARE_DATA(stackModel->index(22, 0, tIdx), "22"); COMPARE_DATA(stackModel->index(39, 0, tIdx), "39"); COMPARE_DATA(stackModel->index(40, 0, tIdx), "40"); stackModel->fetchMoreFrames(); QTest::qWait(200); QCOMPARE(stackModel->fetchFramesCalled, 3); QCOMPARE(stackModel->rowCount(tIdx), 121); COMPARE_DATA(stackModel->index(40, 0, tIdx), "40"); COMPARE_DATA(stackModel->index(41, 0, tIdx), "41"); COMPARE_DATA(stackModel->index(42, 0, tIdx), "42"); COMPARE_DATA(stackModel->index(119, 0, tIdx), "119"); COMPARE_DATA(stackModel->index(120, 0, tIdx), "120"); stackModel->fetchMoreFrames(); QTest::qWait(200); QCOMPARE(stackModel->fetchFramesCalled, 4); QCOMPARE(stackModel->rowCount(tIdx), 299); COMPARE_DATA(stackModel->index(120, 0, tIdx), "120"); COMPARE_DATA(stackModel->index(121, 0, tIdx), "121"); COMPARE_DATA(stackModel->index(122, 0, tIdx), "122"); COMPARE_DATA(stackModel->index(298, 0, tIdx), "298"); COMPARE_DATA(stackModel->index(298, 1, tIdx), "main"); COMPARE_DATA(stackModel->index(298, 2, tIdx), fileName+":30"); stackModel->fetchMoreFrames(); //nothing to fetch, we are at the end QTest::qWait(200); QCOMPARE(stackModel->fetchFramesCalled, 4); QCOMPARE(stackModel->rowCount(tIdx), 299); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testStackDeactivateAndActive() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; TestFrameStackModel *stackModel = session->frameStackModel(); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 21); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::PausedState); QModelIndex tIdx = stackModel->index(0,0); session->stepOut(); WAIT_FOR_STATE(session, DebugSession::PausedState); QTest::qWait(200); COMPARE_DATA(tIdx, "#1 at main"); QCOMPARE(stackModel->rowCount(tIdx), 1); COMPARE_DATA(stackModel->index(0, 0, tIdx), "0"); COMPARE_DATA(stackModel->index(0, 1, tIdx), "main"); COMPARE_DATA(stackModel->index(0, 2, tIdx), debugeeFileName+":30"); session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testStackSwitchThread() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeethreads"))); QString fileName = findSourceFile(QStringLiteral("debugeethreads.cpp")); TestFrameStackModel *stackModel = session->frameStackModel(); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 40); // t3.start(); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QVERIFY(stackModel->rowCount() > 2); QModelIndex tIdx = stackModel->index(0,0); COMPARE_DATA(tIdx, "#1 at main"); QCOMPARE(stackModel->rowCount(tIdx), 1); COMPARE_DATA(stackModel->index(0, 0, tIdx), "0"); COMPARE_DATA(stackModel->index(0, 1, tIdx), "main"); COMPARE_DATA(stackModel->index(0, 2, tIdx), fileName+":41"); // QThread::usleep(500000); tIdx = stackModel->index(1,0); QVERIFY(stackModel->data(tIdx).toString().startsWith("#2 at ")); stackModel->setCurrentThread(2); QTest::qWait(200); int rows = stackModel->rowCount(tIdx); QVERIFY(rows > 3); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testAttach() { SKIP_IF_ATTACH_FORBIDDEN(); #ifdef Q_OS_FREEBSD // Despite successful attach GDB MI spits out a error message "Can't allocate registers". This gets caught by KDevMI layer and gets interpreted as error. // Upstream PR: https://sourceware.org/bugzilla/show_bug.cgi?id=23464 QSKIP("GDB on FreeBSD produces an unexpected error message, on which KDevelop chokes"); #endif QString fileName = findSourceFile(QStringLiteral("debugeeslow.cpp")); KProcess debugeeProcess; debugeeProcess << QStringLiteral("nice") << findExecutable(QStringLiteral("debuggee_debugeeslow")).toLocalFile(); debugeeProcess.start(); QVERIFY(debugeeProcess.waitForStarted()); QTest::qWait(100); auto *session = new TestDebugSession; session->attachToProcess(debugeeProcess.pid()); WAIT_FOR_STATE(session, DebugSession::PausedState); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 39); // } after foo(); QTest::qWait(100); session->run(); QTest::qWait(2000); WAIT_FOR_STATE(session, DebugSession::PausedState); if (session->line() < 39 || session->line() < 40) { QCOMPARE(session->line(), 39); } session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testManualAttach() { SKIP_IF_ATTACH_FORBIDDEN(); #ifdef Q_OS_FREEBSD // Despite successful attach GDB MI spits out a error message "Can't allocate registers". This gets caught by KDevMI layer and gets interpreted as error. // Upstream PR: https://sourceware.org/bugzilla/show_bug.cgi?id=23464 QSKIP("GDB on FreeBSD produces an unexpected error message, on which KDevelop chokes"); #endif KProcess debugeeProcess; debugeeProcess << QStringLiteral("nice") << findExecutable(QStringLiteral("debuggee_debugeeslow")).toLocalFile(); debugeeProcess.start(); QVERIFY(debugeeProcess.waitForStarted()); auto *session = new TestDebugSession; TestLaunchConfiguration cfg; cfg.config().writeEntry(Config::RemoteGdbRunEntry, QUrl::fromLocalFile(findFile(GDB_SRC_DIR, QStringLiteral("unittests/gdb_script_empty")))); QVERIFY(session->startDebugging(&cfg, m_iface)); session->addCommand(MI::NonMI, QStringLiteral("attach %0").arg(debugeeProcess.pid())); WAIT_FOR_STATE(session, DebugSession::PausedState); session->run(); QTest::qWait(2000); // give the slow inferior some extra time to run WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testCoreFile() { QFileInfo f(QStringLiteral("core")); f.setCaching(false); // don't cache information if (f.exists()) { QVERIFY(QFile::remove(f.canonicalFilePath())); } KProcess debugeeProcess; debugeeProcess.setOutputChannelMode(KProcess::MergedChannels); debugeeProcess << QStringLiteral("bash") << QStringLiteral("-c") << "ulimit -c unlimited; " + findExecutable(QStringLiteral("debuggee_crash")).toLocalFile(); debugeeProcess.start(); debugeeProcess.waitForFinished(); qDebug() << "Debuggee output:\n" << debugeeProcess.readAll(); bool coreFileFound = f.exists(); if (!coreFileFound) { // Try to use coredumpctl auto coredumpctl = QStandardPaths::findExecutable(QStringLiteral("coredumpctl")); if (!coredumpctl.isEmpty()) { KProcess::execute(coredumpctl, {"-1", "-o", f.absoluteFilePath(), "dump", "debuggee_crash"}, 5000); // coredumpctl seems to create an empty file "core" even if no cores can be delivered // (like when run inside docker containers as on KDE CI or with kernel.core_pattern=|/dev/null) // so also check for size != 0 coreFileFound = f.exists() && (f.size() > 0); } } if (!coreFileFound) QSKIP("no core dump found, check your system configuration (see /proc/sys/kernel/core_pattern).", SkipSingle); auto *session = new TestDebugSession; session->examineCoreFile(findExecutable(QStringLiteral("debuggee_crash")), QUrl::fromLocalFile(f.canonicalFilePath())); TestFrameStackModel *stackModel = session->frameStackModel(); WAIT_FOR_STATE(session, DebugSession::StoppedState); QModelIndex tIdx = stackModel->index(0,0); QCOMPARE(stackModel->rowCount(QModelIndex()), 1); QCOMPARE(stackModel->columnCount(QModelIndex()), 3); COMPARE_DATA(tIdx, "#1 at foo"); session->stopDebugger(); WAIT_FOR_STATE(session, DebugSession::EndedState); } KDevelop::VariableCollection *variableCollection() { return KDevelop::ICore::self()->debugController()->variableCollection(); } void GdbTest::testVariablesLocals() { auto *session = new TestDebugSession; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 22); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::PausedState); QTest::qWait(1000); QCOMPARE(variableCollection()->rowCount(), 2); QModelIndex i = variableCollection()->index(1, 0); COMPARE_DATA(i, "Locals"); QCOMPARE(variableCollection()->rowCount(i), 2); COMPARE_DATA(variableCollection()->index(0, 0, i), "i"); COMPARE_DATA(variableCollection()->index(0, 1, i), "0"); COMPARE_DATA(variableCollection()->index(1, 0, i), "j"); // COMPARE_DATA(variableCollection()->index(1, 1, i), "1"); // j is not initialized yet session->run(); QTest::qWait(1000); WAIT_FOR_STATE(session, DebugSession::PausedState); COMPARE_DATA(variableCollection()->index(0, 0, i), "i"); COMPARE_DATA(variableCollection()->index(0, 1, i), "1"); COMPARE_DATA(variableCollection()->index(1, 0, i), "j"); COMPARE_DATA(variableCollection()->index(1, 1, i), "1"); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testVariablesLocalsStruct() { auto *session = new TestDebugSession; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 38); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::PausedState); QTest::qWait(1000); QModelIndex i = variableCollection()->index(1, 0); QCOMPARE(variableCollection()->rowCount(i), 4); int structIndex = 0; for(int j=0; j<3; ++j) { if (variableCollection()->index(j, 0, i).data().toString() == QLatin1String("ts")) { structIndex = j; } } COMPARE_DATA(variableCollection()->index(structIndex, 0, i), "ts"); COMPARE_DATA(variableCollection()->index(structIndex, 1, i), "{...}"); QModelIndex ts = variableCollection()->index(structIndex, 0, i); COMPARE_DATA(variableCollection()->index(0, 0, ts), "..."); variableCollection()->expanded(ts); QTest::qWait(100); COMPARE_DATA(variableCollection()->index(0, 0, ts), "a"); COMPARE_DATA(variableCollection()->index(0, 1, ts), "0"); COMPARE_DATA(variableCollection()->index(1, 0, ts), "b"); COMPARE_DATA(variableCollection()->index(1, 1, ts), "1"); COMPARE_DATA(variableCollection()->index(2, 0, ts), "c"); COMPARE_DATA(variableCollection()->index(2, 1, ts), "2"); session->stepInto(); WAIT_FOR_STATE(session, DebugSession::PausedState); QTest::qWait(1000); COMPARE_DATA(variableCollection()->index(structIndex, 0, i), "ts"); COMPARE_DATA(variableCollection()->index(structIndex, 1, i), "{...}"); COMPARE_DATA(variableCollection()->index(0, 1, ts), "1"); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testVariablesWatches() { auto *session = new TestDebugSession; KDevelop::ICore::self()->debugController()->variableCollection()->variableWidgetShown(); TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 38); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::PausedState); variableCollection()->watches()->add(QStringLiteral("ts")); QTest::qWait(300); QModelIndex i = variableCollection()->index(0, 0); QCOMPARE(variableCollection()->rowCount(i), 1); COMPARE_DATA(variableCollection()->index(0, 0, i), "ts"); COMPARE_DATA(variableCollection()->index(0, 1, i), "{...}"); QModelIndex ts = variableCollection()->index(0, 0, i); COMPARE_DATA(variableCollection()->index(0, 0, ts), "..."); variableCollection()->expanded(ts); QTest::qWait(100); COMPARE_DATA(variableCollection()->index(0, 0, ts), "a"); COMPARE_DATA(variableCollection()->index(0, 1, ts), "0"); COMPARE_DATA(variableCollection()->index(1, 0, ts), "b"); COMPARE_DATA(variableCollection()->index(1, 1, ts), "1"); COMPARE_DATA(variableCollection()->index(2, 0, ts), "c"); COMPARE_DATA(variableCollection()->index(2, 1, ts), "2"); session->stepInto(); WAIT_FOR_STATE(session, DebugSession::PausedState); QTest::qWait(100); COMPARE_DATA(variableCollection()->index(0, 0, i), "ts"); COMPARE_DATA(variableCollection()->index(0, 1, i), "{...}"); COMPARE_DATA(variableCollection()->index(0, 1, ts), "1"); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testVariablesWatchesQuotes() { auto *session = new TestDebugSession; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateWatches); TestLaunchConfiguration cfg; // the unquoted string (the actual content): t\"t // quoted string (what we would write as a c string): "t\\\"t" // written in source file: R"("t\\\"t")" const QString testString(QStringLiteral("t\\\"t")); // the actual content const QString quotedTestString(QStringLiteral(R"("t\\\"t")")); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 38); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); variableCollection()->watches()->add(quotedTestString); //just a constant string QTest::qWait(300); QModelIndex i = variableCollection()->index(0, 0); QCOMPARE(variableCollection()->rowCount(i), 1); COMPARE_DATA(variableCollection()->index(0, 0, i), quotedTestString); COMPARE_DATA(variableCollection()->index(0, 1, i), "[" + QString::number(testString.length() + 1) + "]"); QModelIndex testStr = variableCollection()->index(0, 0, i); COMPARE_DATA(variableCollection()->index(0, 0, testStr), "..."); variableCollection()->expanded(testStr); QTest::qWait(100); int len = testString.length(); for (int ind = 0; ind < len; ind++) { COMPARE_DATA(variableCollection()->index(ind, 0, testStr), QString::number(ind)); QChar c = testString.at(ind); QString value = QString::number(c.toLatin1()) + " '"; if (c == '\\') value += QLatin1String("\\\\"); else if (c == '\'') value += QLatin1String("\\'"); else value += c; value += QLatin1String("'"); COMPARE_DATA(variableCollection()->index(ind, 1, testStr), value); } COMPARE_DATA(variableCollection()->index(len, 0, testStr), QString::number(len)); COMPARE_DATA(variableCollection()->index(len, 1, testStr), "0 '\\000'"); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testVariablesWatchesTwoSessions() { auto *session = new TestDebugSession; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateWatches); TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 38); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::PausedState); variableCollection()->watches()->add(QStringLiteral("ts")); QTest::qWait(300); QModelIndex ts = variableCollection()->index(0, 0, variableCollection()->index(0, 0)); variableCollection()->expanded(ts); QTest::qWait(100); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); //check if variable is marked as out-of-scope QCOMPARE(variableCollection()->watches()->childCount(), 1); auto* v = qobject_cast(variableCollection()->watches()->child(0)); QVERIFY(v); QVERIFY(!v->inScope()); QCOMPARE(v->childCount(), 3); v = qobject_cast(v->child(0)); QVERIFY(!v->inScope()); //start a second debug session session = new TestDebugSession; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateWatches); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::PausedState); QTest::qWait(300); QCOMPARE(variableCollection()->watches()->childCount(), 1); ts = variableCollection()->index(0, 0, variableCollection()->index(0, 0)); v = qobject_cast(variableCollection()->watches()->child(0)); QVERIFY(v); QVERIFY(v->inScope()); QCOMPARE(v->childCount(), 3); v = qobject_cast(v->child(0)); QVERIFY(v->inScope()); QCOMPARE(v->data(1, Qt::DisplayRole).toString(), QString::number(0)); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); //check if variable is marked as out-of-scope v = qobject_cast(variableCollection()->watches()->child(0)); QVERIFY(!v->inScope()); QVERIFY(!dynamic_cast(v->child(0))->inScope()); } void GdbTest::testVariablesStopDebugger() { auto *session = new TestDebugSession; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 38); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::PausedState); session->stopDebugger(); QTest::qWait(300); } void GdbTest::testVariablesStartSecondSession() { QPointer session = new TestDebugSession; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 38); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::PausedState); QPointer session2 = new TestDebugSession; session2->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 38); QVERIFY(session2->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session2, DebugSession::PausedState); session2->run(); WAIT_FOR_STATE(session2, DebugSession::EndedState); } void GdbTest::testVariablesSwitchFrame() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); TestFrameStackModel *stackModel = session->frameStackModel(); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 24); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::PausedState); QTest::qWait(500); QModelIndex i = variableCollection()->index(1, 0); COMPARE_DATA(i, "Locals"); QCOMPARE(variableCollection()->rowCount(i), 2); COMPARE_DATA(variableCollection()->index(0, 0, i), "i"); COMPARE_DATA(variableCollection()->index(0, 1, i), "1"); COMPARE_DATA(variableCollection()->index(1, 0, i), "j"); stackModel->setCurrentFrame(1); QTest::qWait(200); i = variableCollection()->index(1, 0); QCOMPARE(variableCollection()->rowCount(i), 4); COMPARE_DATA(variableCollection()->index(2, 0, i), "argc"); COMPARE_DATA(variableCollection()->index(2, 1, i), "1"); COMPARE_DATA(variableCollection()->index(3, 0, i), "argv"); breakpoints()->removeRow(0); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testVariablesQuicklySwitchFrame() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); TestFrameStackModel *stackModel = session->frameStackModel(); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 24); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::PausedState); QTest::qWait(500); QModelIndex i = variableCollection()->index(1, 0); COMPARE_DATA(i, "Locals"); QCOMPARE(variableCollection()->rowCount(i), 2); COMPARE_DATA(variableCollection()->index(0, 0, i), "i"); COMPARE_DATA(variableCollection()->index(0, 1, i), "1"); COMPARE_DATA(variableCollection()->index(1, 0, i), "j"); stackModel->setCurrentFrame(1); QTest::qWait(300); stackModel->setCurrentFrame(0); QTest::qWait(1); stackModel->setCurrentFrame(1); QTest::qWait(1); stackModel->setCurrentFrame(0); QTest::qWait(1); stackModel->setCurrentFrame(1); QTest::qWait(500); i = variableCollection()->index(1, 0); QCOMPARE(variableCollection()->rowCount(i), 4); QStringList locs; for (int j = 0; j < variableCollection()->rowCount(i); ++j) { locs << variableCollection()->index(j, 0, i).data().toString(); } QVERIFY(locs.contains("argc")); QVERIFY(locs.contains("argv")); QVERIFY(locs.contains("x")); breakpoints()->removeRow(0); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testSegfaultDebugee() { auto *session = new TestDebugSession; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_crash"))); QString fileName = findSourceFile(QStringLiteral("debugeecrash.cpp")); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 23); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->line(), 23); session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 24); session->stopDebugger(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testSwitchFrameGdbConsole() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; TestFrameStackModel *stackModel = session->frameStackModel(); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 24); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(stackModel->currentFrame(), 0); stackModel->setCurrentFrame(1); QCOMPARE(stackModel->currentFrame(), 1); QTest::qWait(500); QCOMPARE(stackModel->currentFrame(), 1); session->addUserCommand(QStringLiteral("print x")); QTest::qWait(500); //currentFrame must not reset to 0; Bug 222882 QCOMPARE(stackModel->currentFrame(), 1); } //Bug 201771 void GdbTest::testInsertAndRemoveBreakpointWhileRunning() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeeslow"))); QString fileName = findSourceFile(QStringLiteral("debugeeslow.cpp")); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE(session, DebugSession::ActiveState); QTest::qWait(2000); qDebug() << "adding breakpoint"; KDevelop::Breakpoint *b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 30); // ++i; b->setDeleted(); WAIT_FOR_STATE(session, DebugSession::EndedState); } //Bug 274390 void GdbTest::testCommandOrderFastStepping() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeeqt"))); breakpoints()->addCodeBreakpoint(QStringLiteral("main")); QVERIFY(session->startDebugging(&cfg, m_iface)); for(int i=0; i<20; i++) { session->stepInto(); } WAIT_FOR_STATE(session, DebugSession::PausedState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testPickupManuallyInsertedBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QStringLiteral("main")); QVERIFY(session->startDebugging(&cfg, m_iface)); session->addCommand(MI::NonMI, QStringLiteral("break debugee.cpp:32")); session->stepInto(); WAIT_FOR_STATE(session, DebugSession::PausedState); QTest::qWait(1000); //wait for breakpoints update QCOMPARE(breakpoints()->breakpoints().count(), 2); QCOMPARE(breakpoints()->rowCount(), 2); KDevelop::Breakpoint *b = breakpoints()->breakpoint(1); QVERIFY(b); QCOMPARE(b->line(), 31); //we start with 0, gdb with 1 QCOMPARE(b->url().fileName(), QString("debugee.cpp")); } //Bug 270970 void GdbTest::testPickupManuallyInsertedBreakpointOnlyOnce() { auto *session = new TestDebugSession; //inject here, so it behaves similar like a command from .gdbinit QTemporaryFile configScript; configScript.open(); configScript.write(QStringLiteral("file %0\n").arg(findExecutable(QStringLiteral("debuggee_debugee")).toLocalFile()).toLocal8Bit()); configScript.write("break debugee.cpp:32\n"); configScript.close(); TestLaunchConfiguration cfg; KConfigGroup grp = cfg.config(); grp.writeEntry(Config::RemoteGdbConfigEntry, QUrl::fromLocalFile(configScript.fileName())); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(QStringLiteral("debugee.cpp")), 31); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(breakpoints()->breakpoints().count(), 1); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testPickupCatchThrowOnlyOnce() { QTemporaryFile configScript; configScript.open(); configScript.write("catch throw\n"); configScript.close(); TestLaunchConfiguration cfg; KConfigGroup grp = cfg.config(); grp.writeEntry(Config::RemoteGdbConfigEntry, QUrl::fromLocalFile(configScript.fileName())); for (int i = 0; i < 2; ++i) { auto* session = new TestDebugSession; QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::EndedState); } QCOMPARE(breakpoints()->rowCount(), 1); //one from kdevelop, one from runScript } void GdbTest::testRunGdbScript() { auto *session = new TestDebugSession; QTemporaryFile runScript; runScript.open(); runScript.write("file " + KShell::quoteArg(findExecutable(QStringLiteral("debuggee_debugee")).toLocalFile()).toUtf8() + "\n"); runScript.write("break main\n"); runScript.write("run\n"); runScript.close(); TestLaunchConfiguration cfg; KConfigGroup grp = cfg.config(); grp.writeEntry(Config::RemoteGdbRunEntry, QUrl::fromLocalFile(runScript.fileName())); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->line(), 27); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testRemoteDebug() { const QString gdbserverExecutable = QStandardPaths::findExecutable(QStringLiteral("gdbserver")); if (gdbserverExecutable.isEmpty()) { QSKIP("Skipping, gdbserver not available", SkipSingle); } auto *session = new TestDebugSession; QTemporaryFile shellScript(QDir::currentPath()+"/shellscript"); shellScript.open(); shellScript.write("gdbserver localhost:2345 " + findExecutable(QStringLiteral("debuggee_debugee")).toLocalFile().toUtf8() + "\n"); shellScript.close(); shellScript.setPermissions(shellScript.permissions() | QFile::ExeUser); QFile::copy(shellScript.fileName(), shellScript.fileName()+"-copy"); //to avoid "Text file busy" on executing (why?) QTemporaryFile runScript(QDir::currentPath()+"/runscript"); runScript.open(); runScript.write("file " + findExecutable(QStringLiteral("debuggee_debugee")).toLocalFile().toUtf8() + "\n"); runScript.write("target remote localhost:2345\n"); runScript.write("break debugee.cpp:30\n"); runScript.write("continue\n"); runScript.close(); TestLaunchConfiguration cfg; KConfigGroup grp = cfg.config(); grp.writeEntry(Config::RemoteGdbShellEntry, QUrl::fromLocalFile((shellScript.fileName()+"-copy"))); grp.writeEntry(Config::RemoteGdbRunEntry, QUrl::fromLocalFile(runScript.fileName())); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->line(), 29); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); QFile::remove(shellScript.fileName()+"-copy"); } void GdbTest::testRemoteDebugInsertBreakpoint() { const QString gdbserverExecutable = QStandardPaths::findExecutable(QStringLiteral("gdbserver")); if (gdbserverExecutable.isEmpty()) { QSKIP("Skipping, gdbserver not available", SkipSingle); } auto *session = new TestDebugSession; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 29); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 35); QTemporaryFile shellScript(QDir::currentPath()+"/shellscript"); shellScript.open(); shellScript.write("gdbserver localhost:2345 " + findExecutable(QStringLiteral("debuggee_debugee")).toLocalFile().toUtf8() + "\n"); shellScript.close(); shellScript.setPermissions(shellScript.permissions() | QFile::ExeUser); QFile::copy(shellScript.fileName(), shellScript.fileName()+"-copy"); //to avoid "Text file busy" on executing (why?) QTemporaryFile runScript(QDir::currentPath()+"/runscript"); runScript.open(); runScript.write("file " + findExecutable(QStringLiteral("debuggee_debugee")).toLocalFile().toUtf8() + '\n'); runScript.write("target remote localhost:2345\n"); runScript.write("break debugee.cpp:30\n"); runScript.write("continue\n"); runScript.close(); TestLaunchConfiguration cfg; KConfigGroup grp = cfg.config(); grp.writeEntry(Config::RemoteGdbShellEntry, QUrl::fromLocalFile(shellScript.fileName()+"-copy")); grp.writeEntry(Config::RemoteGdbRunEntry, QUrl::fromLocalFile(runScript.fileName())); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->line(), 29); QCOMPARE(breakpoints()->breakpoints().count(), 2); //one from kdevelop, one from runScript session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 35); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); QFile::remove(shellScript.fileName()+"-copy"); } void GdbTest::testRemoteDebugInsertBreakpointPickupOnlyOnce() { const QString gdbserverExecutable = QStandardPaths::findExecutable(QStringLiteral("gdbserver")); if (gdbserverExecutable.isEmpty()) { QSKIP("Skipping, gdbserver not available", SkipSingle); } auto *session = new TestDebugSession; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 35); QTemporaryFile shellScript(QDir::currentPath()+"/shellscript"); shellScript.open(); shellScript.write("gdbserver localhost:2345 "+findExecutable(QStringLiteral("debuggee_debugee")).toLocalFile().toLatin1()+"\n"); shellScript.close(); shellScript.setPermissions(shellScript.permissions() | QFile::ExeUser); QFile::copy(shellScript.fileName(), shellScript.fileName()+"-copy"); //to avoid "Text file busy" on executing (why?) QTemporaryFile runScript(QDir::currentPath()+"/runscript"); runScript.open(); runScript.write("file "+findExecutable(QStringLiteral("debuggee_debugee")).toLocalFile().toLatin1()+"\n"); runScript.write("target remote localhost:2345\n"); runScript.write("break debugee.cpp:30\n"); runScript.write("continue\n"); runScript.close(); TestLaunchConfiguration cfg; KConfigGroup grp = cfg.config(); grp.writeEntry(Config::RemoteGdbShellEntry, QUrl::fromLocalFile((shellScript.fileName()+"-copy"))); grp.writeEntry(Config::RemoteGdbRunEntry, QUrl::fromLocalFile(runScript.fileName())); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->line(), 29); QCOMPARE(breakpoints()->breakpoints().count(), 2); //one from kdevelop, one from runScript session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 35); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); //************************** second session session = new TestDebugSession; QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->line(), 29); QCOMPARE(breakpoints()->breakpoints().count(), 2); //one from kdevelop, one from runScript session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 35); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); QFile::remove(shellScript.fileName()+"-copy"); } void GdbTest::testBreakpointWithSpaceInPath() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeespace"))); KConfigGroup grp = cfg.config(); QString fileName = findSourceFile(QStringLiteral("debugee space.cpp")); KDevelop::Breakpoint * b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 20); QCOMPARE(b->state(), KDevelop::Breakpoint::NotStartedState); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->line(), 20); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testBreakpointDisabledOnStart() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 28) ->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 29); KDevelop::Breakpoint* b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 31); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->line(), 29); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Checked); session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 31); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testCatchpoint() { auto *session = new TestDebugSession; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeeexception"))); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile(QStringLiteral("debugeeexception.cpp"))), 29); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE(session, DebugSession::PausedState); QTest::qWait(1000); TestFrameStackModel* fsModel = session->frameStackModel(); QCOMPARE(fsModel->currentFrame(), 0); QCOMPARE(session->line(), 29); session->addCommand(MI::NonMI, QStringLiteral("catch throw")); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QTest::qWait(1000); const QVector frames = fsModel->frames(fsModel->currentThread()); QVERIFY(frames.size() >= 2); // frame 0 is somewhere inside libstdc++ QCOMPARE(frames[1].file, QUrl::fromLocalFile(findSourceFile("debugeeexception.cpp"))); QCOMPARE(frames[1].line, 22); QCOMPARE(breakpoints()->rowCount(),2); QVERIFY(!breakpoints()->breakpoint(0)->location().isEmpty()); QVERIFY(!breakpoints()->breakpoint(1)->location().isEmpty()); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testThreadAndFrameInfo() { // Check if --thread is added to user commands auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeethreads"))); QString fileName = findSourceFile(QStringLiteral("debugeethreads.cpp")); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 38); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QSignalSpy outputSpy(session, &TestDebugSession::debuggerUserCommandOutput); session->addCommand(new MI::UserCommand(MI::ThreadInfo, QString())); session->addCommand(new MI::UserCommand(MI::StackListLocals, QStringLiteral("0"))); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); // wait for command finish // outputs should be // 1. -thread-info // 2. ^done for thread-info // 3. -stack-list-locals // 4. ^done for -stack-list-locals QCOMPARE(outputSpy.count(), 4); QVERIFY(outputSpy.at(2).at(0).toString().contains(QLatin1String("--thread 1"))); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::parseBug304730() { MI::FileSymbol file; file.contents = QByteArray("^done,bkpt={" "number=\"1\",type=\"breakpoint\",disp=\"keep\",enabled=\"y\",addr=\"\",times=\"0\"," "original-location=\"/media/portable/Projects/BDSInpainting/PatchMatch/PatchMatch.hpp:231\"}," "{number=\"1.1\",enabled=\"y\",addr=\"0x081d84aa\"," "func=\"PatchMatch, 2u> >" "::Propagation(ForwardPropagationNeighbors)\"," "file=\"/media/portable/Projects/BDSInpainting/Drivers/../PatchMatch/PatchMatch.hpp\"," "fullname=\"/media/portable/Projects/BDSInpainting/PatchMatch/PatchMatch.hpp\",line=\"231\"}," "{number=\"1.2\",enabled=\"y\",addr=\"0x081d8ae2\"," "func=\"PatchMatch, 2u> >" "::Propagation(BackwardPropagationNeighbors)\"," "file=\"/media/portable/Projects/BDSInpainting/Drivers/../PatchMatch/PatchMatch.hpp\"," "fullname=\"/media/portable/Projects/BDSInpainting/PatchMatch/PatchMatch.hpp\",line=\"231\"}," "{number=\"1.3\",enabled=\"y\",addr=\"0x081d911a\"," "func=\"PatchMatch, 2u> >" "::Propagation(AllowedPropagationNeighbors)\"," "file=\"/media/portable/Projects/BDSInpainting/Drivers/../PatchMatch/PatchMatch.hpp\"," "fullname=\"/media/portable/Projects/BDSInpainting/PatchMatch/PatchMatch.hpp\",line=\"231\"}"); MI::MIParser parser; std::unique_ptr record(parser.parse(&file)); QVERIFY(record.get() != nullptr); } void GdbTest::testMultipleLocationsBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeemultilocbreakpoint"))); breakpoints()->addCodeBreakpoint(QStringLiteral("aPlusB")); //TODO check if the additional location breakpoint is added session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->line(), 19); session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->line(), 23); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testBug301287() { auto *session = new TestDebugSession; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateWatches); TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 28); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE(session, DebugSession::PausedState); variableCollection()->watches()->add(QStringLiteral("argc")); QTest::qWait(300); QModelIndex i = variableCollection()->index(0, 0); QCOMPARE(variableCollection()->rowCount(i), 1); COMPARE_DATA(variableCollection()->index(0, 0, i), "argc"); COMPARE_DATA(variableCollection()->index(0, 1, i), "1"); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); //start second debug session (same cfg) session = new TestDebugSession; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateWatches); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE(session, DebugSession::PausedState); QTest::qWait(300); i = variableCollection()->index(0, 0); QCOMPARE(variableCollection()->rowCount(i), 1); COMPARE_DATA(variableCollection()->index(0, 0, i), "argc"); COMPARE_DATA(variableCollection()->index(0, 1, i), "1"); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testMultipleBreakpoint() { auto *session = new TestDebugSession; //there'll be about 3-4 breakpoints, but we treat it like one. TestLaunchConfiguration c(findExecutable(QStringLiteral("debuggee_debugeemultiplebreakpoint"))); KDevelop::Breakpoint *b = breakpoints()->addCodeBreakpoint(QStringLiteral("debugeemultiplebreakpoint.cpp:52")); session->startDebugging(&c, m_iface); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(breakpoints()->breakpoints().count(), 1); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testRegularExpressionBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration c(findExecutable(QStringLiteral("debuggee_debugeemultilocbreakpoint"))); breakpoints()->addCodeBreakpoint(QStringLiteral("main")); session->startDebugging(&c, m_iface); WAIT_FOR_STATE(session, DebugSession::PausedState); session->addCommand(MI::NonMI, QStringLiteral("rbreak .*aPl.*B")); QTest::qWait(100); session->run(); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(breakpoints()->breakpoints().count(), 3); session->addCommand(MI::BreakDelete, QString()); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testChangeBreakpointWhileRunning() { auto *session = new TestDebugSession; TestLaunchConfiguration c(findExecutable(QStringLiteral("debuggee_debugeeslow"))); KDevelop::Breakpoint* b = breakpoints()->addCodeBreakpoint(QStringLiteral("debugeeslow.cpp:30")); session->startDebugging(&c, m_iface); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QVERIFY(session->currentLine() >= 29 && session->currentLine() <= 31 ); session->run(); WAIT_FOR_STATE(session, DebugSession::ActiveState); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); //to make one loop QTest::qWait(2000); WAIT_FOR_STATE(session, DebugSession::ActiveState); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Checked); QTest::qWait(100); WAIT_FOR_STATE(session, DebugSession::PausedState); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); session->run(); QTest::qWait(100); WAIT_FOR_STATE(session, DebugSession::EndedState); } void GdbTest::testDebugInExternalTerminal() { TestLaunchConfiguration cfg; const QStringList consoles { "konsole", "xterm", "xfce4-terminal", "gnome-terminal" }; for (const QString& console : consoles) { TestDebugSession* session = nullptr; if (QStandardPaths::findExecutable(console).isEmpty()) { continue; } session = new TestDebugSession(); cfg.config().writeEntry("External Terminal"/*ExecutePlugin::terminalEntry*/, console); cfg.config().writeEntry("Use External Terminal"/*ExecutePlugin::useTerminalEntry*/, true); KDevelop::Breakpoint* b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(debugeeFileName), 28); session->startDebugging(&cfg, m_iface); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(b->state(), KDevelop::Breakpoint::CleanState); session->stepInto(); WAIT_FOR_STATE(session, DebugSession::PausedState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } } // see: https://bugs.kde.org/show_bug.cgi?id=339231 void GdbTest::testPathWithSpace() { auto* session = new TestDebugSession; auto debugee = findExecutable(QStringLiteral("path with space/debuggee_spacedebugee")); TestLaunchConfiguration c(debugee, KIO::upUrl(debugee)); KDevelop::Breakpoint* b = breakpoints()->addCodeBreakpoint(QStringLiteral("spacedebugee.cpp:30")); QCOMPARE(b->state(), KDevelop::Breakpoint::NotStartedState); session->startDebugging(&c, m_iface); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(b->state(), KDevelop::Breakpoint::CleanState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } bool GdbTest::waitForState(DebugSession *session, DebugSession::DebuggerState state, const char *file, int line, bool waitForIdle) { QPointer s(session); //session can get deleted in DebugController QTime stopWatch; stopWatch.start(); // legacy behavior for tests that implicitly may require waiting for idle, // but which were written before waitForIdle was added waitForIdle = waitForIdle || state != MIDebugSession::EndedState; while (s && (s->state() != state || (waitForIdle && s->debuggerStateIsOn(s_dbgBusy)))) { if (stopWatch.elapsed() > 5000) { qWarning() << "current state" << s->state() << "waiting for" << state; QTest::qFail(qPrintable(QString("Timeout before reaching state %0").arg(state)), file, line); return false; } QTest::qWait(20); } // NOTE: don't wait anymore after leaving the loop. Waiting re-enters event loop and // may change session state. if (!s && state != MIDebugSession::EndedState) { QTest::qFail(qPrintable(QString("Session ended before reaching state %0").arg(state)), file, line); return false; } qDebug() << "Reached state " << state << " in " << file << ':' << line; return true; } } // end of namespace GDB } // end of namespace KDevMI QTEST_MAIN(KDevMI::GDB::GdbTest) #include "test_gdb.moc" #include "moc_test_gdb.cpp" diff --git a/plugins/lldb/unittests/test_lldb.cpp b/plugins/lldb/unittests/test_lldb.cpp index 51242cd95b..a006983109 100644 --- a/plugins/lldb/unittests/test_lldb.cpp +++ b/plugins/lldb/unittests/test_lldb.cpp @@ -1,1906 +1,1906 @@ /* * Unit tests for LLDB debugger plugin Copyright 2009 Niko Sams Copyright 2013 Vlas Puhov * Copyright 2016 Aetf * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "test_lldb.h" #include "controllers/framestackmodel.h" #include "debugsession.h" #include "tests/debuggers-tests-config.h" #include "tests/testhelper.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define WAIT_FOR_STATE(session, state) \ do { if (!KDevMI::waitForState((session), (state), __FILE__, __LINE__)) return; } while (0) #define WAIT_FOR_STATE_AND_IDLE(session, state) \ do { if (!KDevMI::waitForState((session), (state), __FILE__, __LINE__, true)) return; } while (0) #define WAIT_FOR_A_WHILE(session, ms) \ do { if (!KDevMI::waitForAWhile((session), (ms), __FILE__, __LINE__)) return; } while (0) #define WAIT_FOR(session, condition) \ do { \ KDevMI::TestWaiter w((session), #condition, __FILE__, __LINE__); \ while (w.waitUnless((condition))) /* nothing */ ; \ } while(0) #define COMPARE_DATA(index, expected) \ do { if (!KDevMI::compareData((index), (expected), __FILE__, __LINE__)) return; } while (0) #define SKIP_IF_ATTACH_FORBIDDEN() \ do { \ if (KDevMI::isAttachForbidden(__FILE__, __LINE__)) \ return; \ } while(0) using namespace KDevelop; using namespace KDevMI::LLDB; using KDevMI::findExecutable; using KDevMI::findFile; using KDevMI::findSourceFile; namespace { class WritableEnvironmentProfileList : public EnvironmentProfileList { public: explicit WritableEnvironmentProfileList(KConfig* config) : EnvironmentProfileList(config) {} using EnvironmentProfileList::variables; using EnvironmentProfileList::saveSettings; using EnvironmentProfileList::removeProfile; }; class TestLaunchConfiguration : public ILaunchConfiguration { public: explicit TestLaunchConfiguration(const QUrl& executable = findExecutable(QStringLiteral("debuggee_debugee")), const QUrl& workingDirectory = QUrl()) { qDebug() << "FIND" << executable; c = KSharedConfig::openConfig(); c->deleteGroup("launch"); cfg = c->group("launch"); cfg.writeEntry("isExecutable", true); cfg.writeEntry("Executable", executable); cfg.writeEntry("Working Directory", workingDirectory); } const KConfigGroup config() const override { return cfg; } KConfigGroup config() override { return cfg; }; QString name() const override { return QStringLiteral("Test-Launch"); } KDevelop::IProject* project() const override { return nullptr; } KDevelop::LaunchConfigurationType* type() const override { return nullptr; } KConfig* rootConfig() { return c.data(); } private: KConfigGroup cfg; KSharedConfigPtr c; }; class TestFrameStackModel : public LldbFrameStackModel { Q_OBJECT public: explicit TestFrameStackModel(DebugSession* session) : LldbFrameStackModel(session), fetchFramesCalled(0), fetchThreadsCalled(0) {} void fetchFrames(int threadNumber, int from, int to) override { fetchFramesCalled++; LldbFrameStackModel::fetchFrames(threadNumber, from, to); } void fetchThreads() override { fetchThreadsCalled++; LldbFrameStackModel::fetchThreads(); } int fetchFramesCalled; int fetchThreadsCalled; }; class TestDebugSession : public DebugSession { Q_OBJECT public: TestDebugSession() : DebugSession() { // explicit set formatter path to force use in-tree formatters, not the one installed in system. auto formatter = findFile(LLDB_SRC_DIR, "formatters/all.py"); setFormatterPath(formatter); setSourceInitFile(false); m_frameStackModel = new TestFrameStackModel(this); KDevelop::ICore::self()->debugController()->addSession(this); } TestFrameStackModel* frameStackModel() const override { return m_frameStackModel; } private: TestFrameStackModel* m_frameStackModel; }; } // end of anonymous namespace BreakpointModel* LldbTest::breakpoints() { return m_core->debugController()->breakpointModel(); } VariableCollection *LldbTest::variableCollection() { return m_core->debugController()->variableCollection(); } Variable *LldbTest::watchVariableAt(int i) { auto watchRoot = variableCollection()->indexForItem(variableCollection()->watches(), 0); auto idx = variableCollection()->index(i, 0, watchRoot); return qobject_cast(variableCollection()->itemForIndex(idx)); } QModelIndex LldbTest::localVariableIndexAt(int i, int col) { auto localRoot = variableCollection()->indexForItem(variableCollection()->locals(), 0); return variableCollection()->index(i, col, localRoot); } // Called before the first testfunction is executed void LldbTest::initTestCase() { AutoTestShell::init(); m_core = TestCore::initialize(Core::NoUi); m_iface = m_core->pluginController() ->pluginForExtension(QStringLiteral("org.kdevelop.IExecutePlugin"), QStringLiteral("kdevexecute")) ->extension(); Q_ASSERT(m_iface); m_debugeeFileName = findSourceFile("debugee.cpp"); const QString lldbMiExecutable = QStandardPaths::findExecutable(QStringLiteral("lldb-mi")); if (lldbMiExecutable.isEmpty()) { QSKIP("Skipping, lldb-mi not available"); } } // Called after the last testfunction was executed void LldbTest::cleanupTestCase() { TestCore::shutdown(); } // Called before each testfunction is executed void LldbTest::init() { //remove all breakpoints - so we can set our own in the test KConfigGroup bpCfg = KSharedConfig::openConfig()->group("breakpoints"); bpCfg.writeEntry("number", 0); bpCfg.sync(); breakpoints()->removeRows(0, breakpoints()->rowCount()); while (variableCollection()->watches()->childCount() > 0) { auto var = watchVariableAt(0); if (!var) break; var->die(); } } void LldbTest::cleanup() { // Called after every testfunction } void LldbTest::testStdout() { auto *session = new TestDebugSession; QSignalSpy outputSpy(session, &TestDebugSession::inferiorStdoutLines); TestLaunchConfiguration cfg; QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, KDevelop::IDebugSession::EndedState); QVERIFY(outputSpy.count() > 0); QStringList outputLines; while (outputSpy.count() > 0) { - QList arguments = outputSpy.takeFirst(); + const QList arguments = outputSpy.takeFirst(); for (const auto &item : arguments) { outputLines.append(item.toStringList()); } } QCOMPARE(outputLines, QStringList() << "Hello, world!" << "Hello"); } void LldbTest::testEnvironmentSet() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeeechoenv"))); cfg.config().writeEntry("EnvironmentGroup", "LldbTestGroup"); WritableEnvironmentProfileList envProfiles(cfg.rootConfig()); envProfiles.removeProfile(QStringLiteral("LldbTestGroup")); auto &envs = envProfiles.variables(QStringLiteral("LldbTestGroup")); envs[QStringLiteral("VariableA")] = QStringLiteral("-A' \" complex --value"); envs[QStringLiteral("VariableB")] = QStringLiteral("-B' \" complex --value"); envProfiles.saveSettings(cfg.rootConfig()); QSignalSpy outputSpy(session, &TestDebugSession::inferiorStdoutLines); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, KDevelop::IDebugSession::EndedState); QVERIFY(outputSpy.count() > 0); QStringList outputLines; while (outputSpy.count() > 0) { - QList arguments = outputSpy.takeFirst(); + const QList arguments = outputSpy.takeFirst(); for (const auto &item : arguments) { outputLines.append(item.toStringList()); } } QCOMPARE(outputLines, QStringList() << "-A' \" complex --value" << "-B' \" complex --value"); } void LldbTest::testBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; KDevelop::Breakpoint * b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 29); QCOMPARE(b->state(), KDevelop::Breakpoint::NotStartedState); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(b->state(), KDevelop::Breakpoint::CleanState); QCOMPARE(session->currentLine(), 29); session->stepInto(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); session->stepInto(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); QCOMPARE(b->state(), KDevelop::Breakpoint::NotStartedState); } void LldbTest::testBreakOnStart() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; cfg.config().writeEntry(KDevMI::Config::BreakOnStartEntry, true); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::PausedState); // line 28 is the start of main function in debugee.cpp QCOMPARE(session->currentLine(), 27); // currentLine is zero-based session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testDisableBreakpoint() { QSKIP("Skipping... In lldb-mi -d flag has no effect when mixed with -f"); //Description: We must stop only on the third breakpoint int firstBreakLine=28; int secondBreakLine=23; int thirdBreakLine=24; int fourthBreakLine=31; auto *session = new TestDebugSession; TestLaunchConfiguration cfg; KDevelop::Breakpoint *b; b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), firstBreakLine); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); //this is needed to emulate debug from GUI. If we are in edit mode, the debugSession doesn't exist. m_core->debugController()->breakpointModel()->blockSignals(true); b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), secondBreakLine); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); //all disabled breakpoints were added auto *thirdBreak = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), thirdBreakLine); m_core->debugController()->breakpointModel()->blockSignals(false); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), thirdBreak->line()); //disable existing breakpoint thirdBreak->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); //add another disabled breakpoint b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), fourthBreakLine); WAIT_FOR_A_WHILE(session, 300); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); WAIT_FOR_A_WHILE(session, 300); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testChangeLocationBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; auto *b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 27); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 27); WAIT_FOR_A_WHILE(session, 100); b->setLine(28); WAIT_FOR_A_WHILE(session, 100); session->run(); WAIT_FOR_A_WHILE(session, 100); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 28); WAIT_FOR_A_WHILE(session, 500); breakpoints()->setData(breakpoints()->index(0, KDevelop::Breakpoint::LocationColumn), QString(m_debugeeFileName+":30")); QCOMPARE(b->line(), 29); WAIT_FOR_A_WHILE(session, 100); QCOMPARE(b->line(), 29); session->run(); WAIT_FOR_A_WHILE(session, 100); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 29); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testDeleteBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; QCOMPARE(breakpoints()->rowCount(), 0); //add breakpoint before startDebugging breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 21); QCOMPARE(breakpoints()->rowCount(), 1); breakpoints()->removeRow(0); QCOMPARE(breakpoints()->rowCount(), 0); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 22); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); breakpoints()->removeRow(0); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testPendingBreakpoint() { QSKIP("Skipping... Pending breakpoint not work on lldb-mi"); auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 28); auto * b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("test_lldb.cpp")), 10); QCOMPARE(b->state(), Breakpoint::NotStartedState); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::PausedState); QCOMPARE(b->state(), Breakpoint::PendingState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testUpdateBreakpoint() { // Description: user might insert breakpoints using lldb console. model should // pick up the manually set breakpoint auto *session = new TestDebugSession; TestLaunchConfiguration cfg; // break at line 29 auto b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 28); QCOMPARE(breakpoints()->rowCount(), 1); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); // stop at line 29 session->stepInto(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); // stop after step QCOMPARE(session->currentLine(), 23-1); // at the beginning of foo():23: ++i; session->addUserCommand(QStringLiteral("break set --file %1 --line %2").arg(m_debugeeFileName).arg(33)); WAIT_FOR_A_WHILE(session, 20); QCOMPARE(breakpoints()->rowCount(), 2); b = breakpoints()->breakpoint(1); QCOMPARE(b->url(), QUrl::fromLocalFile(m_debugeeFileName)); QCOMPARE(b->line(), 33-1); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); // stop at line 25 QCOMPARE(session->currentLine(), 33-1); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testIgnoreHitsBreakpoint() { QSKIP("Skipping... lldb-mi doesn't provide breakpoint hit count update"); auto *session = new TestDebugSession; TestLaunchConfiguration cfg; KDevelop::Breakpoint * b1 = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 21); b1->setIgnoreHits(1); KDevelop::Breakpoint * b2 = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 22); QVERIFY(session->startDebugging(&cfg, m_iface)); //WAIT_FOR_STATE(session, DebugSession::PausedState); WAIT_FOR(session, session->state() == DebugSession::PausedState && b2->hitCount() == 1); b2->setIgnoreHits(1); session->run(); //WAIT_FOR_STATE(session, DebugSession::PausedState); WAIT_FOR(session, session->state() == DebugSession::PausedState && b1->hitCount() == 1); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testConditionBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; auto b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 39); b->setCondition(QStringLiteral("x[0] == 'H'")); b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 23); b->setCondition(QStringLiteral("i==2")); b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 24); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR(session, session->state() == DebugSession::PausedState && session->currentLine() == 24); b->setCondition(QStringLiteral("i == 0")); WAIT_FOR_A_WHILE(session, 100); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 23); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 39); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testBreakOnWriteBreakpoint() { QSKIP("Skipping... lldb-mi doesn't have proper watchpoint support"); auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 24); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 24); breakpoints()->addWatchpoint(QStringLiteral("i")); WAIT_FOR_A_WHILE(session, 100); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 23); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 24); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testBreakOnWriteWithConditionBreakpoint() { QSKIP("Skipping... lldb-mi doesn't have proper watchpoint support"); auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 24); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 24); KDevelop::Breakpoint *b = breakpoints()->addWatchpoint(QStringLiteral("i")); b->setCondition(QStringLiteral("i==2")); QTest::qWait(100); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 23); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 24); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testBreakOnReadBreakpoint() { QSKIP("Skipping... lldb-mi doesn't have proper watchpoint support"); auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addReadWatchpoint(QStringLiteral("foo::i")); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 23); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testBreakOnReadBreakpoint2() { QSKIP("Skipping... lldb-mi doesn't have proper watchpoint support"); auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 24); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 24); breakpoints()->addReadWatchpoint(QStringLiteral("i")); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 22); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 24); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testBreakOnAccessBreakpoint() { QSKIP("Skipping... lldb-mi doesn't have proper watchpoint support"); auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 24); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 24); breakpoints()->addAccessWatchpoint(QStringLiteral("i")); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 22); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 23); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 24); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testInsertBreakpointWhileRunning() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeeslow"))); QString fileName = findSourceFile("debugeeslow.cpp"); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::ActiveState); WAIT_FOR_A_WHILE(session, 2000); qDebug() << "adding breakpoint"; KDevelop::Breakpoint *b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 25); WAIT_FOR_A_WHILE(session, 500); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); WAIT_FOR_A_WHILE(session, 500); QCOMPARE(session->currentLine(), 25); b->setDeleted(); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testInsertBreakpointWhileRunningMultiple() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeeslow"))); QString fileName = findSourceFile("debugeeslow.cpp"); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::ActiveState); WAIT_FOR_A_WHILE(session, 2000); qDebug() << "adding breakpoint"; auto b1 = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 24); auto b2 = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 25); WAIT_FOR_A_WHILE(session, 500); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); WAIT_FOR_A_WHILE(session, 500); QCOMPARE(session->currentLine(), 24); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); WAIT_FOR_A_WHILE(session, 500); QCOMPARE(session->currentLine(), 25); b1->setDeleted(); b2->setDeleted(); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testInsertBreakpointFunctionName() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QStringLiteral("main")); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 27); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testManualBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QStringLiteral("main")); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 27); breakpoints()->removeRows(0, 1); WAIT_FOR_A_WHILE(session, 100); QCOMPARE(breakpoints()->rowCount(), 0); session->addCommand(MI::NonMI, QStringLiteral("break set --file debugee.cpp --line 23")); WAIT_FOR_A_WHILE(session, 100); QCOMPARE(breakpoints()->rowCount(), 1); auto b = breakpoints()->breakpoint(0); QCOMPARE(b->line(), 22); session->addCommand(MI::NonMI, QStringLiteral("break disable 2")); session->addCommand(MI::NonMI, QStringLiteral("break modify -c 'i == 1' 2")); session->addCommand(MI::NonMI, QStringLiteral("break modify -i 1 2")); WAIT_FOR_A_WHILE(session, 1000); QCOMPARE(b->enabled(), false); QEXPECT_FAIL("", "LLDB 4.0 does not report condition in mi response", Continue); QCOMPARE(b->condition(), QString("i == 1")); QEXPECT_FAIL("", "LLDB 4.0 does not report ignore hits in mi response", Continue); QCOMPARE(b->ignoreHits(), 1); session->addCommand(MI::NonMI, QStringLiteral("break delete 2")); WAIT_FOR_A_WHILE(session, 100); QEXPECT_FAIL("", "LLDB 4.0 does not report breakpoint deletion as mi notification", Continue); QCOMPARE(breakpoints()->rowCount(), 0); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } //Bug 201771 void LldbTest::testInsertAndRemoveBreakpointWhileRunning() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeeslow"))); QString fileName = findSourceFile("debugeeslow.cpp"); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::ActiveState); WAIT_FOR_A_WHILE(session, 1000); KDevelop::Breakpoint *b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 25); WAIT_FOR_A_WHILE(session, 200); // wait for feedback notification from lldb-mi b->setDeleted(); WAIT_FOR_A_WHILE(session, 3000); // give slow debugee extra time to run WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testPickupManuallyInsertedBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QStringLiteral("main")); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); session->addCommand(MI::NonMI, QStringLiteral("break set --file debugee.cpp --line 32")); session->stepInto(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(breakpoints()->breakpoints().count(), 2); QCOMPARE(breakpoints()->rowCount(), 2); KDevelop::Breakpoint *b = breakpoints()->breakpoint(1); QVERIFY(b); QCOMPARE(b->line(), 31); //we start with 0, gdb with 1 QCOMPARE(b->url().fileName(), QString("debugee.cpp")); } //Bug 270970 void LldbTest::testPickupManuallyInsertedBreakpointOnlyOnce() { auto *session = new TestDebugSession; QString sourceFile = findSourceFile("debugee.cpp"); //inject here, so it behaves similar like a command from .lldbinit QTemporaryFile configScript; configScript.open(); configScript.write(QStringLiteral("break set --file %0 --line 32\n").arg(sourceFile).toLocal8Bit()); configScript.close(); TestLaunchConfiguration cfg; KConfigGroup grp = cfg.config(); grp.writeEntry(Config::LldbConfigScriptEntry, QUrl::fromLocalFile(configScript.fileName())); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(QStringLiteral("debugee.cpp")), 31); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(breakpoints()->breakpoints().count(), 1); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testBreakpointWithSpaceInPath() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeespace"))); KConfigGroup grp = cfg.config(); QString fileName = findSourceFile("debugee space.cpp"); KDevelop::Breakpoint * b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 20); QCOMPARE(b->state(), KDevelop::Breakpoint::NotStartedState); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 20); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testBreakpointDisabledOnStart() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; auto b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 23); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 29); b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 34); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QEXPECT_FAIL("", "See LLDB bug 28703: -d flag has no effect", Abort); QCOMPARE(session->currentLine(), 29); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Checked); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 34); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testMultipleLocationsBreakpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeemultilocbreakpoint"))); breakpoints()->addCodeBreakpoint(QStringLiteral("aPlusB")); //TODO check if the additional location breakpoint is added QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 19); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 23); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testMultipleBreakpoint() { auto *session = new TestDebugSession; //there'll be about 3-4 breakpoints, but we treat it like one. TestLaunchConfiguration c(findExecutable(QStringLiteral("debuggee_debugeemultiplebreakpoint"))); auto b = breakpoints()->addCodeBreakpoint(QStringLiteral("debugeemultiplebreakpoint.cpp:52")); QVERIFY(session->startDebugging(&c, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(breakpoints()->breakpoints().count(), 1); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testRegularExpressionBreakpoint() { QSKIP("Skipping... lldb has only one breakpoint for multiple locations" " (and lldb-mi returns the first one), not support this yet"); auto *session = new TestDebugSession; TestLaunchConfiguration c(findExecutable(QStringLiteral("debuggee_debugeemultilocbreakpoint"))); breakpoints()->addCodeBreakpoint(QStringLiteral("main")); QVERIFY(session->startDebugging(&c, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); session->addCommand(MI::NonMI, QStringLiteral("break set --func-regex .*aPl.*B")); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(breakpoints()->breakpoints().count(), 3); session->addCommand(MI::BreakDelete, QString()); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testChangeBreakpointWhileRunning() { QSKIP("Skipping... lldb-mi command -break-enable doesn't enable breakpoint"); auto *session = new TestDebugSession; TestLaunchConfiguration c(findExecutable(QStringLiteral("debuggee_debugeeslow"))); KDevelop::Breakpoint* b = breakpoints()->addCodeBreakpoint(QStringLiteral("debugeeslow.cpp:25")); QVERIFY(session->startDebugging(&c, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QVERIFY(session->currentLine() >= 24 && session->currentLine() <= 26 ); session->run(); WAIT_FOR_STATE(session, DebugSession::ActiveState); qDebug() << "Disabling breakpoint"; b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); //to make one loop WAIT_FOR_A_WHILE(session, 2500); qDebug() << "Waiting for active"; WAIT_FOR_STATE(session, DebugSession::ActiveState); qDebug() << "Enabling breakpoint"; // Use native user command works, but not through -break-enable, which is triggered by setData session->addCommand(MI::NonMI, QStringLiteral("break enable")); //b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Checked); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); b->setData(KDevelop::Breakpoint::EnableColumn, Qt::Unchecked); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testCatchpoint() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeeexception"))); session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); TestFrameStackModel* fsModel = session->frameStackModel(); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("debugeeexception.cpp")), 29); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(fsModel->currentFrame(), 0); QCOMPARE(session->currentLine(), 29); session->addCommand(MI::NonMI, QStringLiteral("break set -E c++")); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); const auto frames = fsModel->frames(fsModel->currentThread()); QVERIFY(frames.size() >= 2); // frame 0 is somewhere inside libstdc++ QCOMPARE(frames[1].file, QUrl::fromLocalFile(findSourceFile("debugeeexception.cpp"))); QCOMPARE(frames[1].line, 22); QCOMPARE(breakpoints()->rowCount(),2); QVERIFY(!breakpoints()->breakpoint(0)->location().isEmpty()); QVERIFY(!breakpoints()->breakpoint(1)->location().isEmpty()); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testShowStepInSource() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; QSignalSpy showStepInSourceSpy(session, &TestDebugSession::showStepInSource); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 29); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); session->stepInto(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); session->stepInto(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); { QCOMPARE(showStepInSourceSpy.count(), 3); QList arguments = showStepInSourceSpy.takeFirst(); QCOMPARE(arguments.first().toUrl(), QUrl::fromLocalFile(m_debugeeFileName)); QCOMPARE(arguments.at(1).toInt(), 29); arguments = showStepInSourceSpy.takeFirst(); QCOMPARE(arguments.first().toUrl(), QUrl::fromLocalFile(m_debugeeFileName)); QCOMPARE(arguments.at(1).toInt(), 22); arguments = showStepInSourceSpy.takeFirst(); QCOMPARE(arguments.first().toUrl(), QUrl::fromLocalFile(m_debugeeFileName)); QCOMPARE(arguments.at(1).toInt(), 23); } } void LldbTest::testStack() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; TestFrameStackModel *stackModel = session->frameStackModel(); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 21); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QModelIndex tIdx = stackModel->index(0,0); QCOMPARE(stackModel->rowCount(QModelIndex()), 1); QCOMPARE(stackModel->columnCount(QModelIndex()), 3); COMPARE_DATA(tIdx, "#1 at foo()"); QCOMPARE(stackModel->rowCount(tIdx), 4); QCOMPARE(stackModel->columnCount(tIdx), 3); COMPARE_DATA(stackModel->index(0, 0, tIdx), "0"); COMPARE_DATA(stackModel->index(0, 1, tIdx), "foo()"); COMPARE_DATA(stackModel->index(0, 2, tIdx), m_debugeeFileName+":23"); COMPARE_DATA(stackModel->index(1, 0, tIdx), "1"); COMPARE_DATA(stackModel->index(1, 1, tIdx), "main"); COMPARE_DATA(stackModel->index(1, 2, tIdx), m_debugeeFileName+":29"); COMPARE_DATA(stackModel->index(2, 0, tIdx), "2"); COMPARE_DATA(stackModel->index(2, 1, tIdx), "__libc_start_main"); COMPARE_DATA(stackModel->index(3, 0, tIdx), "3"); COMPARE_DATA(stackModel->index(3, 1, tIdx), "_start"); session->stepOut(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); COMPARE_DATA(tIdx, "#1 at main"); QCOMPARE(stackModel->rowCount(tIdx), 3); COMPARE_DATA(stackModel->index(0, 0, tIdx), "0"); COMPARE_DATA(stackModel->index(0, 1, tIdx), "main"); COMPARE_DATA(stackModel->index(0, 2, tIdx), m_debugeeFileName+":30"); COMPARE_DATA(stackModel->index(1, 0, tIdx), "1"); COMPARE_DATA(stackModel->index(1, 1, tIdx), "__libc_start_main"); COMPARE_DATA(stackModel->index(2, 0, tIdx), "2"); COMPARE_DATA(stackModel->index(2, 1, tIdx), "_start"); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testStackFetchMore() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeerecursion"))); QString fileName = findSourceFile("debugeerecursion.cpp"); TestFrameStackModel *stackModel = session->frameStackModel(); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 25); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->frameStackModel()->fetchFramesCalled, 1); QModelIndex tIdx = stackModel->index(0,0); QCOMPARE(stackModel->rowCount(QModelIndex()), 1); QCOMPARE(stackModel->columnCount(QModelIndex()), 3); COMPARE_DATA(tIdx, "#1 at foo()"); QCOMPARE(stackModel->rowCount(tIdx), 21); COMPARE_DATA(stackModel->index(0, 0, tIdx), "0"); COMPARE_DATA(stackModel->index(0, 1, tIdx), "foo()"); COMPARE_DATA(stackModel->index(0, 2, tIdx), fileName+":26"); COMPARE_DATA(stackModel->index(1, 0, tIdx), "1"); COMPARE_DATA(stackModel->index(1, 1, tIdx), "foo()"); COMPARE_DATA(stackModel->index(1, 2, tIdx), fileName+":24"); COMPARE_DATA(stackModel->index(2, 0, tIdx), "2"); COMPARE_DATA(stackModel->index(2, 1, tIdx), "foo()"); COMPARE_DATA(stackModel->index(2, 2, tIdx), fileName+":24"); COMPARE_DATA(stackModel->index(19, 0, tIdx), "19"); COMPARE_DATA(stackModel->index(20, 0, tIdx), "20"); stackModel->fetchMoreFrames(); WAIT_FOR_A_WHILE(session, 200); QCOMPARE(stackModel->fetchFramesCalled, 2); QCOMPARE(stackModel->rowCount(tIdx), 41); COMPARE_DATA(stackModel->index(20, 0, tIdx), "20"); COMPARE_DATA(stackModel->index(21, 0, tIdx), "21"); COMPARE_DATA(stackModel->index(22, 0, tIdx), "22"); COMPARE_DATA(stackModel->index(39, 0, tIdx), "39"); COMPARE_DATA(stackModel->index(40, 0, tIdx), "40"); stackModel->fetchMoreFrames(); WAIT_FOR_A_WHILE(session, 200); QCOMPARE(stackModel->fetchFramesCalled, 3); QCOMPARE(stackModel->rowCount(tIdx), 121); COMPARE_DATA(stackModel->index(40, 0, tIdx), "40"); COMPARE_DATA(stackModel->index(41, 0, tIdx), "41"); COMPARE_DATA(stackModel->index(42, 0, tIdx), "42"); COMPARE_DATA(stackModel->index(119, 0, tIdx), "119"); COMPARE_DATA(stackModel->index(120, 0, tIdx), "120"); stackModel->fetchMoreFrames(); WAIT_FOR_A_WHILE(session, 200); QCOMPARE(stackModel->fetchFramesCalled, 4); QCOMPARE(stackModel->rowCount(tIdx), 301); COMPARE_DATA(stackModel->index(120, 0, tIdx), "120"); COMPARE_DATA(stackModel->index(121, 0, tIdx), "121"); COMPARE_DATA(stackModel->index(122, 0, tIdx), "122"); COMPARE_DATA(stackModel->index(298, 0, tIdx), "298"); COMPARE_DATA(stackModel->index(298, 1, tIdx), "main"); COMPARE_DATA(stackModel->index(298, 2, tIdx), fileName+":30"); COMPARE_DATA(stackModel->index(299, 0, tIdx), "299"); COMPARE_DATA(stackModel->index(299, 1, tIdx), "__libc_start_main"); COMPARE_DATA(stackModel->index(300, 0, tIdx), "300"); COMPARE_DATA(stackModel->index(300, 1, tIdx), "_start"); stackModel->fetchMoreFrames(); //nothing to fetch, we are at the end WAIT_FOR_A_WHILE(session, 200); QCOMPARE(stackModel->fetchFramesCalled, 4); QCOMPARE(stackModel->rowCount(tIdx), 301); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testStackDeactivateAndActive() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; TestFrameStackModel *stackModel = session->frameStackModel(); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 21); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::PausedState); QModelIndex tIdx = stackModel->index(0,0); session->stepOut(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); COMPARE_DATA(tIdx, "#1 at main"); QCOMPARE(stackModel->rowCount(tIdx), 3); COMPARE_DATA(stackModel->index(0, 0, tIdx), "0"); COMPARE_DATA(stackModel->index(0, 1, tIdx), "main"); COMPARE_DATA(stackModel->index(0, 2, tIdx), m_debugeeFileName+":30"); COMPARE_DATA(stackModel->index(1, 0, tIdx), "1"); COMPARE_DATA(stackModel->index(1, 1, tIdx), "__libc_start_main"); COMPARE_DATA(stackModel->index(2, 0, tIdx), "2"); COMPARE_DATA(stackModel->index(2, 1, tIdx), "_start"); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testStackSwitchThread() { QSKIP("Skipping... lldb-mi crashes when break at a location with multiple threads running"); auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeethreads"))); QString fileName = findSourceFile("debugeethreads.cpp"); TestFrameStackModel *stackModel = session->frameStackModel(); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 38); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(stackModel->rowCount(), 4); QModelIndex tIdx = stackModel->index(0,0); COMPARE_DATA(tIdx, "#1 at main"); QCOMPARE(stackModel->rowCount(tIdx), 1); COMPARE_DATA(stackModel->index(0, 0, tIdx), "0"); COMPARE_DATA(stackModel->index(0, 1, tIdx), "main"); COMPARE_DATA(stackModel->index(0, 2, tIdx), fileName+":39"); tIdx = stackModel->index(1,0); QVERIFY(stackModel->data(tIdx).toString().startsWith("#2 at ")); stackModel->setCurrentThread(2); WAIT_FOR_A_WHILE(session, 200); int rows = stackModel->rowCount(tIdx); QVERIFY(rows > 3); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testAttach() { SKIP_IF_ATTACH_FORBIDDEN(); QString fileName = findSourceFile("debugeeslow.cpp"); KProcess debugeeProcess; debugeeProcess << QStringLiteral("nice") << findExecutable(QStringLiteral("debuggee_debugeeslow")).toLocalFile(); debugeeProcess.start(); QVERIFY(debugeeProcess.waitForStarted()); QTest::qWait(100); auto *session = new TestDebugSession; session->attachToProcess(debugeeProcess.pid()); WAIT_FOR_A_WHILE(session, 100); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 35); // lldb-mi silently stops when attaching to a process. Force it continue to run. session->addCommand(MI::ExecContinue, QString(), MI::CmdMaybeStartsRunning); WAIT_FOR_A_WHILE(session, 2000); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 35); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testRemoteDebugging() { KProcess gdbServer; gdbServer << QStringLiteral("lldb-server") << QStringLiteral("gdbserver") << QStringLiteral("*:1234"); gdbServer.start(); QVERIFY(gdbServer.waitForStarted()); auto *session = new TestDebugSession; TestLaunchConfiguration cfg; cfg.config().writeEntry(Config::LldbRemoteDebuggingEntry, true); cfg.config().writeEntry(Config::LldbRemoteServerEntry, "localhost:1234"); cfg.config().writeEntry(Config::LldbRemotePathEntry, "/tmp"); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 34); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testCoreFile() { QFileInfo f(QStringLiteral("core")); f.setCaching(false); // don't cache information if (f.exists()) { QVERIFY(QFile::remove(f.canonicalFilePath())); } KProcess debugeeProcess; debugeeProcess.setOutputChannelMode(KProcess::MergedChannels); debugeeProcess << QStringLiteral("bash") << QStringLiteral("-c") << "ulimit -c unlimited; " + findExecutable(QStringLiteral("debuggee_crash")).toLocalFile(); debugeeProcess.start(); debugeeProcess.waitForFinished(); qDebug() << debugeeProcess.readAll(); bool coreFileFound = f.exists(); if (!coreFileFound) { // Try to use coredumpctl qDebug() << "try to use coredumpctl"; auto coredumpctl = QStandardPaths::findExecutable(QStringLiteral("coredumpctl")); if (!coredumpctl.isEmpty()) { KProcess::execute(coredumpctl, {"-1", "-o", f.absoluteFilePath(), "dump", "debuggee_crash"}); // coredumpctl seems to create an empty file "core" even if no cores can be delivered // (like when run inside docker containers as on KDE CI or with kernel.core_pattern=|/dev/null) // so also check for size != 0 coreFileFound = f.exists() && (f.size() > 0); } } if (!coreFileFound) QSKIP("no core dump found, check your system configuration (see /proc/sys/kernel/core_pattern).", SkipSingle); auto *session = new TestDebugSession; session->examineCoreFile(findExecutable(QStringLiteral("debuggee_crash")), QUrl::fromLocalFile(f.canonicalFilePath())); TestFrameStackModel *stackModel = session->frameStackModel(); WAIT_FOR_STATE(session, DebugSession::StoppedState); QModelIndex tIdx = stackModel->index(0,0); QCOMPARE(stackModel->rowCount(QModelIndex()), 1); QCOMPARE(stackModel->columnCount(QModelIndex()), 3); COMPARE_DATA(tIdx, "#1 at foo()"); session->stopDebugger(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testVariablesLocals() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; session->variableController()->setAutoUpdate(IVariableController::UpdateLocals); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 24); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(variableCollection()->rowCount(), 2); QModelIndex i = variableCollection()->index(1, 0); COMPARE_DATA(i, "Locals"); QCOMPARE(variableCollection()->rowCount(i), 1); COMPARE_DATA(variableCollection()->index(0, 0, i), "j"); COMPARE_DATA(variableCollection()->index(0, 1, i), "1"); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); COMPARE_DATA(variableCollection()->index(0, 0, i), "j"); COMPARE_DATA(variableCollection()->index(0, 1, i), "2"); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testVariablesLocalsStruct() { auto *session = new TestDebugSession; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); TestLaunchConfiguration cfg; breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 38); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::PausedState); WAIT_FOR_A_WHILE(session, 1000); QModelIndex i = variableCollection()->index(1, 0); QCOMPARE(variableCollection()->rowCount(i), 4); int structIndex = 0; for(int j=0; j<3; ++j) { if (variableCollection()->index(j, 0, i).data().toString() == QLatin1String("ts")) { structIndex = j; } } COMPARE_DATA(variableCollection()->index(structIndex, 0, i), "ts"); COMPARE_DATA(variableCollection()->index(structIndex, 1, i), "{...}"); QModelIndex ts = variableCollection()->index(structIndex, 0, i); COMPARE_DATA(variableCollection()->index(0, 0, ts), "..."); variableCollection()->expanded(ts); WAIT_FOR_A_WHILE(session, 100); COMPARE_DATA(variableCollection()->index(0, 0, ts), "a"); COMPARE_DATA(variableCollection()->index(0, 1, ts), "0"); COMPARE_DATA(variableCollection()->index(1, 0, ts), "b"); COMPARE_DATA(variableCollection()->index(1, 1, ts), "1"); COMPARE_DATA(variableCollection()->index(2, 0, ts), "c"); COMPARE_DATA(variableCollection()->index(2, 1, ts), "2"); session->stepInto(); WAIT_FOR_STATE(session, DebugSession::PausedState); WAIT_FOR_A_WHILE(session, 1000); COMPARE_DATA(variableCollection()->index(structIndex, 0, i), "ts"); COMPARE_DATA(variableCollection()->index(structIndex, 1, i), "{...}"); COMPARE_DATA(variableCollection()->index(0, 1, ts), "1"); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testVariablesWatches() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; m_core->debugController()->variableCollection()->variableWidgetShown(); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 38); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::PausedState); variableCollection()->watches()->add(QStringLiteral("ts")); WAIT_FOR_A_WHILE(session, 300); QModelIndex i = variableCollection()->index(0, 0); QCOMPARE(variableCollection()->rowCount(i), 1); COMPARE_DATA(variableCollection()->index(0, 0, i), "ts"); COMPARE_DATA(variableCollection()->index(0, 1, i), "{...}"); QModelIndex ts = variableCollection()->index(0, 0, i); COMPARE_DATA(variableCollection()->index(0, 0, ts), "..."); variableCollection()->expanded(ts); WAIT_FOR_A_WHILE(session, 100); COMPARE_DATA(variableCollection()->index(0, 0, ts), "a"); COMPARE_DATA(variableCollection()->index(0, 1, ts), "0"); COMPARE_DATA(variableCollection()->index(1, 0, ts), "b"); COMPARE_DATA(variableCollection()->index(1, 1, ts), "1"); COMPARE_DATA(variableCollection()->index(2, 0, ts), "c"); COMPARE_DATA(variableCollection()->index(2, 1, ts), "2"); session->stepInto(); WAIT_FOR_STATE(session, DebugSession::PausedState); WAIT_FOR_A_WHILE(session, 100); COMPARE_DATA(variableCollection()->index(0, 0, i), "ts"); COMPARE_DATA(variableCollection()->index(0, 1, i), "{...}"); COMPARE_DATA(variableCollection()->index(0, 1, ts), "1"); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testVariablesWatchesQuotes() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateWatches); // the unquoted string (the actual content): t\"t // quoted string (what we would write as a c string): "t\\\"t" // written in source file: R"("t\\\"t")" const QString testString(QStringLiteral("t\\\"t")); // the actual content const QString quotedTestString(QStringLiteral(R"("t\\\"t")")); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 38); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, DebugSession::PausedState); variableCollection()->watches()->add(quotedTestString); //just a constant string WAIT_FOR_A_WHILE(session, 3000); QModelIndex i = variableCollection()->index(0, 0); QCOMPARE(variableCollection()->rowCount(i), 1); COMPARE_DATA(variableCollection()->index(0, 0, i), quotedTestString); QEXPECT_FAIL("", "LLDB 4.0 cannot deal with string literal in expression when debugging, causing memory access error", Abort); COMPARE_DATA(variableCollection()->index(0, 1, i), quotedTestString); QModelIndex testStr = variableCollection()->index(0, 0, i); COMPARE_DATA(variableCollection()->index(0, 0, testStr), "..."); variableCollection()->expanded(testStr); WAIT_FOR_A_WHILE(session, 100); int len = testString.length(); for (int ind = 0; ind < len; ind++) { COMPARE_DATA(variableCollection()->index(ind, 0, testStr), QStringLiteral("[%0]").arg(ind)); QChar c = testString.at(ind); QString value = QString::number(c.toLatin1()) + " '" + c + "'"; COMPARE_DATA(variableCollection()->index(ind, 1, testStr), value); } COMPARE_DATA(variableCollection()->index(len, 0, testStr), QStringLiteral("[%0]").arg(len)); COMPARE_DATA(variableCollection()->index(len, 1, testStr), "0 '\\0'"); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testVariablesWatchesTwoSessions() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateWatches); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 38); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); variableCollection()->watches()->add(QStringLiteral("ts")); WAIT_FOR_A_WHILE(session, 300); QModelIndex ts = variableCollection()->index(0, 0, variableCollection()->index(0, 0)); variableCollection()->expanded(ts); WAIT_FOR_A_WHILE(session, 100); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); //check if variable is marked as out-of-scope QCOMPARE(variableCollection()->watches()->childCount(), 1); auto v = qobject_cast(watchVariableAt(0)); QVERIFY(v); QVERIFY(!v->inScope()); QCOMPARE(v->childCount(), 3); v = qobject_cast(v->child(0)); QVERIFY(!v->inScope()); //start a second debug session session = new TestDebugSession; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateWatches); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(variableCollection()->watches()->childCount(), 1); ts = variableCollection()->index(0, 0, variableCollection()->index(0, 0)); v = qobject_cast(watchVariableAt(0)); QVERIFY(v); QVERIFY(v->inScope()); QCOMPARE(v->childCount(), 3); v = qobject_cast(v->child(0)); QVERIFY(v->inScope()); COMPARE_DATA(variableCollection()->indexForItem(v, 1), QString::number(0)); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); //check if variable is marked as out-of-scope v = qobject_cast(watchVariableAt(0)); QVERIFY(!v->inScope()); QVERIFY(!qobject_cast(v->child(0))->inScope()); } void LldbTest::testVariablesStopDebugger() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 38); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); session->stopDebugger(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testVariablesStartSecondSession() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 38); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); session = new TestDebugSession; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 38); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testVariablesSwitchFrame() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); TestFrameStackModel *stackModel = session->frameStackModel(); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 24); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QModelIndex i = variableCollection()->index(1, 0); COMPARE_DATA(i, "Locals"); QCOMPARE(variableCollection()->rowCount(i), 1); COMPARE_DATA(variableCollection()->index(0, 0, i), "j"); // only non-static variable works COMPARE_DATA(variableCollection()->index(0, 1, i), "1"); stackModel->setCurrentFrame(1); WAIT_FOR_A_WHILE(session, 200); i = variableCollection()->index(1, 0); QCOMPARE(variableCollection()->rowCount(i), 4); COMPARE_DATA(variableCollection()->index(2, 0, i), "argc"); COMPARE_DATA(variableCollection()->index(2, 1, i), "1"); COMPARE_DATA(variableCollection()->index(3, 0, i), "argv"); breakpoints()->removeRow(0); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testVariablesQuicklySwitchFrame() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); TestFrameStackModel *stackModel = session->frameStackModel(); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 24); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QModelIndex i = variableCollection()->index(1, 0); COMPARE_DATA(i, "Locals"); QCOMPARE(variableCollection()->rowCount(i), 1); COMPARE_DATA(variableCollection()->index(0, 0, i), "j"); // only non-static variable works COMPARE_DATA(variableCollection()->index(0, 1, i), "1"); stackModel->setCurrentFrame(1); WAIT_FOR_A_WHILE(session, 300); stackModel->setCurrentFrame(0); WAIT_FOR_A_WHILE(session, 1); stackModel->setCurrentFrame(1); WAIT_FOR_A_WHILE(session, 1); stackModel->setCurrentFrame(0); WAIT_FOR_A_WHILE(session, 1); stackModel->setCurrentFrame(1); WAIT_FOR_A_WHILE(session, 500); i = variableCollection()->index(1, 0); QCOMPARE(variableCollection()->rowCount(i), 4); QStringList locs; for (int j = 0; j < variableCollection()->rowCount(i); ++j) { locs << variableCollection()->index(j, 0, i).data().toString(); } QVERIFY(locs.contains("argc")); QVERIFY(locs.contains("argv")); QVERIFY(locs.contains("x")); breakpoints()->removeRow(0); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testSwitchFrameLldbConsole() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; TestFrameStackModel *stackModel = session->frameStackModel(); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 24); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(stackModel->currentFrame(), 0); stackModel->setCurrentFrame(1); QCOMPARE(stackModel->currentFrame(), 1); WAIT_FOR_A_WHILE(session, 500); QCOMPARE(stackModel->currentFrame(), 1); session->addUserCommand(QStringLiteral("print i")); WAIT_FOR_A_WHILE(session, 500); //currentFrame must not reset to 0; Bug 222882 QCOMPARE(stackModel->currentFrame(), 1); } void LldbTest::testSegfaultDebugee() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_crash"))); session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateLocals); QString fileName = findSourceFile("debugeecrash.cpp"); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(fileName), 23); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 23); session->run(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 24); session->stopDebugger(); WAIT_FOR_STATE(session, DebugSession::EndedState); } //Bug 274390 void LldbTest::testCommandOrderFastStepping() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeeqt"))); breakpoints()->addCodeBreakpoint(QStringLiteral("main")); QVERIFY(session->startDebugging(&cfg, m_iface)); for(int i=0; i<20; i++) { session->stepInto(); } WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testRunLldbScript() { auto *session = new TestDebugSession; QTemporaryFile runScript; runScript.open(); runScript.write(QStringLiteral("break set --file %1 --line 35\n").arg(findSourceFile("debugee.cpp")).toUtf8()); runScript.close(); TestLaunchConfiguration cfg; KConfigGroup grp = cfg.config(); grp.writeEntry(Config::LldbConfigScriptEntry, QUrl::fromLocalFile(runScript.fileName())); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(session->currentLine(), 35); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testBug301287() { auto *session = new TestDebugSession; TestLaunchConfiguration cfg; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateWatches); breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 28); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); variableCollection()->watches()->add(QStringLiteral("argc")); WAIT_FOR_A_WHILE(session, 300); QModelIndex i = variableCollection()->index(0, 0); QCOMPARE(variableCollection()->rowCount(i), 1); COMPARE_DATA(variableCollection()->index(0, 0, i), "argc"); COMPARE_DATA(variableCollection()->index(0, 1, i), "1"); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); //start second debug session (same cfg) session = new TestDebugSession; session->variableController()->setAutoUpdate(KDevelop::IVariableController::UpdateWatches); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); i = variableCollection()->index(0, 0); QCOMPARE(variableCollection()->rowCount(i), 1); COMPARE_DATA(variableCollection()->index(0, 0, i), "argc"); COMPARE_DATA(variableCollection()->index(0, 1, i), "1"); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void LldbTest::testDebugInExternalTerminal() { TestLaunchConfiguration cfg; const QStringList consoles { "konsole", "xterm", "xfce4-terminal", "gnome-terminal" }; for (const QString& console : consoles) { if (QStandardPaths::findExecutable(console).isEmpty()) { continue; } auto* session = new TestDebugSession(); cfg.config().writeEntry("External Terminal"/*ExecutePlugin::terminalEntry*/, console); cfg.config().writeEntry("Use External Terminal"/*ExecutePlugin::useTerminalEntry*/, true); KDevelop::Breakpoint* b = breakpoints()->addCodeBreakpoint(QUrl::fromLocalFile(m_debugeeFileName), 28); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(b->state(), KDevelop::Breakpoint::CleanState); session->stepInto(); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } } void LldbTest::testSpecialPath() { QSKIP("Skipping... lldb-mi itself can't handle path with space in application dir"); auto* session = new TestDebugSession; auto debugee = findExecutable(QStringLiteral("path with space/debuggee_spacedebugee")); TestLaunchConfiguration c(debugee, KIO::upUrl(debugee)); KDevelop::Breakpoint* b = breakpoints()->addCodeBreakpoint(QStringLiteral("spacedebugee.cpp:30")); QCOMPARE(b->state(), KDevelop::Breakpoint::NotStartedState); QVERIFY(session->startDebugging(&c, m_iface)); WAIT_FOR_STATE_AND_IDLE(session, DebugSession::PausedState); QCOMPARE(b->state(), KDevelop::Breakpoint::CleanState); session->run(); WAIT_FOR_STATE(session, DebugSession::EndedState); } void KDevMI::LLDB::LldbTest::testEnvironmentCd() { auto *session = new TestDebugSession; QSignalSpy outputSpy(session, &TestDebugSession::inferiorStdoutLines); auto path = KIO::upUrl(findExecutable(QStringLiteral("path with space/debuggee_spacedebugee"))); TestLaunchConfiguration cfg(findExecutable(QStringLiteral("debuggee_debugeepath")), path); QVERIFY(session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE(session, KDevelop::IDebugSession::EndedState); QVERIFY(outputSpy.count() > 0); QStringList outputLines; while (outputSpy.count() > 0) { - QList arguments = outputSpy.takeFirst(); + const QList arguments = outputSpy.takeFirst(); for (const auto &item : arguments) { outputLines.append(item.toStringList()); } } QCOMPARE(outputLines, QStringList() << path.toLocalFile()); } QTEST_MAIN(KDevMI::LLDB::LldbTest) #include "test_lldb.moc" diff --git a/plugins/lldb/unittests/test_lldbformatters.cpp b/plugins/lldb/unittests/test_lldbformatters.cpp index 5819f1ab5a..ced4f8bab8 100644 --- a/plugins/lldb/unittests/test_lldbformatters.cpp +++ b/plugins/lldb/unittests/test_lldbformatters.cpp @@ -1,1032 +1,1032 @@ /* * Unit tests for LLDB debugger plugin * Copyright 2016 Aetf * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "test_lldbformatters.h" #include "controllers/variable.h" #include "controllers/variablecontroller.h" #include "debugsession.h" #include "stringhelpers.h" #include "tests/testhelper.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define WAIT_FOR_STATE(session, state) \ do { if (!KDevMI::waitForState((session), (state), __FILE__, __LINE__)) return; } while (0) #define WAIT_FOR_STATE_AND_IDLE(session, state) \ do { if (!KDevMI::waitForState((session), (state), __FILE__, __LINE__, true)) return; } while (0) #define WAIT_FOR_A_WHILE_AND_IDLE(session, ms) \ do { if (!KDevMI::waitForAWhile((session), (ms), __FILE__, __LINE__)) return; \ if (!KDevMI::waitForState((session), DebugSession::PausedState, __FILE__, __LINE__, true)) \ return; \ } while (0) #define WAIT_FOR(session, condition) \ do { \ KDevMI::TestWaiter w((session), #condition, __FILE__, __LINE__); \ while (w.waitUnless((condition))) /* nothing */ ; \ } while(0) #define COMPARE_DATA(index, expected) \ do { if (!KDevMI::compareData((index), (expected), __FILE__, __LINE__)) return; } while (0) #define VERIFY_LOCAL(row, name, summary, children) \ do { \ if (!verifyVariable((row), (name), (summary), (children), __FILE__, __LINE__)) \ return; \ } while (0) #define VERIFY_WATCH(row, name, summary, children) \ do { \ if (!verifyVariable((row), (name), (summary), (children), __FILE__, __LINE__, false)) \ return; \ } while (0) using namespace KDevelop; using namespace KDevMI::LLDB; using KDevMI::findExecutable; using KDevMI::findSourceFile; using KDevMI::findFile; using KDevMI::compareData; class TestLaunchConfiguration : public ILaunchConfiguration { public: explicit TestLaunchConfiguration(const QString& executable, const QUrl& workingDirectory = QUrl()) { auto execPath = findExecutable(executable); qDebug() << "FIND" << execPath; c = KSharedConfig::openConfig(); c->deleteGroup("launch"); cfg = c->group("launch"); cfg.writeEntry("isExecutable", true); cfg.writeEntry("Executable", execPath); cfg.writeEntry("Working Directory", workingDirectory); } const KConfigGroup config() const override { return cfg; } KConfigGroup config() override { return cfg; }; QString name() const override { return QStringLiteral("Test-Launch"); } KDevelop::IProject* project() const override { return nullptr; } KDevelop::LaunchConfigurationType* type() const override { return nullptr; } private: KConfigGroup cfg; KSharedConfigPtr c; }; class TestDebugSession : public DebugSession { Q_OBJECT public: TestDebugSession() : DebugSession() { setSourceInitFile(false); // explicit set formatter path to force use in-tree formatters, not the one installed in system. auto formatter = findFile(LLDB_SRC_DIR, "formatters/all.py"); setFormatterPath(formatter); KDevelop::ICore::self()->debugController()->addSession(this); variableController()->setAutoUpdate(IVariableController::UpdateLocals); } }; VariableCollection *LldbFormattersTest::variableCollection() { return m_core->debugController()->variableCollection(); } QModelIndex LldbFormattersTest::watchVariableIndexAt(int i, int col) { auto watchRoot = variableCollection()->indexForItem(variableCollection()->watches(), 0); return variableCollection()->index(i, col, watchRoot); } QModelIndex LldbFormattersTest::localVariableIndexAt(int i, int col) { auto localRoot = variableCollection()->indexForItem(variableCollection()->locals(), 0); return variableCollection()->index(i, col, localRoot); } // Note: line is zero-based KDevelop::Breakpoint* LldbFormattersTest::addCodeBreakpoint(const QUrl& location, int line) { return m_core->debugController()->breakpointModel()->addCodeBreakpoint(location, line); } // Called before the first testfunction is executed void LldbFormattersTest::initTestCase() { AutoTestShell::init({QStringLiteral("kdevlldb"), QStringLiteral("kdevexecute")}); m_core = TestCore::initialize(Core::NoUi); m_iface = m_core->pluginController() ->pluginForExtension(QStringLiteral("org.kdevelop.IExecutePlugin"), QStringLiteral("kdevexecute")) ->extension(); Q_ASSERT(m_iface); const QString lldbMiExecutable = QStandardPaths::findExecutable(QStringLiteral("lldb-mi")); if (lldbMiExecutable.isEmpty()) { QSKIP("Skipping, lldb-mi not available"); } } // Called after the last testfunction was executed void LldbFormattersTest::cleanupTestCase() { TestCore::shutdown(); } // Called before each testfunction is executed void LldbFormattersTest::init() { //remove all breakpoints - so we can set our own in the test KConfigGroup bpCfg = KSharedConfig::openConfig()->group("breakpoints"); bpCfg.writeEntry("number", 0); bpCfg.sync(); auto count = m_core->debugController()->breakpointModel()->rowCount(); m_core->debugController()->breakpointModel()->removeRows(0, count); while (variableCollection()->watches()->childCount() > 0) { auto idx = watchVariableIndexAt(0); auto var = qobject_cast(variableCollection()->itemForIndex(idx)); if (!var) break; var->die(); } m_session = new TestDebugSession; } void LldbFormattersTest::cleanup() { // Called after every testfunction if (m_session) m_session->stopDebugger(); WAIT_FOR_STATE(m_session, DebugSession::EndedState); m_session.clear(); } bool LldbFormattersTest::verifyVariable(int index, const QString &name, const QString &expectedSummary, QStringList expectedChildren, const char *file, int line, bool isLocal, bool useRE, bool unordered) { QList> childrenPairs; childrenPairs.reserve(expectedChildren.size()); if (unordered) { qDebug() << "useRE set to true when unordered = true"; useRE = true; expectedChildren.sort(); for (auto& c : expectedChildren) { childrenPairs << qMakePair(QStringLiteral(R"(^\[\d+\]$)"), c); } } else { for (int i = 0; i != expectedChildren.size(); ++i) { childrenPairs << qMakePair(QStringLiteral("[%0]").arg(i), expectedChildren[i]); } } return verifyVariable(index, name, expectedSummary, childrenPairs, file, line, isLocal, useRE, unordered); } bool LldbFormattersTest::verifyVariable(int index, const QString &name, const QString &expectedSummary, QList> expectedChildren, const char *file, int line, bool isLocal, bool useRE, bool unordered) { QModelIndex varIdx, summaryIdx; if (isLocal) { varIdx = localVariableIndexAt(index, 0); summaryIdx = localVariableIndexAt(index, 1); } else { varIdx = watchVariableIndexAt(index, 0); summaryIdx = watchVariableIndexAt(index, 1); } if (!compareData(varIdx, name, file, line)) { return false; } if (!compareData(summaryIdx, expectedSummary, file, line, useRE)) { return false; } // fetch all children auto var = variableCollection()->itemForIndex(varIdx); auto childCount = 0; while (childCount != variableCollection()->rowCount(varIdx)) { childCount = variableCollection()->rowCount(varIdx); var->fetchMoreChildren(); if (!waitForAWhile(m_session, 50, file, line)) return false; } if (childCount != expectedChildren.length()) { QTest::qFail(qPrintable(QString("'%0' didn't match expected '%1' in %2:%3") .arg(childCount).arg(expectedChildren.length()).arg(file).arg(line)), file, line); return false; } QVector theOrder; theOrder.reserve(childCount); for (int i = 0; i != childCount; ++i) { theOrder.push_back(i); } if (unordered) { qDebug() << "actual list sorted for unordered compare"; std::sort(theOrder.begin(), theOrder.end(), [&](int a, int b){ auto indexA = variableCollection()->index(a, 1, varIdx); auto indexB = variableCollection()->index(b, 1, varIdx); return indexA.model()->data(indexA, Qt::DisplayRole).toString() < indexB.model()->data(indexB, Qt::DisplayRole).toString(); }); std::sort(expectedChildren.begin(), expectedChildren.end(), [](const QPair &a, const QPair &b){ return a.second < b.second; }); qDebug() << "sorted actual order" << theOrder; qDebug() << "sorted expectedChildren" << expectedChildren; } for (int i = 0; i != childCount; ++i) { if (!compareData(variableCollection()->index(theOrder[i], 0, varIdx), expectedChildren[i].first, file, line, useRE)) { return false; } if (!compareData(variableCollection()->index(theOrder[i], 1, varIdx), expectedChildren[i].second, file, line, useRE)) { return false; } } return true; } void LldbFormattersTest::testQChar() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qchar")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qchar.cpp")), 4); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QList> children; children << qMakePair(QStringLiteral("ucs"), QStringLiteral("107")); VERIFY_LOCAL(0, "c", "'k'", children); } void LldbFormattersTest::testQString() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qstring")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qstring.cpp")), 4); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QString expected = QStringLiteral("test最后一个不是特殊字符'\"\\u6211"); QStringList children; - for (auto ch : expected) { + for (auto ch : qAsConst(expected)) { children << Utils::quote(ch, '\''); } VERIFY_LOCAL(0, "s", Utils::quote(expected), children); m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 5); expected.append("x"); children << QStringLiteral("'x'"); VERIFY_LOCAL(0, "s", Utils::quote(expected), children); m_session->run(); WAIT_FOR_STATE(m_session, DebugSession::EndedState); } void LldbFormattersTest::testQByteArray() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qbytearray")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qbytearray.cpp")), 4); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QStringList charlist { R"(-26 '\xe6')", R"(-104 '\x98')", R"(-81 '\xaf')", R"(39 ''')", R"(34 '"')", R"(92 '\')", R"(117 'u')", R"(54 '6')", R"(50 '2')", R"(49 '1')", R"(49 '1')", }; VERIFY_LOCAL(0, "ba", R"("\xe6\x98\xaf'\"\\u6211")", charlist); m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 5); charlist << QStringLiteral("120 'x'"); VERIFY_LOCAL(0, "ba", R"("\xe6\x98\xaf'\"\\u6211x")", charlist); m_session->run(); WAIT_FOR_STATE(m_session, DebugSession::EndedState); } void LldbFormattersTest::testQListContainer_data() { QTest::addColumn("container"); QTest::addColumn("unordered"); QTest::newRow("QList") << "QList" << false; QTest::newRow("QQueue") << "QQueue" << false; QTest::newRow("QVector") << "QVector" << false; QTest::newRow("QStack") << "QStack" << false; QTest::newRow("QLinkedList") << "QLinkedList" << false; QTest::newRow("QSet") << "QSet" << true; } void LldbFormattersTest::testQListContainer() { QFETCH(QString, container); QFETCH(bool, unordered); TestLaunchConfiguration cfg(QStringLiteral("debuggee_qlistcontainer")); cfg.config().writeEntry(KDevMI::Config::BreakOnStartEntry, true); auto watchRoot = variableCollection()->indexForItem(variableCollection()->watches(), 0); variableCollection()->expanded(watchRoot); variableCollection()->variableWidgetShown(); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); m_session->addUserCommand(QStringLiteral("break set --func doStuff<%1>()").arg(container)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); m_session->run(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 33); // line 34: intList << 10 << 20; auto var = variableCollection()->watches()->add(QStringLiteral("intList")); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("intList"), QStringLiteral(""), QStringList{}, __FILE__, __LINE__, false, false, unordered)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 34); // line 35: intList << 30; variableCollection()->expanded(watchVariableIndexAt(0)); // expand this node for correct update. WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("intList"), QStringLiteral(""), QStringList{"10", "20"}, __FILE__, __LINE__, false, false, unordered)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 36); // line 37: Container stringList; if (!verifyVariable(0, QStringLiteral("intList"), QStringLiteral(""), QStringList{"10", "20", "30"}, __FILE__, __LINE__, false, false, unordered)) { return; } var->die(); // m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 37); // line 38: stringList << "a" << "bc"; var = variableCollection()->watches()->add(QStringLiteral("stringList")); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("stringList"), QStringLiteral(""), QStringList{}, __FILE__, __LINE__, false, false, unordered)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 38); // line 39: stringList << "d"; variableCollection()->expanded(watchVariableIndexAt(0)); // expand this node for correct update. WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("stringList"), QStringLiteral(""), QStringList{"\"a\"", "\"bc\""}, __FILE__, __LINE__, false, false, unordered)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 40); // line 41: Container structList; if (!verifyVariable(0, QStringLiteral("stringList"), QStringLiteral(""), QStringList{"\"a\"", "\"bc\"", "\"d\""}, __FILE__, __LINE__, false, false, unordered)) { return; } var->die(); // m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 41); // line 42: structList << A(QStringLiteral("a"), QStringLiteral("b"), 100, -200); var = variableCollection()->watches()->add(QStringLiteral("structList")); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("structList"), QStringLiteral(""), QStringList{}, __FILE__, __LINE__, false, false, unordered)) { return; } m_session->runUntil({}, 43); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 42); // line 43: structList << A(); variableCollection()->expanded(watchVariableIndexAt(0)); // expand this node for correct update. WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("structList"), QStringLiteral(""), QStringList{"{...}"}, __FILE__, __LINE__, false, false, unordered)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 44); // line 45: Container pointerList; if (!verifyVariable(0, QStringLiteral("structList"), QStringLiteral(""), QStringList{"{...}", "{...}"}, __FILE__, __LINE__, false, false, unordered)) { return; } var->die(); // m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 45); // line 46: pointerList << new int(1) << new int(2); var = variableCollection()->watches()->add(QStringLiteral("pointerList")); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("pointerList"), QStringLiteral(""), QStringList{}, __FILE__, __LINE__, false, false, unordered)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 46); // line 47: pointerList << new int(3); variableCollection()->expanded(watchVariableIndexAt(0)); // expand this node for correct update. WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("pointerList"), QStringLiteral(""), QStringList{"^0x[0-9A-Fa-f]+$", "^0x[0-9A-Fa-f]+$"}, __FILE__, __LINE__, false, true, unordered)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 47); // line 48: qDeleteAll(pointerList); if (!verifyVariable(0, QStringLiteral("pointerList"), QStringLiteral(""), QStringList{"^0x[0-9A-Fa-f]+$", "^0x[0-9A-Fa-f]+$", "^0x[0-9A-Fa-f]+$"}, __FILE__, __LINE__, false, true, unordered)) { return; } var->die(); m_session->stepOver(); // step over qDeleteAll // > m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 50); // line 51: pairList << QPair(1, 2) << qMakePair(2, 3); var = variableCollection()->watches()->add(QStringLiteral("pairList")); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); VERIFY_WATCH(0, "pairList", "", QStringList{}); m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 51); // line 52: pairList << qMakePair(4, 5); variableCollection()->expanded(watchVariableIndexAt(0)); // expand this node for correct update. WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("pairList"), QStringLiteral(""), QStringList{"{...}", "{...}"}, __FILE__, __LINE__, false, false, unordered)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 54); // line 55: int i = 0; if (!verifyVariable(0, QStringLiteral("pairList"), QStringLiteral(""), QStringList{"{...}", "{...}", "{...}"}, __FILE__, __LINE__, false, false, unordered)) { return; } var->die(); } void LldbFormattersTest::testQListPOD() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qlistpod")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qlistpod.cpp")), 30); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); auto watchRoot = variableCollection()->indexForItem(variableCollection()->watches(), 0); variableCollection()->expanded(watchRoot); variableCollection()->variableWidgetShown(); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); variableCollection()->watches()->add(QStringLiteral("b")); variableCollection()->watches()->add(QStringLiteral("c")); variableCollection()->watches()->add(QStringLiteral("uc")); variableCollection()->watches()->add(QStringLiteral("s")); variableCollection()->watches()->add(QStringLiteral("us")); variableCollection()->watches()->add(QStringLiteral("i")); variableCollection()->watches()->add(QStringLiteral("ui")); variableCollection()->watches()->add(QStringLiteral("l")); variableCollection()->watches()->add(QStringLiteral("ul")); variableCollection()->watches()->add(QStringLiteral("i64")); variableCollection()->watches()->add(QStringLiteral("ui64")); variableCollection()->watches()->add(QStringLiteral("f")); variableCollection()->watches()->add(QStringLiteral("d")); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); VERIFY_WATCH(0, "b", "", (QStringList{"false"})); VERIFY_WATCH(1, "c", "", (QStringList{"50 '2'"})); VERIFY_WATCH(2, "uc", "", (QStringList{"50 '2'"})); VERIFY_WATCH(3, "s", "", (QStringList{"50"})); VERIFY_WATCH(4, "us", "", (QStringList{"50"})); VERIFY_WATCH(5, "i", "", (QStringList{"50"})); VERIFY_WATCH(6, "ui", "", (QStringList{"50"})); VERIFY_WATCH(7, "l", "", (QStringList{"50"})); VERIFY_WATCH(8, "ul", "", (QStringList{"50"})); VERIFY_WATCH(9, "i64", "", (QStringList{"50"})); VERIFY_WATCH(10, "ui64", "", (QStringList{"50"})); VERIFY_WATCH(11, "f", "", (QStringList{"50"})); VERIFY_WATCH(12, "d", "", (QStringList{"50"})); } void LldbFormattersTest::testQMapInt() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qmapint")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qmapint.cpp")), 6); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); VERIFY_LOCAL(0, "m", "", (QStringList{"(10, 100)", "(20, 200)"})); m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 7); VERIFY_LOCAL(0, "m", "", (QStringList{"(10, 100)", "(20, 200)", "(30, 300)"})); } void LldbFormattersTest::testQMapString() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qmapstring")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qmapstring.cpp")), 7); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 7); // line 8: m[QStringLiteral("30")] = QStringLiteral("300"); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); VERIFY_LOCAL(0, "m", "", (QStringList{"(\"10\", \"100\")", "(\"20\", \"200\")"})); m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 8); // line 9: return 0; VERIFY_LOCAL(0, "m", "", (QStringList{"(\"10\", \"100\")", "(\"20\", \"200\")", "(\"30\", \"300\")"})); } void LldbFormattersTest::testQMapStringBool() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qmapstringbool")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qmapstringbool.cpp")), 7); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); VERIFY_LOCAL(0, "m", "", (QStringList{"(\"10\", true)", "(\"20\", false)"})); m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 8); VERIFY_LOCAL(0, "m", "", (QStringList{"(\"10\", true)", "(\"20\", false)", "(\"30\", true)"})); } void LldbFormattersTest::testQHashInt() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qhashint")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qhashint.cpp")), 6); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("h"), QStringLiteral(""), QStringList{"(10, 100)", "(20, 200)"}, __FILE__, __LINE__, true, false, true)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 7); if (!verifyVariable(0, QStringLiteral("h"), QStringLiteral(""), QStringList{"(10, 100)", "(20, 200)", "(30, 300)"}, __FILE__, __LINE__, true, false, true)) { return; } } void LldbFormattersTest::testQHashString() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qhashstring")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qhashstring.cpp")), 7); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("h"), QStringLiteral(""), QStringList{"(\"10\", \"100\")", "(\"20\", \"200\")"}, __FILE__, __LINE__, true, false, true)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 8); if (!verifyVariable(0, QStringLiteral("h"), QStringLiteral(""), {"(\"10\", \"100\")", "(\"20\", \"200\")", "(\"30\", \"300\")"}, __FILE__, __LINE__, true, false, true)) { return; } } void LldbFormattersTest::testQSetInt() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qsetint")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qsetint.cpp")), 6); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("s"), QStringLiteral(""), QStringList{"10", "20"}, __FILE__, __LINE__, true, false, true)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 7); if (!verifyVariable(0, QStringLiteral("s"), QStringLiteral(""), QStringList{"10", "20", "30"}, __FILE__, __LINE__, true, false, true)) { return; } } void LldbFormattersTest::testQSetString() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qsetstring")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qsetstring.cpp")), 7); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("s"), QStringLiteral(""), QStringList{"\"10\"", "\"20\""}, __FILE__, __LINE__, true, false, true)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 8); if (!verifyVariable(0, QStringLiteral("s"), QStringLiteral(""), {"\"10\"", "\"20\"", "\"30\""}, __FILE__, __LINE__, true, false, true)) { return; } } void LldbFormattersTest::testQDate() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qdate")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qdate.cpp")), 5); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QList> children; children.append({QStringLiteral("jd"), QStringLiteral("2455217")}); children.append({QStringLiteral("(ISO)"), QStringLiteral("\"2010-01-20\"")}); children.append({QStringLiteral("(Locale)"), QStringLiteral("\".+\"")}); // (Locale) and summary are locale dependent if (!verifyVariable(0, QStringLiteral("d"), QStringLiteral(".+"), children, __FILE__, __LINE__, true, true, false)) { return; } } void LldbFormattersTest::testQTime() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qtime")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qtime.cpp")), 5); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QList> children; children.append({QStringLiteral("mds"), QStringLiteral("55810123")}); children.append({QStringLiteral("(ISO)"), QStringLiteral("\"15:30:10.000123\"")}); children.append({QStringLiteral("(Locale)"), QStringLiteral("\".+\"")}); // (Locale) and summary are locale dependent if (!verifyVariable(0, QStringLiteral("t"), QStringLiteral(".+"), children, __FILE__, __LINE__, true, true, false)) { return; } } void LldbFormattersTest::testQDateTime() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qdatetime")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qdatetime.cpp")), 5); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QList> children; children.append({QStringLiteral("toTime_t"), QStringLiteral("1264019473")}); children.append({QStringLiteral("(ISO)"), QStringLiteral("\"2010-01-20 20:31:13\"")}); children.append({QStringLiteral("(Locale)"), QStringLiteral("\".+\"")}); // (Locale), (UTC) and summary are locale dependent children.append({QStringLiteral("(UTC)"), QStringLiteral("\".+\"")}); if (!verifyVariable(0, QStringLiteral("dt"), QStringLiteral(".+"), children, __FILE__, __LINE__, true, true, false)) { return; } } void LldbFormattersTest::testQUrl() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qurl")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qurl.cpp")), 4); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QList> children; children.append({QStringLiteral("(port)"), QStringLiteral("12345")}); children.append({QStringLiteral("(scheme)"), QStringLiteral("\"http\"")}); children.append({QStringLiteral("(userName)"), QStringLiteral("\"user\"")}); children.append({QStringLiteral("(password)"), QStringLiteral("")}); children.append({QStringLiteral("(host)"), QStringLiteral("\"www.kdevelop.org\"")}); children.append({QStringLiteral("(path)"), QStringLiteral("\"/foo\"")}); children.append({QStringLiteral("(query)"), QStringLiteral("\"xyz=bar\"")}); children.append({QStringLiteral("(fragment)"), QStringLiteral("\"asdf\"")}); VERIFY_LOCAL(0, "u", "\"http://user@www.kdevelop.org:12345/foo?xyz=bar#asdf\"", children); } void LldbFormattersTest::testQUuid() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_quuid")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("quuid.cpp")), 4); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); VERIFY_LOCAL(0, "id", "QUuid({9ec3b70b-d105-42bf-b3b4-656e44d2e223})", (QStringList{})); } void LldbFormattersTest::testKTextEditorTypes() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_ktexteditortypes")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("ktexteditortypes.cpp")), 8); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); auto watchRoot = variableCollection()->indexForItem(variableCollection()->watches(), 0); variableCollection()->expanded(watchRoot); variableCollection()->variableWidgetShown(); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); variableCollection()->watches()->add(QStringLiteral("cursor")); variableCollection()->watches()->add(QStringLiteral("range")); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QList> cursorChildren; cursorChildren.append({QStringLiteral("m_line"), QStringLiteral("1")}); cursorChildren.append({QStringLiteral("m_column"), QStringLiteral("1")}); QList> rangeChildren; rangeChildren.append({QStringLiteral("m_start"), QStringLiteral("(1, 1)")}); rangeChildren.append({QStringLiteral("m_end"), QStringLiteral("(2, 2)")}); VERIFY_WATCH(0, "cursor", "(1, 1)", cursorChildren); VERIFY_WATCH(1, "range", "[(1, 1) -> (2, 2)]", rangeChildren); } void LldbFormattersTest::testKDevelopTypes() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_kdeveloptypes")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("kdeveloptypes.cpp")), 11); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); auto watchRoot = variableCollection()->indexForItem(variableCollection()->watches(), 0); variableCollection()->expanded(watchRoot); variableCollection()->variableWidgetShown(); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); variableCollection()->watches()->add(QStringLiteral("path1")); variableCollection()->watches()->add(QStringLiteral("path2")); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QList> path1Children; path1Children.append({QStringLiteral("m_data"), QStringLiteral("")}); QList> path2Children; path2Children.append({QStringLiteral("m_data"), QStringLiteral("")}); VERIFY_WATCH(0, "path1", "(\"tmp\", \"foo\")", path1Children); VERIFY_WATCH(1, "path2", "(\"http://www.test.com\", \"tmp\", \"asdf.txt\")", path2Children); } QTEST_MAIN(LldbFormattersTest) #include "test_lldbformatters.moc" diff --git a/plugins/perforce/ui/perforceimportmetadatawidget.cpp b/plugins/perforce/ui/perforceimportmetadatawidget.cpp index 60bf919104..98c3fc4bea 100644 --- a/plugins/perforce/ui/perforceimportmetadatawidget.cpp +++ b/plugins/perforce/ui/perforceimportmetadatawidget.cpp @@ -1,214 +1,214 @@ /*************************************************************************** * This file is part of KDevelop Perforce plugin, KDE project * * * * Copyright 2018 Morten Danielsen Volden * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "perforceimportmetadatawidget.h" #include #include #include #include #include using namespace KDevelop; PerforceImportMetadataWidget::PerforceImportMetadataWidget(QWidget* parent) : VcsImportMetadataWidget(parent) , m_ui(new Ui::PerforceImportMetadataWidget) { m_ui->setupUi(this); m_ui->executableLoc->setText("/usr/bin/p4"); m_ui->p4portEdit->setText("perforce:1666"); QProcessEnvironment curEnv = QProcessEnvironment::systemEnvironment(); m_ui->p4configEdit->setText(curEnv.contains("P4CONFIG") ? curEnv.value("P4CONFIG") : ""); m_ui->p4portEdit->setText(curEnv.contains("P4PORT") ? curEnv.value("P4PORT") : ""); m_ui->p4userEdit->setText(curEnv.contains("P4USER") ? curEnv.value("P4USER") : ""); curEnv.contains("P4CONFIG") ? m_ui->radioButtonConfig->setChecked(true) : m_ui->radioButtonVariables->setChecked(true); curEnv.contains("P4CONFIG") ? m_ui->p4configEdit->setEnabled(true) : m_ui->p4configEdit->setEnabled(false); m_ui->sourceLoc->setEnabled(false); m_ui->sourceLoc->setMode(KFile::Directory); m_ui->errorMsg->setTextColor(QColor(255, 0, 0)); m_ui->errorMsg->setReadOnly(true); m_ui->p4clientEdit->setEditable(true); connect(m_ui->p4clientEdit, QOverload<>::of(&KComboBox::returnPressed), this, &PerforceImportMetadataWidget::changed); connect(m_ui->radioButtonConfig, &QRadioButton::clicked, m_ui->p4configEdit, &QLineEdit::setEnabled); connect(m_ui->radioButtonVariables, &QRadioButton::clicked, m_ui->p4configEdit, &QLineEdit::setDisabled); connect(m_ui->testP4setupButton, &QPushButton::pressed, this, &PerforceImportMetadataWidget::testP4setup); } QUrl PerforceImportMetadataWidget::source() const { return m_ui->sourceLoc->url(); } VcsLocation PerforceImportMetadataWidget::destination() const { VcsLocation dest; dest.setRepositoryServer(m_ui->p4portEdit->text()); dest.setUserData(QVariant::fromValue(m_ui->p4userEdit->text())); dest.setRepositoryBranch(m_ui->p4clientEdit->itemText(0)); return dest; } QString PerforceImportMetadataWidget::message() const { return QString(); //TODO: return m_ui->message->toPlainText(); } void PerforceImportMetadataWidget::setSourceLocation(const VcsLocation& url) { m_ui->sourceLoc->setUrl(url.localUrl()); } void PerforceImportMetadataWidget::setSourceLocationEditable(bool enable) { m_ui->sourceLoc->setEnabled(enable); } void PerforceImportMetadataWidget::setMessage(const QString& message) { Q_UNUSED(message); //FIXME: correct ui field needs to be set //m_ui->message->setText(message); } bool PerforceImportMetadataWidget::hasValidData() const { // FIXME: It has valid data if testP4setup has completed correctly. AND client name has been set to something return !m_ui->p4clientEdit->itemText(0).isEmpty(); } void PerforceImportMetadataWidget::testP4setup() { m_ui->errorMsg->clear(); m_ui->p4clientEdit->clear(); if (!validateP4executable()) return; QDir execDir(m_ui->sourceLoc->url().toLocalFile()); QTemporaryDir tmpDir; if (!execDir.exists()) execDir.setPath(tmpDir.path()); if(!validateP4port(execDir.path())) return; if(!validateP4user(execDir.path())) return; emit changed(); } bool PerforceImportMetadataWidget::validateP4executable() { if (QStandardPaths::findExecutable(m_ui->executableLoc->url().toLocalFile()).isEmpty()) { m_ui->errorMsg->setText("Unable to find perforce executable. Is it installed on the system? Is it in your PATH?"); return false; } return true; } bool PerforceImportMetadataWidget::validateP4user(const QString& projectDir) const { QProcess exec; QProcessEnvironment p4execEnvironment; p4execEnvironment.insert(QString("P4PORT"), m_ui->p4portEdit->displayText()); exec.setWorkingDirectory(projectDir); exec.setProcessEnvironment(p4execEnvironment); exec.start(m_ui->executableLoc->url().toLocalFile(), QStringList{QStringLiteral("workspaces"), QStringLiteral("-u"), m_ui->p4userEdit->text()} ); exec.waitForFinished(); QString processStdout(exec.readAllStandardOutput()); QString processStderr(exec.readAllStandardError()); // std::cout << "Exited with code: " << exec.exitCode() << std::endl; // std::cout << "Exited with stdout" << processStdout.toStdString() << std::endl; // std::cout << "Exited with stderr" << processStderr.toStdString() << std::endl; if (exec.exitCode() != 0) { if(!processStderr.isEmpty()) { m_ui->errorMsg->setText(processStderr); } else { QString msg("P4 Client failed with exit code: "); msg += QString::number(exec.exitCode()); m_ui->errorMsg->setText(msg); } return false; } if(!processStdout.isEmpty()) { - QStringList clientCmdOutput = processStdout.split(QLatin1Char('\n'),QString::SkipEmptyParts); + const QStringList clientCmdOutput = processStdout.split(QLatin1Char('\n'),QString::SkipEmptyParts); QStringList clientItems; clientItems.reserve(clientCmdOutput.size()); for(QString const& clientLine : clientCmdOutput) { QStringList wordsInLine = clientLine.split(QLatin1Char(' ')); // Client mvo_testkdevinteg 2017/05/22 root C:\P4repo 'Created by mvo. ' -- Line would be expected to look like so clientItems.append(wordsInLine.at(1)); } m_ui->p4clientEdit->addItems(clientItems); } return true; } bool PerforceImportMetadataWidget::validateP4port(const QString& projectDir) const { QProcess exec; QProcessEnvironment p4execEnvironment; p4execEnvironment.insert(QString("P4PORT"), m_ui->p4portEdit->displayText()); QTextStream out(stdout); const auto& env = p4execEnvironment.toStringList(); for (const QString& x : env) { out << x << QLatin1Char('\n'); } out.flush(); exec.setWorkingDirectory(projectDir); exec.setProcessEnvironment(p4execEnvironment); exec.start(m_ui->executableLoc->url().toLocalFile(), QStringList() << QStringLiteral("info")); exec.waitForFinished(); //QString processStdout(exec.readAllStandardOutput()); QString processStderr(exec.readAllStandardError()); //std::cout << "Exited with code: " << exec.exitCode() << std::endl; //std::cout << "Exited with stdout" << processStdout.toStdString() << std::endl; //std::cout << "Exited with stderr" << processStderr.toStdString() << std::endl; if (exec.exitCode() != 0) { if(!processStderr.isEmpty()) { m_ui->errorMsg->setText(processStderr); } else { QString msg("P4 Client failed with error code: "); msg += QString::number(exec.exitCode()); m_ui->errorMsg->setText(msg); } return false; } return true; } diff --git a/plugins/projectmanagerview/cutcopypastehelpers.cpp b/plugins/projectmanagerview/cutcopypastehelpers.cpp index 87d8b83852..2353a9cd20 100644 --- a/plugins/projectmanagerview/cutcopypastehelpers.cpp +++ b/plugins/projectmanagerview/cutcopypastehelpers.cpp @@ -1,355 +1,355 @@ /* This file is part of KDevelop Copyright (C) 2017 Alexander Potashev This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "cutcopypastehelpers.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace CutCopyPasteHelpers { TaskInfo::TaskInfo(const TaskStatus status, const TaskType type, const Path::List& src, const Path& dest) : m_status(status), m_type(type), m_src(src), m_dest(dest) { } TaskInfo TaskInfo::createMove(const bool ok, const Path::List& src, const Path& dest) { return TaskInfo(ok ? TaskStatus::SUCCESS : TaskStatus::FAILURE, TaskType::MOVE, src, dest); } TaskInfo TaskInfo::createCopy(const bool ok, const Path::List& src, const Path& dest) { return TaskInfo(ok ? TaskStatus::SUCCESS : TaskStatus::FAILURE, TaskType::COPY, src, dest); } TaskInfo TaskInfo::createDeletion(const bool ok, const Path::List& src, const Path& dest) { return TaskInfo(ok ? TaskStatus::SUCCESS : TaskStatus::FAILURE, TaskType::DELETION, src, dest); } static QWidget* createPasteStatsWidget(QWidget *parent, const QVector& tasks) { // TODO: Create a model for the task list, and use it here instead of using QTreeWidget auto* treeWidget = new QTreeWidget(parent); QList items; items.reserve(tasks.size()); for (const TaskInfo& task : tasks) { int srcCount = task.m_src.size(); const bool withChildren = srcCount != 1; const QString destPath = task.m_dest.pathOrUrl(); QString text; if (withChildren) { // Multiple source items in the current suboperation switch (task.m_type) { case TaskType::MOVE: text = i18np("Move %1 item into %2", "Move %1 items into %2", srcCount, destPath); break; case TaskType::COPY: text = i18np("Copy %1 item into %2", "Copy %1 items into %2", srcCount, destPath); break; case TaskType::DELETION: text = i18np("Delete %1 item", "Delete %1 items", srcCount); break; } } else { // One source item in the current suboperation const QString srcPath = task.m_src[0].pathOrUrl(); switch (task.m_type) { case TaskType::MOVE: text = i18n("Move item %1 into %2", srcPath, destPath); break; case TaskType::COPY: text = i18n("Copy item %1 into %2", srcPath, destPath); break; case TaskType::DELETION: text = i18n("Delete item %1", srcPath); break; } } QString tooltip; QString iconName; switch (task.m_status) { case TaskStatus::SUCCESS: tooltip = i18n("Suboperation succeeded"); iconName = QStringLiteral("dialog-ok"); break; case TaskStatus::FAILURE: tooltip = i18n("Suboperation failed"); iconName = QStringLiteral("dialog-error"); break; case TaskStatus::SKIPPED: tooltip = i18n("Suboperation skipped to prevent data loss"); iconName = QStringLiteral("dialog-warning"); break; } auto* item = new QTreeWidgetItem; item->setText(0, text); item->setIcon(0, QIcon::fromTheme(iconName)); item->setToolTip(0, tooltip); items.append(item); if (withChildren) { for (const Path& src : task.m_src) { auto* childItem = new QTreeWidgetItem; childItem->setText(0, src.pathOrUrl()); item->addChild(childItem); } } } treeWidget->insertTopLevelItems(0, items); treeWidget->headerItem()->setHidden(true); return treeWidget; } SourceToDestinationMap mapSourceToDestination(const Path::List& sourcePaths, const Path& destinationPath) { // For example you are moving the following items into /dest/ // * /tests/ // * /tests/abc.cpp // If you pass them as is, moveFilesAndFolders() will crash (see note: // "Do not attempt to move subitems along with their parents"). // Thus we filter out subitems from "Path::List filteredPaths". // // /tests/abc.cpp will be implicitly moved to /dest/tests/abc.cpp, for // that reason we add "/dest/tests/abc.cpp" into "result.finalPaths" as well as // "/dest/tests". // // "result.finalPaths" will be used to highlight destination items after // copy/move. Path::List sortedPaths = sourcePaths; std::sort(sortedPaths.begin(), sortedPaths.end()); SourceToDestinationMap result; for (const Path& path : sortedPaths) { if (!result.filteredPaths.isEmpty() && result.filteredPaths.back().isParentOf(path)) { // think: "/tests" const Path& previousPath = result.filteredPaths.back(); // think: "/dest" + "/".relativePath("/tests/abc.cpp") = /dest/tests/abc.cpp result.finalPaths[previousPath].append(Path(destinationPath, previousPath.parent().relativePath(path))); } else { // think: "/tests" result.filteredPaths.append(path); // think: "/dest" + "tests" = "/dest/tests" result.finalPaths[path].append(Path(destinationPath, path.lastPathSegment())); } } return result; } struct ClassifiedPaths { // Items originating from projects open in this KDevelop session QHash> itemsPerProject; // Items that do not belong to known projects Path::List alienSrcPaths; }; static ClassifiedPaths classifyPaths(const Path::List& paths, KDevelop::ProjectModel* projectModel) { ClassifiedPaths result; for (const Path& path : paths) { - QList items = projectModel->itemsForPath(IndexedString(path.path())); + const QList items = projectModel->itemsForPath(IndexedString(path.path())); if (!items.empty()) { for (ProjectBaseItem* item : items) { IProject* project = item->project(); auto itemsIt = result.itemsPerProject.find(project); if (itemsIt == result.itemsPerProject.end()) { itemsIt = result.itemsPerProject.insert(project, QList()); } itemsIt->append(item); } } else { result.alienSrcPaths.append(path); } } return result; } QVector copyMoveItems(const Path::List& paths, ProjectBaseItem* destItem, const Operation operation) { KDevelop::ProjectModel* projectModel = KDevelop::ICore::self()->projectController()->projectModel(); const ClassifiedPaths cl = classifyPaths(paths, projectModel); QVector tasks; IProject* destProject = destItem->project(); IProjectFileManager* destProjectFileManager = destProject->projectFileManager(); ProjectFolderItem* destFolder = destItem->folder(); Path destPath = destFolder->path(); const auto& srcProjects = cl.itemsPerProject.keys(); for (IProject* srcProject : srcProjects) { const auto& itemsList = cl.itemsPerProject[srcProject]; Path::List pathsList; pathsList.reserve(itemsList.size()); for (KDevelop::ProjectBaseItem* item : itemsList) { pathsList.append(item->path()); } if (srcProject == destProject) { if (operation == Operation::CUT) { // Move inside project const bool ok = destProjectFileManager->moveFilesAndFolders(itemsList, destFolder); tasks.append(TaskInfo::createMove(ok, pathsList, destPath)); } else { // Copy inside project const bool ok = destProjectFileManager->copyFilesAndFolders(pathsList, destFolder); tasks.append(TaskInfo::createCopy(ok, pathsList, destPath)); } } else { // Copy/move between projects: // 1. Copy and add into destination project; // 2. Remove from source project. const bool copy_ok = destProjectFileManager->copyFilesAndFolders(pathsList, destFolder); tasks.append(TaskInfo::createCopy(copy_ok, pathsList, destPath)); if (operation == Operation::CUT) { if (copy_ok) { IProjectFileManager* srcProjectFileManager = srcProject->projectFileManager(); const bool deletion_ok = srcProjectFileManager->removeFilesAndFolders(itemsList); tasks.append(TaskInfo::createDeletion(deletion_ok, pathsList, destPath)); } else { tasks.append(TaskInfo(TaskStatus::SKIPPED, TaskType::DELETION, pathsList, destPath)); } } } } // Copy/move items from outside of all open projects if (!cl.alienSrcPaths.isEmpty()) { const bool alien_copy_ok = destProjectFileManager->copyFilesAndFolders(cl.alienSrcPaths, destFolder); tasks.append(TaskInfo::createCopy(alien_copy_ok, cl.alienSrcPaths, destPath)); if (operation == Operation::CUT) { if (alien_copy_ok) { QList urlsToDelete; urlsToDelete.reserve(cl.alienSrcPaths.size()); for (const Path& path : cl.alienSrcPaths) { urlsToDelete.append(path.toUrl()); } KIO::DeleteJob* deleteJob = KIO::del(urlsToDelete); const bool deletion_ok = deleteJob->exec(); tasks.append(TaskInfo::createDeletion(deletion_ok, cl.alienSrcPaths, destPath)); } else { tasks.append(TaskInfo(TaskStatus::SKIPPED, TaskType::DELETION, cl.alienSrcPaths, destPath)); } } } return tasks; } void showWarningDialogForFailedPaste(QWidget* parent, const QVector& tasks) { QDialog* dialog = new QDialog(parent); dialog->setWindowTitle(i18nc("@title:window", "Paste Failed")); auto *buttonBox = new QDialogButtonBox(dialog); buttonBox->setStandardButtons(QDialogButtonBox::Ok); QObject::connect(buttonBox, &QDialogButtonBox::clicked, dialog, &QDialog::accept); dialog->setWindowModality(Qt::WindowModal); dialog->setModal(true); QWidget* mainWidget = new QWidget(dialog); auto* mainLayout = new QVBoxLayout(mainWidget); const int spacingHint = mainWidget->style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing); mainLayout->setSpacing(spacingHint * 2); // provide extra spacing mainLayout->setMargin(0); auto* hLayout = new QHBoxLayout; hLayout->setMargin(0); hLayout->setSpacing(-1); // use default spacing mainLayout->addLayout(hLayout, 0); QLabel* iconLabel = new QLabel(mainWidget); // Icon QStyleOption option; option.initFrom(mainWidget); QIcon icon = QIcon::fromTheme(QStringLiteral("dialog-warning")); iconLabel->setPixmap(icon.pixmap(mainWidget->style()->pixelMetric(QStyle::PM_MessageBoxIconSize, &option, mainWidget))); auto* iconLayout = new QVBoxLayout(); iconLayout->addStretch(1); iconLayout->addWidget(iconLabel); iconLayout->addStretch(5); hLayout->addLayout(iconLayout, 0); hLayout->addSpacing(spacingHint); const QString text = i18n("Failed to paste. Below is a list of suboperations that have been attempted."); QLabel* messageLabel = new QLabel(text, mainWidget); messageLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); hLayout->addWidget(messageLabel, 5); QWidget* statsWidget = createPasteStatsWidget(dialog, tasks); auto* topLayout = new QVBoxLayout; dialog->setLayout(topLayout); topLayout->addWidget(mainWidget); topLayout->addWidget(statsWidget, 1); topLayout->addWidget(buttonBox); dialog->setMinimumSize(300, qMax(150, qMax(iconLabel->sizeHint().height(), messageLabel->sizeHint().height()))); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); } } // namespace CutCopyPasteHelpers diff --git a/plugins/projectmanagerview/projectmanagerviewplugin.cpp b/plugins/projectmanagerview/projectmanagerviewplugin.cpp index 161ff11d14..9442d6e3ac 100644 --- a/plugins/projectmanagerview/projectmanagerviewplugin.cpp +++ b/plugins/projectmanagerview/projectmanagerviewplugin.cpp @@ -1,801 +1,801 @@ /* This file is part of KDevelop Copyright 2004 Roberto Raggi Copyright 2007 Andreas Pakulat Copyright 2016, 2017 Alexander Potashev This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "projectmanagerviewplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "projectmanagerview.h" #include "debug.h" #include "cutcopypastehelpers.h" using namespace KDevelop; K_PLUGIN_FACTORY_WITH_JSON(ProjectManagerFactory, "kdevprojectmanagerview.json", registerPlugin();) namespace { QAction* createSeparatorAction() { auto* separator = new QAction(nullptr); separator->setSeparator(true); return separator; } // Returns nullptr iff the list of URLs to copy/cut was empty QMimeData* createClipboardMimeData(const bool cut) { auto* ctx = dynamic_cast( ICore::self()->selectionController()->currentSelection()); QList urls; QList mostLocalUrls; const auto& items = ctx->items(); for (const ProjectBaseItem* item : items) { if (item->folder() || item->file()) { const QUrl& url = item->path().toUrl(); urls << url; mostLocalUrls << KFileItem(url).mostLocalUrl(); } } qCDebug(PLUGIN_PROJECTMANAGERVIEW) << urls; if (urls.isEmpty()) { return nullptr; } auto* mimeData = new QMimeData; KIO::setClipboardDataCut(mimeData, cut); KUrlMimeData::setUrls(urls, mostLocalUrls, mimeData); return mimeData; } } // anonymous namespace class KDevProjectManagerViewFactory: public KDevelop::IToolViewFactory { public: explicit KDevProjectManagerViewFactory( ProjectManagerViewPlugin *plugin ): mplugin( plugin ) {} QWidget* create( QWidget *parent = nullptr ) override { return new ProjectManagerView( mplugin, parent ); } Qt::DockWidgetArea defaultPosition() const override { return Qt::LeftDockWidgetArea; } QString id() const override { return QStringLiteral("org.kdevelop.ProjectsView"); } private: ProjectManagerViewPlugin *mplugin; }; class ProjectManagerViewPluginPrivate { public: ProjectManagerViewPluginPrivate() {} KDevProjectManagerViewFactory *factory; QList ctxProjectItemList; QAction* m_buildAll; QAction* m_build; QAction* m_install; QAction* m_clean; QAction* m_configure; QAction* m_prune; }; static QList itemsFromIndexes(const QList& indexes) { QList items; ProjectModel* model = ICore::self()->projectController()->projectModel(); items.reserve(indexes.size()); for (const QModelIndex& index : indexes) { items += model->itemFromIndex(index); } return items; } ProjectManagerViewPlugin::ProjectManagerViewPlugin( QObject *parent, const QVariantList& ) : IPlugin( QStringLiteral("kdevprojectmanagerview"), parent ), d(new ProjectManagerViewPluginPrivate) { d->m_buildAll = new QAction( i18n("Build all Projects"), this ); d->m_buildAll->setIcon(QIcon::fromTheme(QStringLiteral("run-build"))); connect( d->m_buildAll, &QAction::triggered, this, &ProjectManagerViewPlugin::buildAllProjects ); actionCollection()->addAction( QStringLiteral("project_buildall"), d->m_buildAll ); d->m_build = new QAction( i18n("Build Selection"), this ); d->m_build->setIconText( i18n("Build") ); actionCollection()->setDefaultShortcut( d->m_build, Qt::Key_F8 ); d->m_build->setIcon(QIcon::fromTheme(QStringLiteral("run-build"))); d->m_build->setEnabled( false ); connect( d->m_build, &QAction::triggered, this, &ProjectManagerViewPlugin::buildProjectItems ); actionCollection()->addAction( QStringLiteral("project_build"), d->m_build ); d->m_install = new QAction( i18n("Install Selection"), this ); d->m_install->setIconText( i18n("Install") ); d->m_install->setIcon(QIcon::fromTheme(QStringLiteral("run-build-install"))); actionCollection()->setDefaultShortcut( d->m_install, Qt::SHIFT + Qt::Key_F8 ); d->m_install->setEnabled( false ); connect( d->m_install, &QAction::triggered, this, &ProjectManagerViewPlugin::installProjectItems ); actionCollection()->addAction( QStringLiteral("project_install"), d->m_install ); d->m_clean = new QAction( i18n("Clean Selection"), this ); d->m_clean->setIconText( i18n("Clean") ); d->m_clean->setIcon(QIcon::fromTheme(QStringLiteral("run-build-clean"))); d->m_clean->setEnabled( false ); connect( d->m_clean, &QAction::triggered, this, &ProjectManagerViewPlugin::cleanProjectItems ); actionCollection()->addAction( QStringLiteral("project_clean"), d->m_clean ); d->m_configure = new QAction( i18n("Configure Selection"), this ); d->m_configure->setMenuRole( QAction::NoRole ); // OSX: Be explicit about role, prevent hiding due to conflict with "Preferences..." menu item d->m_configure->setIconText( i18n("Configure") ); d->m_configure->setIcon(QIcon::fromTheme(QStringLiteral("run-build-configure"))); d->m_configure->setEnabled( false ); connect( d->m_configure, &QAction::triggered, this, &ProjectManagerViewPlugin::configureProjectItems ); actionCollection()->addAction( QStringLiteral("project_configure"), d->m_configure ); d->m_prune = new QAction( i18n("Prune Selection"), this ); d->m_prune->setIconText( i18n("Prune") ); d->m_prune->setIcon(QIcon::fromTheme(QStringLiteral("run-build-prune"))); d->m_prune->setEnabled( false ); connect( d->m_prune, &QAction::triggered, this, &ProjectManagerViewPlugin::pruneProjectItems ); actionCollection()->addAction( QStringLiteral("project_prune"), d->m_prune ); // only add the action so that its known in the actionCollection // and so that it's shortcut etc. pp. is restored // apparently that is not possible to be done in the view itself *sigh* actionCollection()->addAction( QStringLiteral("locate_document") ); setXMLFile( QStringLiteral("kdevprojectmanagerview.rc") ); d->factory = new KDevProjectManagerViewFactory( this ); core()->uiController()->addToolView( i18n("Projects"), d->factory ); connect(core()->selectionController(), &ISelectionController::selectionChanged, this, &ProjectManagerViewPlugin::updateActionState); connect(ICore::self()->projectController()->buildSetModel(), &KDevelop::ProjectBuildSetModel::rowsInserted, this, &ProjectManagerViewPlugin::updateFromBuildSetChange); connect(ICore::self()->projectController()->buildSetModel(), &KDevelop::ProjectBuildSetModel::rowsRemoved, this, &ProjectManagerViewPlugin::updateFromBuildSetChange); connect(ICore::self()->projectController()->buildSetModel(), &KDevelop::ProjectBuildSetModel::modelReset, this, &ProjectManagerViewPlugin::updateFromBuildSetChange); } void ProjectManagerViewPlugin::updateFromBuildSetChange() { updateActionState( core()->selectionController()->currentSelection() ); } void ProjectManagerViewPlugin::updateActionState( KDevelop::Context* ctx ) { bool isEmpty = ICore::self()->projectController()->buildSetModel()->items().isEmpty(); if( isEmpty ) { isEmpty = !ctx || ctx->type() != Context::ProjectItemContext || static_cast(ctx)->items().isEmpty(); } d->m_build->setEnabled( !isEmpty ); d->m_install->setEnabled( !isEmpty ); d->m_clean->setEnabled( !isEmpty ); d->m_configure->setEnabled( !isEmpty ); d->m_prune->setEnabled( !isEmpty ); } ProjectManagerViewPlugin::~ProjectManagerViewPlugin() { delete d; } void ProjectManagerViewPlugin::unload() { qCDebug(PLUGIN_PROJECTMANAGERVIEW) << "unloading manager view"; core()->uiController()->removeToolView(d->factory); } ContextMenuExtension ProjectManagerViewPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent) { if( context->type() != KDevelop::Context::ProjectItemContext ) return IPlugin::contextMenuExtension(context, parent); auto* ctx = static_cast(context); const QList items = ctx->items(); d->ctxProjectItemList.clear(); if( items.isEmpty() ) return IPlugin::contextMenuExtension(context, parent); //TODO: also needs: removeTarget, removeFileFromTarget, runTargetsFromContextMenu ContextMenuExtension menuExt; bool needsCreateFile = true; bool needsCreateFolder = true; bool needsCloseProjects = true; bool needsBuildItems = true; bool needsFolderItems = true; bool needsCutRenameRemove = true; bool needsRemoveTargetFiles = true; bool needsPaste = true; //needsCreateFile if there is one item and it's a folder or target needsCreateFile &= (items.count() == 1) && (items.first()->folder() || items.first()->target()); //needsCreateFolder if there is one item and it's a folder needsCreateFolder &= (items.count() == 1) && (items.first()->folder()); needsPaste = needsCreateFolder; d->ctxProjectItemList.reserve(items.size()); for (ProjectBaseItem* item : items) { d->ctxProjectItemList << item->index(); //needsBuildItems if items are limited to targets and buildfolders needsBuildItems &= item->target() || item->type() == ProjectBaseItem::BuildFolder; //needsCloseProjects if items are limited to top level folders (Project Folders) needsCloseProjects &= item->folder() && !item->folder()->parent(); //needsFolderItems if items are limited to folders needsFolderItems &= (bool)item->folder(); //needsRemove if items are limited to non-top-level folders or files that don't belong to targets needsCutRenameRemove &= (item->folder() && item->parent()) || (item->file() && !item->parent()->target()); //needsRemoveTargets if items are limited to file items with target parents needsRemoveTargetFiles &= (item->file() && item->parent()->target()); } if ( needsCreateFile ) { QAction* action = new QAction(i18n("Create &File..."), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::createFileFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, action ); } if ( needsCreateFolder ) { QAction* action = new QAction(i18n("Create F&older..."), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("folder-new"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::createFolderFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, action ); } if ( needsBuildItems ) { QAction* action = new QAction(i18nc("@action", "&Build"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("run-build"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::buildItemsFromContextMenu ); menuExt.addAction( ContextMenuExtension::BuildGroup, action ); action = new QAction(i18nc("@action", "&Install"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("run-build-install"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::installItemsFromContextMenu ); menuExt.addAction( ContextMenuExtension::BuildGroup, action ); action = new QAction(i18nc("@action", "&Clean"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("run-build-clean"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::cleanItemsFromContextMenu ); menuExt.addAction( ContextMenuExtension::BuildGroup, action ); action = new QAction(i18n("&Add to Build Set"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::addItemsFromContextMenuToBuildset ); menuExt.addAction( ContextMenuExtension::BuildGroup, action ); } if ( needsCloseProjects ) { QAction* close = new QAction(i18np("C&lose Project", "Close Projects", items.count()), parent); close->setIcon(QIcon::fromTheme(QStringLiteral("project-development-close"))); connect( close, &QAction::triggered, this, &ProjectManagerViewPlugin::closeProjects ); menuExt.addAction( ContextMenuExtension::ProjectGroup, close ); } if ( needsFolderItems ) { QAction* action = new QAction(i18n("&Reload"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::reloadFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, action ); } // Populating cut/copy/paste group if ( !menuExt.actions(ContextMenuExtension::FileGroup).isEmpty() ) { menuExt.addAction( ContextMenuExtension::FileGroup, createSeparatorAction() ); } if ( needsCutRenameRemove ) { QAction* cut = KStandardAction::cut(this, SLOT(cutFromContextMenu()), this); cut->setShortcutContext(Qt::WidgetShortcut); menuExt.addAction(ContextMenuExtension::FileGroup, cut); } { QAction* copy = KStandardAction::copy(this, SLOT(copyFromContextMenu()), this); copy->setShortcutContext(Qt::WidgetShortcut); menuExt.addAction( ContextMenuExtension::FileGroup, copy ); } if (needsPaste) { QAction* paste = KStandardAction::paste(this, SLOT(pasteFromContextMenu()), this); paste->setShortcutContext(Qt::WidgetShortcut); menuExt.addAction( ContextMenuExtension::FileGroup, paste ); } // Populating rename/remove group { menuExt.addAction( ContextMenuExtension::FileGroup, createSeparatorAction() ); } if ( needsCutRenameRemove ) { QAction* remove = new QAction(i18n("Remo&ve"), parent); remove->setIcon(QIcon::fromTheme(QStringLiteral("user-trash"))); connect( remove, &QAction::triggered, this, &ProjectManagerViewPlugin::removeFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, remove ); QAction* rename = new QAction(i18n("Re&name..."), parent); rename->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); connect( rename, &QAction::triggered, this, &ProjectManagerViewPlugin::renameItemFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, rename ); } if ( needsRemoveTargetFiles ) { QAction* remove = new QAction(i18n("Remove From &Target"), parent); remove->setIcon(QIcon::fromTheme(QStringLiteral("user-trash"))); connect( remove, &QAction::triggered, this, &ProjectManagerViewPlugin::removeTargetFilesFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, remove ); } if ( needsCutRenameRemove || needsRemoveTargetFiles ) { menuExt.addAction(ContextMenuExtension::FileGroup, createSeparatorAction()); } return menuExt; } void ProjectManagerViewPlugin::closeProjects() { QList projectsToClose; ProjectModel* model = ICore::self()->projectController()->projectModel(); for (const QModelIndex& index : qAsConst(d->ctxProjectItemList)) { KDevelop::ProjectBaseItem* item = model->itemFromIndex(index); if( !projectsToClose.contains( item->project() ) ) { projectsToClose << item->project(); } } d->ctxProjectItemList.clear(); for (KDevelop::IProject* proj : qAsConst(projectsToClose)) { core()->projectController()->closeProject( proj ); } } void ProjectManagerViewPlugin::installItemsFromContextMenu() { runBuilderJob( BuilderJob::Install, itemsFromIndexes(d->ctxProjectItemList) ); d->ctxProjectItemList.clear(); } void ProjectManagerViewPlugin::cleanItemsFromContextMenu() { runBuilderJob( BuilderJob::Clean, itemsFromIndexes( d->ctxProjectItemList ) ); d->ctxProjectItemList.clear(); } void ProjectManagerViewPlugin::buildItemsFromContextMenu() { runBuilderJob( BuilderJob::Build, itemsFromIndexes( d->ctxProjectItemList ) ); d->ctxProjectItemList.clear(); } QList ProjectManagerViewPlugin::collectAllProjects() { QList items; const auto projects = core()->projectController()->projects(); items.reserve(projects.size()); for (auto* project : projects) { items << project->projectItem(); } return items; } void ProjectManagerViewPlugin::buildAllProjects() { runBuilderJob( BuilderJob::Build, collectAllProjects() ); } QList ProjectManagerViewPlugin::collectItems() { QList items; const QList buildItems = ICore::self()->projectController()->buildSetModel()->items(); if( !buildItems.isEmpty() ) { for (const BuildItem& buildItem : buildItems) { if( ProjectBaseItem* item = buildItem.findItem() ) { items << item; } } } else { auto* ctx = static_cast(ICore::self()->selectionController()->currentSelection()); items = ctx->items(); } return items; } void ProjectManagerViewPlugin::runBuilderJob( BuilderJob::BuildType type, const QList& items ) { auto* builder = new BuilderJob; builder->addItems( type, items ); builder->updateJobName(); ICore::self()->uiController()->registerStatus(new JobStatus(builder)); ICore::self()->runController()->registerJob( builder ); } void ProjectManagerViewPlugin::installProjectItems() { runBuilderJob( KDevelop::BuilderJob::Install, collectItems() ); } void ProjectManagerViewPlugin::pruneProjectItems() { runBuilderJob( KDevelop::BuilderJob::Prune, collectItems() ); } void ProjectManagerViewPlugin::configureProjectItems() { runBuilderJob( KDevelop::BuilderJob::Configure, collectItems() ); } void ProjectManagerViewPlugin::cleanProjectItems() { runBuilderJob( KDevelop::BuilderJob::Clean, collectItems() ); } void ProjectManagerViewPlugin::buildProjectItems() { runBuilderJob( KDevelop::BuilderJob::Build, collectItems() ); } void ProjectManagerViewPlugin::addItemsFromContextMenuToBuildset( ) { const auto items = itemsFromIndexes(d->ctxProjectItemList); for (KDevelop::ProjectBaseItem* item : items) { ICore::self()->projectController()->buildSetModel()->addProjectItem( item ); } } void ProjectManagerViewPlugin::runTargetsFromContextMenu( ) { const auto items = itemsFromIndexes(d->ctxProjectItemList); for (KDevelop::ProjectBaseItem* item : items) { KDevelop::ProjectExecutableTargetItem* t=item->executable(); if(t) { qCDebug(PLUGIN_PROJECTMANAGERVIEW) << "Running target: " << t->text() << t->builtUrl(); } } } void ProjectManagerViewPlugin::projectConfiguration( ) { if( !d->ctxProjectItemList.isEmpty() ) { ProjectModel* model = ICore::self()->projectController()->projectModel(); core()->projectController()->configureProject( model->itemFromIndex(d->ctxProjectItemList.at( 0 ))->project() ); } } void ProjectManagerViewPlugin::reloadFromContextMenu( ) { QList< KDevelop::ProjectFolderItem* > folders; const auto items = itemsFromIndexes(d->ctxProjectItemList); for (KDevelop::ProjectBaseItem* item : items) { if ( item->folder() ) { // since reloading should be recursive, only pass the upper-most items bool found = false; const auto currentFolders = folders; for (KDevelop::ProjectFolderItem* existing : currentFolders) { if ( existing->path().isParentOf(item->folder()->path()) ) { // simply skip this child found = true; break; } else if ( item->folder()->path().isParentOf(existing->path()) ) { // remove the child in the list and add the current item instead folders.removeOne(existing); // continue since there could be more than one existing child } } if ( !found ) { folders << item->folder(); } } } for (KDevelop::ProjectFolderItem* folder : qAsConst(folders)) { folder->project()->projectFileManager()->reload(folder); } } void ProjectManagerViewPlugin::createFolderFromContextMenu( ) { const auto items = itemsFromIndexes(d->ctxProjectItemList); for (KDevelop::ProjectBaseItem* item : items) { if ( item->folder() ) { QWidget* window(ICore::self()->uiController()->activeMainWindow()->window()); QString name = QInputDialog::getText ( window, i18n ( "Create Folder in %1", item->folder()->path().pathOrUrl() ), i18n ( "Folder name:" ) ); if (!name.isEmpty()) { item->project()->projectFileManager()->addFolder( Path(item->path(), name), item->folder() ); } } } } void ProjectManagerViewPlugin::removeFromContextMenu() { removeItems(itemsFromIndexes( d->ctxProjectItemList )); } void ProjectManagerViewPlugin::removeItems(const QList< ProjectBaseItem* >& items) { if (items.isEmpty()) { return; } //copy the list of selected items and sort it to guarantee parents will come before children QList sortedItems = items; std::sort(sortedItems.begin(), sortedItems.end(), ProjectBaseItem::pathLessThan); Path lastFolder; QHash< IProjectFileManager*, QList > filteredItems; QStringList itemPaths; for (KDevelop::ProjectBaseItem* item : qAsConst(sortedItems)) { if (item->isProjectRoot()) { continue; } else if (item->folder() || item->file()) { //make sure no children of folders that will be deleted are listed if (lastFolder.isParentOf(item->path())) { continue; } else if (item->folder()) { lastFolder = item->path(); } IProjectFileManager* manager = item->project()->projectFileManager(); if (manager) { filteredItems[manager] << item; itemPaths << item->path().pathOrUrl(); } } } if (filteredItems.isEmpty()) { return; } if (KMessageBox::warningYesNoList( QApplication::activeWindow(), i18np("Do you really want to delete this item?", "Do you really want to delete these %1 items?", itemPaths.size()), itemPaths, i18n("Delete Files"), KStandardGuiItem::del(), KStandardGuiItem::cancel() ) == KMessageBox::No) { return; } //Go though projectmanagers, have them remove the files and folders that they own QHash< IProjectFileManager*, QList >::iterator it; for (it = filteredItems.begin(); it != filteredItems.end(); ++it) { Q_ASSERT(it.key()); it.key()->removeFilesAndFolders(it.value()); } } void ProjectManagerViewPlugin::removeTargetFilesFromContextMenu() { const QList items = itemsFromIndexes( d->ctxProjectItemList ); QHash< IBuildSystemManager*, QList > itemsByBuildSystem; for (ProjectBaseItem* item : items) { itemsByBuildSystem[item->project()->buildSystemManager()].append(item->file()); } QHash< IBuildSystemManager*, QList >::iterator it; for (it = itemsByBuildSystem.begin(); it != itemsByBuildSystem.end(); ++it) it.key()->removeFilesFromTargets(it.value()); } void ProjectManagerViewPlugin::renameItemFromContextMenu() { renameItems(itemsFromIndexes( d->ctxProjectItemList )); } void ProjectManagerViewPlugin::renameItems(const QList< ProjectBaseItem* >& items) { if (items.isEmpty()) { return; } QWidget* window = ICore::self()->uiController()->activeMainWindow()->window(); for (KDevelop::ProjectBaseItem* item : items) { if ((item->type()!=ProjectBaseItem::BuildFolder && item->type()!=ProjectBaseItem::Folder && item->type()!=ProjectBaseItem::File) || !item->parent()) { continue; } const QString src = item->text(); //Change QInputDialog->KFileSaveDialog? QString name = QInputDialog::getText( window, i18n("Rename..."), i18n("New name for '%1':", item->text()), QLineEdit::Normal, item->text() ); if (!name.isEmpty() && name != src) { ProjectBaseItem::RenameStatus status = item->rename( name ); QString errorMessageText; switch(status) { case ProjectBaseItem::RenameOk: break; case ProjectBaseItem::ExistingItemSameName: errorMessageText = i18n("There is already a file named '%1'", name); break; case ProjectBaseItem::ProjectManagerRenameFailed: errorMessageText = i18n("Could not rename '%1'", name); break; case ProjectBaseItem::InvalidNewName: errorMessageText = i18n("'%1' is not a valid file name", name); break; } if (!errorMessageText.isEmpty()) { auto* message = new Sublime::Message(errorMessageText, Sublime::Message::Error); ICore::self()->uiController()->postMessage(message); } } } } ProjectFileItem* createFile(const ProjectFolderItem* item) { QWidget* window = ICore::self()->uiController()->activeMainWindow()->window(); QString name = QInputDialog::getText(window, i18n("Create File in %1", item->path().pathOrUrl()), i18n("File name:")); if(name.isEmpty()) return nullptr; ProjectFileItem* ret = item->project()->projectFileManager()->addFile( Path(item->path(), name), item->folder() ); if (ret) { ICore::self()->documentController()->openDocument( ret->path().toUrl() ); } return ret; } void ProjectManagerViewPlugin::createFileFromContextMenu( ) { const auto items = itemsFromIndexes(d->ctxProjectItemList); for (KDevelop::ProjectBaseItem* item : items) { if ( item->folder() ) { createFile(item->folder()); } else if ( item->target() ) { auto* folder=dynamic_cast(item->parent()); if(folder) { ProjectFileItem* f=createFile(folder); if(f) item->project()->buildSystemManager()->addFilesToTarget(QList() << f, item->target()); } } } } void ProjectManagerViewPlugin::copyFromContextMenu() { qApp->clipboard()->setMimeData(createClipboardMimeData(false)); } void ProjectManagerViewPlugin::cutFromContextMenu() { qApp->clipboard()->setMimeData(createClipboardMimeData(true)); } static void selectItemsByPaths(ProjectManagerView* view, const Path::List& paths) { KDevelop::ProjectModel* projectModel = KDevelop::ICore::self()->projectController()->projectModel(); QList newItems; for (const Path& path : paths) { QList items = projectModel->itemsForPath(IndexedString(path.path())); newItems.append(items); for (ProjectBaseItem* item : qAsConst(items)) { view->expandItem(item->parent()); } } view->selectItems(newItems); } void ProjectManagerViewPlugin::pasteFromContextMenu() { auto* ctx = static_cast(ICore::self()->selectionController()->currentSelection()); if (ctx->items().count() != 1) { return; //do nothing if multiple or none items are selected } ProjectBaseItem* destItem = ctx->items().at(0); if (!destItem->folder()) { return; //do nothing if the target is not a directory } const QMimeData* data = qApp->clipboard()->mimeData(); qCDebug(PLUGIN_PROJECTMANAGERVIEW) << data->urls(); Path::List origPaths = toPathList(data->urls()); const bool isCut = KIO::isClipboardDataCut(data); const CutCopyPasteHelpers::SourceToDestinationMap map = CutCopyPasteHelpers::mapSourceToDestination(origPaths, destItem->folder()->path()); - QVector tasks = CutCopyPasteHelpers::copyMoveItems( + const QVector tasks = CutCopyPasteHelpers::copyMoveItems( map.filteredPaths, destItem, isCut ? CutCopyPasteHelpers::Operation::CUT : CutCopyPasteHelpers::Operation::COPY); // Select new items in the project manager view auto* itemCtx = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (itemCtx) { Path::List finalPathsList; for (const auto& task : tasks) { if (task.m_status == CutCopyPasteHelpers::TaskStatus::SUCCESS && task.m_type != CutCopyPasteHelpers::TaskType::DELETION) { finalPathsList.reserve(finalPathsList.size() + task.m_src.size()); for (const Path& src : task.m_src) { finalPathsList.append(map.finalPaths[src]); } } } selectItemsByPaths(itemCtx->view(), finalPathsList); } // If there was a single failure, display a warning dialog. const bool anyFailed = std::any_of(tasks.begin(), tasks.end(), [](const CutCopyPasteHelpers::TaskInfo& task) { return task.m_status != CutCopyPasteHelpers::TaskStatus::SUCCESS; }); if (anyFailed) { QWidget* window = ICore::self()->uiController()->activeMainWindow()->window(); showWarningDialogForFailedPaste(window, tasks); } } #include "projectmanagerviewplugin.moc" diff --git a/plugins/projectmanagerview/projecttreeview.cpp b/plugins/projectmanagerview/projecttreeview.cpp index a5ea7115ac..bbf1e40e77 100644 --- a/plugins/projectmanagerview/projecttreeview.cpp +++ b/plugins/projectmanagerview/projecttreeview.cpp @@ -1,480 +1,480 @@ /* This file is part of KDevelop Copyright 2005 Roberto Raggi Copyright 2007 Andreas Pakulat Copyright 2009 Aleix Pol This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "projecttreeview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "projectmanagerviewplugin.h" #include "projectmodelsaver.h" #include "projectmodelitemdelegate.h" #include "debug.h" #include #include using namespace KDevelop; namespace { QString settingsConfigGroup() { return QStringLiteral("ProjectTreeView"); } QList fileItemsWithin(const QList& items) { QList fileItems; fileItems.reserve(items.size()); for (ProjectBaseItem* item : items) { if (ProjectFileItem *file = item->file()) fileItems.append(file); else if (item->folder()) fileItems.append(fileItemsWithin(item->children())); } return fileItems; } QList topLevelItemsWithin(QList items) { std::sort(items.begin(), items.end(), ProjectBaseItem::pathLessThan); Path lastFolder; for (int i = items.size() - 1; i >= 0; --i) { if (lastFolder.isParentOf(items[i]->path())) items.removeAt(i); else if (items[i]->folder()) lastFolder = items[i]->path(); } return items; } template void filterDroppedItems(QList &items, ProjectBaseItem* dest) { for (int i = items.size() - 1; i >= 0; --i) { //No drag and drop from and to same location if (items[i]->parent() == dest) items.removeAt(i); //No moving between projects (technically feasible if the projectmanager is the same though...) else if (items[i]->project() != dest->project()) items.removeAt(i); } } //TODO test whether this could be replaced by projectbuildsetwidget.cpp::showContextMenu_appendActions void popupContextMenu_appendActions(QMenu& menu, const QList& actions) { menu.addActions(actions); menu.addSeparator(); } } ProjectTreeView::ProjectTreeView( QWidget *parent ) : QTreeView( parent ), m_previousSelection ( nullptr ) { header()->hide(); setEditTriggers( QAbstractItemView::EditKeyPressed ); setContextMenuPolicy( Qt::CustomContextMenu ); setSelectionMode( QAbstractItemView::ExtendedSelection ); setIndentation(10); setDragEnabled(true); setDragDropMode(QAbstractItemView::InternalMove); setAutoScroll(true); setAutoExpandDelay(300); setItemDelegate(new ProjectModelItemDelegate(this)); connect( this, &ProjectTreeView::customContextMenuRequested, this, &ProjectTreeView::popupContextMenu ); connect( this, &ProjectTreeView::activated, this, &ProjectTreeView::slotActivated ); connect( ICore::self(), &ICore::aboutToShutdown, this, &ProjectTreeView::aboutToShutdown); connect( ICore::self()->projectController(), &IProjectController::projectOpened, this, &ProjectTreeView::restoreState ); connect( ICore::self()->projectController(), &IProjectController::projectClosed, this, &ProjectTreeView::projectClosed ); } ProjectTreeView::~ProjectTreeView() { } ProjectBaseItem* ProjectTreeView::itemAtPos(const QPoint& pos) const { return indexAt(pos).data(ProjectModel::ProjectItemRole).value(); } void ProjectTreeView::dropEvent(QDropEvent* event) { auto* selectionCtxt = static_cast(KDevelop::ICore::self()->selectionController()->currentSelection()); ProjectBaseItem* destItem = itemAtPos(event->pos()); if (destItem && (dropIndicatorPosition() == AboveItem || dropIndicatorPosition() == BelowItem)) destItem = destItem->parent(); if (selectionCtxt && destItem) { if (ProjectFolderItem *folder = destItem->folder()) { QMenu dropMenu(this); QString seq = QKeySequence( Qt::ShiftModifier ).toString(); seq.chop(1); // chop superfluous '+' QAction* move = new QAction(i18n("&Move Here") + QLatin1Char('\t') + seq, &dropMenu); move->setIcon(QIcon::fromTheme(QStringLiteral("go-jump"))); dropMenu.addAction(move); seq = QKeySequence( Qt::ControlModifier ).toString(); seq.chop(1); QAction* copy = new QAction(i18n("&Copy Here") + QLatin1Char('\t') + seq, &dropMenu); copy->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); dropMenu.addAction(copy); dropMenu.addSeparator(); QAction* cancel = new QAction(i18n("C&ancel") + QLatin1Char('\t') + QKeySequence(Qt::Key_Escape).toString(), &dropMenu); cancel->setIcon(QIcon::fromTheme(QStringLiteral("process-stop"))); dropMenu.addAction(cancel); QAction *executedAction = nullptr; Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers(); if (modifiers == Qt::ControlModifier) { executedAction = copy; } else if (modifiers == Qt::ShiftModifier) { executedAction = move; } else { executedAction = dropMenu.exec(this->mapToGlobal(event->pos())); } QList usefulItems = topLevelItemsWithin(selectionCtxt->items()); filterDroppedItems(usefulItems, destItem); Path::List paths; paths.reserve(usefulItems.size()); for (ProjectBaseItem* i : qAsConst(usefulItems)) { paths << i->path(); } bool success = false; if (executedAction == copy) { success = destItem->project()->projectFileManager()->copyFilesAndFolders(paths, folder); } else if (executedAction == move) { success = destItem->project()->projectFileManager()->moveFilesAndFolders(usefulItems, folder); } if (success) { //expand target folder expand( mapFromItem(folder)); //and select new items QItemSelection selection; for (const Path& path : qAsConst(paths)) { const Path targetPath(folder->path(), path.lastPathSegment()); const auto folderChildren = folder->children(); for (ProjectBaseItem* item : folderChildren) { if (item->path() == targetPath) { QModelIndex indx = mapFromItem( item ); selection.append(QItemSelectionRange(indx, indx)); setCurrentIndex(indx); } } } selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect); } } else if (destItem->target() && destItem->project()->buildSystemManager()) { QMenu dropMenu(this); QString seq = QKeySequence( Qt::ControlModifier ).toString(); seq.chop(1); QAction* addToTarget = new QAction(i18n("&Add to Target") + QLatin1Char('\t') + seq, &dropMenu); addToTarget->setIcon(QIcon::fromTheme(QStringLiteral("edit-link"))); dropMenu.addAction(addToTarget); dropMenu.addSeparator(); QAction* cancel = new QAction(i18n("C&ancel") + QLatin1Char('\t') + QKeySequence(Qt::Key_Escape).toString(), &dropMenu); cancel->setIcon(QIcon::fromTheme(QStringLiteral("process-stop"))); dropMenu.addAction(cancel); QAction *executedAction = nullptr; Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers(); if (modifiers == Qt::ControlModifier) { executedAction = addToTarget; } else { executedAction = dropMenu.exec(this->mapToGlobal(event->pos())); } if (executedAction == addToTarget) { QList usefulItems = fileItemsWithin(selectionCtxt->items()); filterDroppedItems(usefulItems, destItem); destItem->project()->buildSystemManager()->addFilesToTarget(usefulItems, destItem->target()); } } } event->accept(); } QModelIndex ProjectTreeView::mapFromSource(const QAbstractProxyModel* proxy, const QModelIndex& sourceIdx) { const QAbstractItemModel* next = proxy->sourceModel(); Q_ASSERT(next == sourceIdx.model() || qobject_cast(next)); if(next == sourceIdx.model()) return proxy->mapFromSource(sourceIdx); else { const auto* nextProxy = qobject_cast(next); QModelIndex idx = mapFromSource(nextProxy, sourceIdx); Q_ASSERT(idx.model() == nextProxy); return proxy->mapFromSource(idx); } } QModelIndex ProjectTreeView::mapFromItem(const ProjectBaseItem* item) { QModelIndex ret = mapFromSource(qobject_cast(model()), item->index()); Q_ASSERT(ret.model() == model()); return ret; } void ProjectTreeView::slotActivated( const QModelIndex &index ) { if ( QApplication::keyboardModifiers() & Qt::CTRL || QApplication::keyboardModifiers() & Qt::SHIFT ) { // Do not open file when Ctrl or Shift is pressed; that's for selection return; } auto *item = index.data(ProjectModel::ProjectItemRole).value(); if ( item && item->file() ) { emit activate( item->file()->path() ); } } void ProjectTreeView::projectClosed(KDevelop::IProject* project) { if ( project == m_previousSelection ) m_previousSelection = nullptr; } QList ProjectTreeView::selectedProjects() { QList itemlist; if ( selectionModel()->hasSelection() ) { - QModelIndexList indexes = selectionModel()->selectedRows(); + const QModelIndexList indexes = selectionModel()->selectedRows(); for ( const QModelIndex& index: indexes ) { auto* item = index.data( ProjectModel::ProjectItemRole ).value(); if ( item ) { itemlist << item; m_previousSelection = item->project(); } } } // add previous selection if nothing is selected right now if ( itemlist.isEmpty() && m_previousSelection ) { itemlist << m_previousSelection->projectItem(); } return itemlist; } KDevelop::IProject* ProjectTreeView::getCurrentProject() { auto itemList = selectedProjects(); if ( !itemList.isEmpty() ) { return itemList.at( 0 )->project(); } return nullptr; } void ProjectTreeView::popupContextMenu( const QPoint &pos ) { QList itemlist; if ( indexAt( pos ).isValid() ) { itemlist = selectedProjects(); } QMenu menu( this ); KDevelop::ProjectItemContextImpl context(itemlist); const QList extensions = ICore::self()->pluginController()->queryPluginsForContextMenuExtensions(&context, &menu); QList buildActions; QList vcsActions; QList analyzeActions; QList extActions; QList projectActions; QList fileActions; QList runActions; for (const ContextMenuExtension& ext : extensions) { buildActions += ext.actions(ContextMenuExtension::BuildGroup); fileActions += ext.actions(ContextMenuExtension::FileGroup); projectActions += ext.actions(ContextMenuExtension::ProjectGroup); vcsActions += ext.actions(ContextMenuExtension::VcsGroup); analyzeActions += ext.actions(ContextMenuExtension::AnalyzeProjectGroup); extActions += ext.actions(ContextMenuExtension::ExtensionGroup); runActions += ext.actions(ContextMenuExtension::RunGroup); } if ( analyzeActions.count() ) { QMenu* analyzeMenu = new QMenu(i18n("Analyze With"), &menu); analyzeMenu->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok"))); for (QAction* act : qAsConst(analyzeActions)) { analyzeMenu->addAction( act ); } analyzeActions = {analyzeMenu->menuAction()}; } popupContextMenu_appendActions(menu, buildActions); popupContextMenu_appendActions(menu, runActions ); popupContextMenu_appendActions(menu, fileActions); popupContextMenu_appendActions(menu, vcsActions); popupContextMenu_appendActions(menu, analyzeActions); popupContextMenu_appendActions(menu, extActions); if (itemlist.size() == 1 && itemlist.first()->folder() && !itemlist.first()->folder()->parent()) { QAction* projectConfig = new QAction(i18n("Open Configuration..."), &menu); projectConfig->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); connect( projectConfig, &QAction::triggered, this, &ProjectTreeView::openProjectConfig ); projectActions << projectConfig; } popupContextMenu_appendActions(menu, projectActions); if ( !menu.isEmpty() ) { menu.exec(viewport()->mapToGlobal(pos)); } } void ProjectTreeView::openProjectConfig() { if ( IProject* project = getCurrentProject() ) { IProjectController* ip = ICore::self()->projectController(); ip->configureProject( project ); } } void ProjectTreeView::saveState( IProject* project ) { // nullptr won't create a usable saved state, so spare the effort if ( !project ) { return; } KConfigGroup configGroup( ICore::self()->activeSession()->config(), settingsConfigGroup() + project->name() ); ProjectModelSaver saver; saver.setProject( project ); saver.setView( this ); saver.saveState( configGroup ); } void ProjectTreeView::restoreState( IProject* project ) { if ( !project ) { return; } KConfigGroup configGroup( ICore::self()->activeSession()->config(), settingsConfigGroup() + project->name() ); ProjectModelSaver saver; saver.setProject( project ); saver.setView( this ); saver.restoreState( configGroup ); } void ProjectTreeView::rowsInserted( const QModelIndex& parent, int start, int end ) { QTreeView::rowsInserted( parent, start, end ); if ( !parent.model() ) { const auto& projects = selectedProjects(); for (const auto& project: projects) { restoreState( project->project() ); } } } void ProjectTreeView::rowsAboutToBeRemoved( const QModelIndex& parent, int start, int end ) { if ( !parent.model() ) { const auto& projects = selectedProjects(); for (const auto& project : projects) { saveState( project->project() ); } } QTreeView::rowsAboutToBeRemoved( parent, start, end ); } void ProjectTreeView::aboutToShutdown() { // save all projects, not just the selected ones const auto projects = ICore::self()->projectController()->projects(); for ( const auto& project: projects ) { saveState( project ); } } void ProjectTreeView::keyPressEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Return && currentIndex().isValid() && state()!=QAbstractItemView::EditingState) { event->accept(); slotActivated(currentIndex()); } else QTreeView::keyPressEvent(event); } void ProjectTreeView::drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const { if (WidgetColorizer::colorizeByProject()) { const auto projectPath = index.data(ProjectModel::ProjectRole).value()->path(); const QColor color = WidgetColorizer::colorForId(qHash(projectPath), palette(), true); WidgetColorizer::drawBranches(this, painter, rect, index, color); } QTreeView::drawBranches(painter, rect, index); } diff --git a/plugins/qmljs/codecompletion/items/functioncalltipcompletionitem.cpp b/plugins/qmljs/codecompletion/items/functioncalltipcompletionitem.cpp index d4adb6cc9e..be6aecf45a 100644 --- a/plugins/qmljs/codecompletion/items/functioncalltipcompletionitem.cpp +++ b/plugins/qmljs/codecompletion/items/functioncalltipcompletionitem.cpp @@ -1,182 +1,182 @@ /* * This file is part of qmljs, the QML/JS language support plugin for KDevelop * Copyright (c) 2014 Denis Steckelmacher * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "functioncalltipcompletionitem.h" #include "../../duchain/helper.h" #include "../../duchain/functiontype.h" #include #include #include #include using namespace KDevelop; using namespace QmlJS; FunctionCalltipCompletionItem::FunctionCalltipCompletionItem(const DeclarationPointer& decl, int depth, int argumentIndex) : m_declaration(decl), m_depth(depth) { // Ensure that decl has a function type if (!decl) { return; } QmlJS::FunctionType::Ptr func = QmlJS::FunctionType::Ptr::dynamicCast(decl->abstractType()); if (!func) { return; } // Arguments can be fetch from the function declaration (if available), or // from its function type Declaration* funcDecl = func->declaration(decl->topContext()); DUContext* argsContext = (funcDecl ? funcDecl->internalContext() : nullptr); QStringList arguments; if (argsContext) { - auto args = argsContext->allDeclarations(CursorInRevision::invalid(), decl->topContext(), false); + const auto args = argsContext->allDeclarations(CursorInRevision::invalid(), decl->topContext(), false); arguments.reserve(args.size()); for (auto pair : args) { arguments.append(pair.first->toString()); } if (argumentIndex < args.count()) { m_currentArgumentType = args.at(argumentIndex).first->abstractType(); } } else { const auto args = func->arguments(); arguments.reserve(args.size()); for (auto type : args) { arguments.append(type->toString()); } if (argumentIndex < func->arguments().count()) { m_currentArgumentType = func->arguments().at(argumentIndex); } } // [type] functionName if (func->returnType()) { m_prefix = func->returnType()->toString() + QLatin1Char(' '); } m_prefix += decl->identifier().toString(); // (arg1, arg2, [currentArgument in m_currentArgument], arg4, arg5) m_arguments = QLatin1Char('('); for (int i=0; i 0) { m_arguments += QLatin1String(", "); } if (i == argumentIndex) { m_currentArgumentStart = m_arguments.length(); m_currentArgumentLength = arguments.at(i).length(); } m_arguments += arguments.at(i); } m_arguments += QLatin1Char(')'); } AbstractType::Ptr FunctionCalltipCompletionItem::currentArgumentType() const { return m_currentArgumentType; } QVariant FunctionCalltipCompletionItem::data(const QModelIndex& index, int role, const CodeCompletionModel* model) const { Q_UNUSED(model) switch (role) { case Qt::DisplayRole: switch (index.column()) { case CodeCompletionModel::Prefix: return m_prefix; case CodeCompletionModel::Arguments: return m_arguments; } break; case CodeCompletionModel::ArgumentHintDepth: return argumentHintDepth(); case CodeCompletionModel::CompletionRole: return (int)completionProperties(); case CodeCompletionModel::HighlightingMethod: if (index.column() == CodeCompletionModel::Arguments) { return (int)CodeCompletionModel::CustomHighlighting; } break; case CodeCompletionModel::CustomHighlight: if (index.column() == CodeCompletionModel::Arguments) { QTextFormat format; format.setBackground(QBrush(QColor::fromRgb(142, 186, 255))); // Same color as kdev-python format.setProperty(QTextFormat::FontWeight, 99); return QVariantList{ m_currentArgumentStart, m_currentArgumentLength, format, }; } break; case Qt::DecorationRole: if (index.column() == CodeCompletionModel::Prefix) { return DUChainUtils::iconForProperties(completionProperties()); } break; } return QVariant(); } DeclarationPointer FunctionCalltipCompletionItem::declaration() const { return m_declaration; } int FunctionCalltipCompletionItem::argumentHintDepth() const { return m_depth; } int FunctionCalltipCompletionItem::inheritanceDepth() const { return 0; } CodeCompletionModel::CompletionProperties FunctionCalltipCompletionItem::completionProperties() const { return CodeCompletionModel::Function; } diff --git a/plugins/qmljs/duchain/declarationbuilder.cpp b/plugins/qmljs/duchain/declarationbuilder.cpp index dc3638880e..eb7e900a19 100644 --- a/plugins/qmljs/duchain/declarationbuilder.cpp +++ b/plugins/qmljs/duchain/declarationbuilder.cpp @@ -1,1542 +1,1542 @@ /************************************************************************************* * Copyright (C) 2012 by Aleix Pol * * Copyright (C) 2012 by Milian Wolff * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 2 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * *************************************************************************************/ #include "declarationbuilder.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include "expressionvisitor.h" #include "parsesession.h" #include "functiondeclaration.h" #include "functiontype.h" #include "helper.h" #include "cache.h" #include "frameworks/nodejs.h" #include #include #include using namespace KDevelop; DeclarationBuilder::DeclarationBuilder(ParseSession* session) : m_prebuilding(false) { m_session = session; } ReferencedTopDUContext DeclarationBuilder::build(const IndexedString& url, QmlJS::AST::Node* node, const ReferencedTopDUContext& updateContext_) { Q_ASSERT(m_session->url() == url); ReferencedTopDUContext updateContext(updateContext_); // The declaration builder needs to run twice, so it can resolve uses of e.g. functions // which are called before they are defined (which is easily possible, due to JS's dynamic nature). if (!m_prebuilding) { qCDebug(KDEV_QMLJS_DUCHAIN) << "building, but running pre-builder first"; auto prebuilder = new DeclarationBuilder(m_session); prebuilder->m_prebuilding = true; updateContext = prebuilder->build(url, node, updateContext); qCDebug(KDEV_QMLJS_DUCHAIN) << "pre-builder finished"; delete prebuilder; if (!m_session->allDependenciesSatisfied()) { qCDebug(KDEV_QMLJS_DUCHAIN) << "dependencies were missing, don't perform the second parsing pass"; return updateContext; } } else { qCDebug(KDEV_QMLJS_DUCHAIN) << "prebuilding"; } return DeclarationBuilderBase::build(url, node, updateContext); } void DeclarationBuilder::startVisiting(QmlJS::AST::Node* node) { DUContext* builtinQmlContext = nullptr; if (QmlJS::isQmlFile(currentContext()) && !currentContext()->url().str().contains(QLatin1String("__builtin_qml.qml"))) { builtinQmlContext = m_session->contextOfFile( QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kdevqmljssupport/nodejsmodules/__builtin_qml.qml")) ); } { DUChainWriteLocker lock; // Remove all the imported parent contexts: imports may have been edited // and there musn't be any leftover parent context currentContext()->topContext()->clearImportedParentContexts(); // Initialize Node.js QmlJS::NodeJS::instance().initialize(this); // Built-in QML types (color, rect, etc) if (builtinQmlContext) { topContext()->addImportedParentContext(builtinQmlContext); } } DeclarationBuilderBase::startVisiting(node); } /* * Functions */ template void DeclarationBuilder::declareFunction(QmlJS::AST::Node* node, bool newPrototypeContext, const Identifier& name, const RangeInRevision& nameRange, QmlJS::AST::Node* parameters, const RangeInRevision& parametersRange, QmlJS::AST::Node* body, const RangeInRevision& bodyRange) { setComment(node); // Declare the function QmlJS::FunctionType::Ptr func(new QmlJS::FunctionType); Decl* decl; { DUChainWriteLocker lock; decl = openDeclaration(name, nameRange); decl->setKind(Declaration::Type); func->setDeclaration(decl); decl->setType(func); } openType(func); // Parameters, if any (a function must always have an internal function context, // so always open a context here even if there are no parameters) DUContext* parametersContext = openContext( node + 1, // Don't call setContextOnNode on node, only the body context can be associated with node RangeInRevision(parametersRange.start, bodyRange.end), // Ensure that this context contains both the parameters and the body DUContext::Function, QualifiedIdentifier(name) ); if (parameters) { QmlJS::AST::Node::accept(parameters, this); } // The internal context of the function is its parameter context { DUChainWriteLocker lock; decl->setInternalContext(parametersContext); } // Open the prototype context, if any. This has to be done before the body // because this context is needed for "this" to be properly resolved // in it. if (newPrototypeContext) { DUChainWriteLocker lock; auto* d = reinterpret_cast(decl); d->setPrototypeContext(openContext( node + 2, // Don't call setContextOnNode on node, only the body context can be associated with node RangeInRevision(parametersRange.start, parametersRange.start), DUContext::Function, // This allows QmlJS::getOwnerOfContext to know that the parent of this context is the function declaration QualifiedIdentifier(name) )); if (name != Identifier(QStringLiteral("Object"))) { // Every class inherit from Object QmlJS::importObjectContext(currentContext(), topContext()); } closeContext(); } // Body, if any (it is a child context of the parameters) openContext( node, bodyRange, DUContext::Other, QualifiedIdentifier(name) ); if (body) { QmlJS::AST::Node::accept(body, this); } // Close the body context and then the parameters context closeContext(); closeContext(); } template void DeclarationBuilder::declareParameters(Node* node, QmlJS::AST::UiQualifiedId* Node::*typeFunc) { for (Node *plist = node; plist; plist = plist->next) { const Identifier name(plist->name.toString()); const RangeInRevision range = m_session->locationToRange(plist->identifierToken); AbstractType::Ptr type = (typeFunc ? typeFromName((plist->*typeFunc)->name.toString()) : // The typeAttribute attribute of plist contains the type name of the argument AbstractType::Ptr(new IntegralType(IntegralType::TypeMixed)) // No type information, use mixed ); { DUChainWriteLocker lock; openDeclaration(name, range); } openType(type); closeAndAssignType(); if (QmlJS::FunctionType::Ptr funType = currentType()) { funType->addArgument(type); } } } bool DeclarationBuilder::visit(QmlJS::AST::FunctionDeclaration* node) { declareFunction( node, true, // A function declaration always has its own prototype context Identifier(node->name.toString()), m_session->locationToRange(node->identifierToken), node->formals, m_session->locationsToRange(node->lparenToken, node->rparenToken), node->body, m_session->locationsToRange(node->lbraceToken, node->rbraceToken) ); return false; } bool DeclarationBuilder::visit(QmlJS::AST::FunctionExpression* node) { declareFunction( node, false, Identifier(), QmlJS::emptyRangeOnLine(node->functionToken), node->formals, m_session->locationsToRange(node->lparenToken, node->rparenToken), node->body, m_session->locationsToRange(node->lbraceToken, node->rbraceToken) ); return false; } bool DeclarationBuilder::visit(QmlJS::AST::FormalParameterList* node) { declareParameters(node, (QmlJS::AST::UiQualifiedId* QmlJS::AST::FormalParameterList::*)nullptr); return DeclarationBuilderBase::visit(node); } bool DeclarationBuilder::visit(QmlJS::AST::UiParameterList* node) { declareParameters(node, &QmlJS::AST::UiParameterList::type); return DeclarationBuilderBase::visit(node); } bool DeclarationBuilder::visit(QmlJS::AST::ReturnStatement* node) { if (QmlJS::FunctionType::Ptr func = currentType()) { AbstractType::Ptr returnType; if (node->expression) { returnType = findType(node->expression).type; } else { returnType = new IntegralType(IntegralType::TypeVoid); } DUChainWriteLocker lock; func->setReturnType(QmlJS::mergeTypes(func->returnType(), returnType)); } return false; // findType has already explored node } void DeclarationBuilder::endVisitFunction() { QmlJS::FunctionType::Ptr func = currentType(); if (func && !func->returnType()) { // A function that returns nothing returns void DUChainWriteLocker lock; func->setReturnType(AbstractType::Ptr(new IntegralType(IntegralType::TypeVoid))); } closeAndAssignType(); } void DeclarationBuilder::endVisit(QmlJS::AST::FunctionDeclaration* node) { DeclarationBuilderBase::endVisit(node); endVisitFunction(); } void DeclarationBuilder::endVisit(QmlJS::AST::FunctionExpression* node) { DeclarationBuilderBase::endVisit(node); endVisitFunction(); } /* * Variables */ void DeclarationBuilder::inferArgumentsFromCall(QmlJS::AST::Node* base, QmlJS::AST::ArgumentList* arguments) { ContextBuilder::ExpressionType expr = findType(base); QmlJS::FunctionType::Ptr func_type = QmlJS::FunctionType::Ptr::dynamicCast(expr.type); DUChainWriteLocker lock; if (!func_type) { return; } auto func_declaration = dynamic_cast(func_type->declaration(topContext())); if (!func_declaration || !func_declaration->internalContext()) { return; } // Put the argument nodes in a list that has a definite size QVector argumentDecls = func_declaration->internalContext()->localDeclarations(); QVector args; for (auto argument = arguments; argument; argument = argument->next) { args.append(argument); } // Don't update a function when it is called with the wrong number // of arguments if (args.size() != argumentDecls.count()) { return; } // Update the types of the function arguments QmlJS::FunctionType::Ptr new_func_type(new QmlJS::FunctionType); for (int i=0; iabstractType(); // Merge the current type of the argument with its type in the call expression AbstractType::Ptr call_type = findType(argument->expression).type; AbstractType::Ptr new_type = QmlJS::mergeTypes(current_type, call_type); // Update the declaration of the argument and its type in the function type if (func_declaration->topContext() == topContext()) { new_func_type->addArgument(new_type); argumentDecls.at(i)->setAbstractType(new_type); } // Add a warning if it is possible that the argument types don't match if (!m_prebuilding && !areTypesEqual(current_type, call_type)) { m_session->addProblem(argument, i18n( "Possible type mismatch between the argument type (%1) and the value passed as argument (%2)", current_type->toString(), call_type->toString() ), IProblem::Hint); } } // Replace the function's type with the new type having updated arguments if (func_declaration->topContext() == topContext()) { new_func_type->setReturnType(func_type->returnType()); new_func_type->setDeclaration(func_declaration); func_declaration->setAbstractType(new_func_type.cast()); if (expr.declaration) { // expr.declaration is the variable that contains the function, while // func_declaration is the declaration of the function. They can be // different and both need to be updated expr.declaration->setAbstractType(new_func_type.cast()); } } return; } bool DeclarationBuilder::visit(QmlJS::AST::VariableDeclaration* node) { setComment(m_session->commentForLocation(node->firstSourceLocation()).toUtf8()); const Identifier name(node->name.toString()); const RangeInRevision range = m_session->locationToRange(node->identifierToken); const AbstractType::Ptr type = findType(node->expression).type; { DUChainWriteLocker lock; openDeclaration(name, range); } openType(type); return false; // findType has already explored node } void DeclarationBuilder::endVisit(QmlJS::AST::VariableDeclaration* node) { DeclarationBuilderBase::endVisit(node); closeAndAssignType(); } bool DeclarationBuilder::visit(QmlJS::AST::BinaryExpression* node) { if (node->op == QSOperator::Assign) { ExpressionType leftType = findType(node->left); ExpressionType rightType = findType(node->right); DUChainWriteLocker lock; if (leftType.declaration) { DUContext* leftCtx = leftType.declaration->context(); DUContext* leftInternalCtx = QmlJS::getInternalContext(leftType.declaration); // object.prototype.method = function(){} : when assigning a function // to a variable living in a Class context, set the prototype // context of the function to the context of the variable if (rightType.declaration && leftCtx->type() == DUContext::Class) { auto func = rightType.declaration.dynamicCast(); if (!QmlJS::getOwnerOfContext(leftCtx) && !leftCtx->importers().isEmpty()) { // MyClass.prototype.myfunc declares "myfunc" in a small context // that is imported by MyClass. The prototype of myfunc should // be the context of MyClass, not the small context in which // it has been declared leftCtx = leftCtx->importers().at(0); } if (func && !func->prototypeContext()) { func->setPrototypeContext(leftCtx); } } if (leftType.declaration->topContext() != topContext()) { // Do not modify a declaration of another file } else if (leftType.isPrototype && leftInternalCtx) { // Assigning something to a prototype is equivalent to making it // inherit from a class: "Class.prototype = ClassOrObject;" leftInternalCtx->clearImportedParentContexts(); QmlJS::importDeclarationInContext( leftInternalCtx, rightType.declaration ); } else { // Merge the already-known type of the variable with the new one leftType.declaration->setAbstractType(QmlJS::mergeTypes(leftType.type, rightType.type)); } } return false; // findType has already explored node } return DeclarationBuilderBase::visit(node); } bool DeclarationBuilder::visit(QmlJS::AST::CallExpression* node) { inferArgumentsFromCall(node->base, node->arguments); return false; } bool DeclarationBuilder::visit(QmlJS::AST::NewMemberExpression* node) { inferArgumentsFromCall(node->base, node->arguments); return false; } /* * Arrays */ void DeclarationBuilder::declareFieldMember(const KDevelop::DeclarationPointer& declaration, const QString& member, QmlJS::AST::Node* node, const QmlJS::AST::SourceLocation& location) { if (QmlJS::isPrototypeIdentifier(member)) { // Don't declare "prototype", this is a special member return; } if (!m_session->allDependenciesSatisfied()) { // Don't declare anything automatically if dependencies are missing: the // checks hereafter may pass now but fail later, thus causing disappearing // declarations return; } DUChainWriteLocker lock; Identifier identifier(member); // Declaration must have an internal context so that the member can be added // into it. DUContext* ctx = QmlJS::getInternalContext(declaration); if (!ctx || ctx->topContext() != topContext()) { return; } // No need to re-declare a field if it already exists // TODO check if we can make getDeclaration receive an Identifier directly if (QmlJS::getDeclaration(QualifiedIdentifier(identifier), ctx, false)) { return; } // The internal context of declaration is already closed and does not contain // location. This can be worked around by opening a new context, declaring the // new field in it, and then adding the context as a parent of // declaration->internalContext(). RangeInRevision range = m_session->locationToRange(location); IntegralType::Ptr type = IntegralType::Ptr(new IntegralType(IntegralType::TypeMixed)); DUContext* importedContext = openContext(node, range, DUContext::Class); auto* decl = openDeclaration(identifier, range); decl->setInSymbolTable(false); // This declaration is in an anonymous context, and the symbol table acts as if the declaration was in the global context openType(type); closeAndAssignType(); closeContext(); ctx->addImportedParentContext(importedContext); } bool DeclarationBuilder::visit(QmlJS::AST::FieldMemberExpression* node) { setComment(m_session->commentForLocation(node->firstSourceLocation()).toUtf8()); ExpressionType type = findType(node->base); if (type.declaration) { declareFieldMember( type.declaration, node->name.toString(), node, node->identifierToken ); } return false; // findType has already visited node->base } bool DeclarationBuilder::visit(QmlJS::AST::ArrayMemberExpression* node) { setComment(m_session->commentForLocation(node->firstSourceLocation()).toUtf8()); // When the user types array["new_key"], declare "new_key" as a new field of // array. auto stringLiteral = QmlJS::AST::cast(node->expression); if (!stringLiteral) { return DeclarationBuilderBase::visit(node); } ExpressionType type = findType(node->base); if (type.declaration) { declareFieldMember( type.declaration, stringLiteral->value.toString(), node, stringLiteral->literalToken ); } node->expression->accept(this); return false; // findType has already visited node->base, and we have just visited node->expression } bool DeclarationBuilder::visit(QmlJS::AST::ObjectLiteral* node) { setComment(m_session->commentForLocation(node->firstSourceLocation()).toUtf8()); // Object literals can appear in the "values" property of enumerations. Their // keys must be declared in the enumeration, not in an anonymous class if (currentContext()->type() == DUContext::Enum) { return DeclarationBuilderBase::visit(node); } // Open an anonymous class declaration, with its internal context StructureType::Ptr type(new StructureType); { DUChainWriteLocker lock; auto* decl = openDeclaration( Identifier(), QmlJS::emptyRangeOnLine(node->lbraceToken) ); decl->setKind(Declaration::Type); decl->setInternalContext(openContext( node, m_session->locationsToRange(node->lbraceToken, node->rbraceToken), DUContext::Class )); type->setDeclaration(decl); // Every object literal inherits from Object QmlJS::importObjectContext(currentContext(), topContext()); } openType(type); return DeclarationBuilderBase::visit(node); } bool DeclarationBuilder::visit(QmlJS::AST::PropertyNameAndValue* node) { setComment(node); if (!node->name || !node->value) { return DeclarationBuilderBase::visit(node); } RangeInRevision range(m_session->locationToRange(node->name->propertyNameToken)); Identifier name(QmlJS::getNodeValue(node->name)); // The type of the declaration can either be an enumeration value or the type // of its expression ExpressionType type; bool inSymbolTable = false; if (currentContext()->type() == DUContext::Enum) { // This is an enumeration value auto value = QmlJS::AST::cast(node->value); EnumeratorType::Ptr enumerator(new EnumeratorType); enumerator->setDataType(IntegralType::TypeInt); if (value) { enumerator->setValue((int)value->value); } type.type = AbstractType::Ptr::staticCast(enumerator); type.declaration = nullptr; inSymbolTable = true; } else { // Normal value type = findType(node->value); } // If a function is assigned to an object member, set the prototype context // of the function to the object containing the member if (type.declaration) { DUChainWriteLocker lock; auto func = type.declaration.dynamicCast(); if (func && !func->prototypeContext()) { func->setPrototypeContext(currentContext()); } } // Open the declaration { DUChainWriteLocker lock; auto* decl = openDeclaration(name, range); decl->setInSymbolTable(inSymbolTable); } openType(type.type); return false; // findType has already explored node->expression } void DeclarationBuilder::endVisit(QmlJS::AST::PropertyNameAndValue* node) { DeclarationBuilderBase::endVisit(node); closeAndAssignType(); } void DeclarationBuilder::endVisit(QmlJS::AST::ObjectLiteral* node) { DeclarationBuilderBase::endVisit(node); if (currentContext()->type() != DUContext::Enum) { // Enums are special-cased in visit(ObjectLiteral) closeContext(); closeAndAssignType(); } } /* * plugins.qmltypes files */ void DeclarationBuilder::declareComponent(QmlJS::AST::UiObjectInitializer* node, const RangeInRevision &range, const Identifier &name) { QString baseClass = QmlJS::getQMLAttributeValue(node->members, QStringLiteral("prototype")).value.section(QLatin1Char('/'), -1, -1); // Declare the component itself StructureType::Ptr type(new StructureType); ClassDeclaration* decl; { DUChainWriteLocker lock; decl = openDeclaration(name, range); decl->setKind(Declaration::Type); decl->setClassType(ClassDeclarationData::Interface); decl->clearBaseClasses(); if (!baseClass.isEmpty()) { addBaseClass(decl, baseClass); } type->setDeclaration(decl); decl->setType(type); // declareExports needs to know the type of decl } openType(type); } void DeclarationBuilder::declareMethod(QmlJS::AST::UiObjectInitializer* node, const RangeInRevision &range, const Identifier &name, bool isSlot, bool isSignal) { QString type_name = QmlJS::getQMLAttributeValue(node->members, QStringLiteral("type")).value; QmlJS::FunctionType::Ptr type(new QmlJS::FunctionType); if (type_name.isEmpty()) { type->setReturnType(typeFromName(QStringLiteral("void"))); } else { type->setReturnType(typeFromName(type_name)); } { DUChainWriteLocker lock; auto* decl = openDeclaration(name, range); decl->setIsSlot(isSlot); decl->setIsSignal(isSignal); type->setDeclaration(decl); } openType(type); } void DeclarationBuilder::declareProperty(QmlJS::AST::UiObjectInitializer* node, const RangeInRevision &range, const Identifier &name) { AbstractType::Ptr type = typeFromName(QmlJS::getQMLAttributeValue(node->members, QStringLiteral("type")).value); { DUChainWriteLocker lock; auto* decl = openDeclaration(name, range); decl->setAbstractType(type); } openType(type); } void DeclarationBuilder::declareParameter(QmlJS::AST::UiObjectInitializer* node, const RangeInRevision &range, const Identifier &name) { QmlJS::FunctionType::Ptr function = currentType(); AbstractType::Ptr type = typeFromName(QmlJS::getQMLAttributeValue(node->members, QStringLiteral("type")).value); Q_ASSERT(function); function->addArgument(type); { DUChainWriteLocker lock; openDeclaration(name, range); } openType(type); } void DeclarationBuilder::declareEnum(const RangeInRevision &range, const Identifier &name) { EnumerationType::Ptr type(new EnumerationType); { DUChainWriteLocker lock; auto* decl = openDeclaration(name, range); decl->setKind(Declaration::Type); decl->setType(type); // The type needs to be set here because closeContext is called before closeAndAssignType and needs to know the type of decl type->setDataType(IntegralType::TypeEnumeration); type->setDeclaration(decl); } openType(type); } void DeclarationBuilder::declareComponentSubclass(QmlJS::AST::UiObjectInitializer* node, const KDevelop::RangeInRevision& range, const QString& baseclass, QmlJS::AST::UiQualifiedId* qualifiedId) { Identifier name( QmlJS::getQMLAttributeValue(node->members, QStringLiteral("name")).value.section(QLatin1Char('/'), -1, -1) ); DUContext::ContextType contextType = DUContext::Class; if (baseclass == QLatin1String("Component")) { // QML component, equivalent to a QML class declareComponent(node, range, name); } else if (baseclass == QLatin1String("Method") || baseclass == QLatin1String("Signal") || baseclass == QLatin1String("Slot")) { // Method (that can also be a signal or a slot) declareMethod(node, range, name, baseclass == QLatin1String("Slot"), baseclass == QLatin1String("Signal")); contextType = DUContext::Function; } else if (baseclass == QLatin1String("Property")) { // A property declareProperty(node, range, name); } else if (baseclass == QLatin1String("Parameter") && currentType()) { // One parameter of a signal/slot/method declareParameter(node, range, name); } else if (baseclass == QLatin1String("Enum")) { // Enumeration. The "values" key contains a dictionary of name -> number entries. declareEnum(range, name); contextType = DUContext::Enum; name = Identifier(); // Enum contexts should have no name so that their members have the correct scope } else { // Define an anonymous subclass of the baseclass. This subclass will // be instantiated when "id:" is encountered name = Identifier(); // Use ExpressionVisitor to find the declaration of the base class DeclarationPointer baseClass = findType(qualifiedId).declaration; StructureType::Ptr type(new StructureType); { DUChainWriteLocker lock; auto* decl = openDeclaration( currentContext()->type() == DUContext::Global ? Identifier(m_session->moduleName()) : name, QmlJS::emptyRangeOnLine(node->lbraceToken) ); decl->clearBaseClasses(); decl->setKind(Declaration::Type); decl->setType(type); // The class needs to know its type early because it contains definitions that depend on that type type->setDeclaration(decl); if (baseClass) { addBaseClass(decl, baseClass->indexedType()); } } openType(type); } // Open a context of the proper type and identifier openContext( node, m_session->locationsToInnerRange(node->lbraceToken, node->rbraceToken), contextType, QualifiedIdentifier(name) ); DUContext* ctx = currentContext(); Declaration* decl = currentDeclaration(); { // Set the inner context of the current declaration, because nested classes // need to know the inner context of their parents DUChainWriteLocker lock; decl->setInternalContext(ctx); if (contextType == DUContext::Enum) { ctx->setPropagateDeclarations(true); } } // If we have have declared a class, import the context of its base classes registerBaseClasses(); } void DeclarationBuilder::declareComponentInstance(QmlJS::AST::ExpressionStatement* expression) { if (!expression) { return; } auto identifier = QmlJS::AST::cast(expression->expression); if (!identifier) { return; } { DUChainWriteLocker lock; injectContext(topContext()); auto* decl = openDeclaration( Identifier(identifier->name.toString()), m_session->locationToRange(identifier->identifierToken) ); closeInjectedContext(); // Put the declaration in the global scope decl->setKind(Declaration::Instance); decl->setType(currentAbstractType()); } closeDeclaration(); } DeclarationBuilder::ExportLiteralsAndNames DeclarationBuilder::exportedNames(QmlJS::AST::ExpressionStatement* exports) { ExportLiteralsAndNames res; if (!exports) { return res; } auto exportslist = QmlJS::AST::cast(exports->expression); if (!exportslist) { return res; } // Explore all the exported symbols for this component and keep only those // having a version compatible with the one of this module QSet knownNames; for (auto it = exportslist->elements; it && it->expression; it = it->next) { auto stringliteral = QmlJS::AST::cast(it->expression); if (!stringliteral) { continue; } // String literal like "Namespace/Class version". QStringList nameAndVersion = stringliteral->value.toString().section(QLatin1Char('/'), -1, -1).split(QLatin1Char(' ')); QString name = nameAndVersion.at(0); if (!knownNames.contains(name)) { knownNames.insert(name); res.append(qMakePair(stringliteral, name)); } } return res; } void DeclarationBuilder::declareExports(const ExportLiteralsAndNames& exports, ClassDeclaration* classdecl) { DUChainWriteLocker lock; // Create the exported versions of the component for (auto& exp : exports) { QmlJS::AST::StringLiteral* literal = exp.first; QString name = exp.second; StructureType::Ptr type(new StructureType); injectContext(currentContext()->parentContext()); // Don't declare the export in its C++-ish component, but in the scope above auto* decl = openDeclaration( Identifier(name), m_session->locationToRange(literal->literalToken) ); closeInjectedContext(); // The exported version inherits from the C++ component decl->setKind(Declaration::Type); decl->setClassType(ClassDeclarationData::Class); decl->clearBaseClasses(); type->setDeclaration(decl); addBaseClass(decl, classdecl->indexedType()); // Open a context for the exported class, and register its base class in it decl->setInternalContext(openContext( literal, DUContext::Class, QualifiedIdentifier(name) )); registerBaseClasses(); closeContext(); openType(type); closeAndAssignType(); } } /* * UI */ void DeclarationBuilder::importDirectory(const QString& directory, QmlJS::AST::UiImport* node) { DUChainWriteLocker lock; QString currentFilePath = currentContext()->topContext()->url().str(); QFileInfo dir(directory); QFileInfoList entries; if (dir.isDir()) { // Import all the files in the given directory entries = QDir(directory).entryInfoList( QStringList{ (QLatin1String("*.") + currentFilePath.section(QLatin1Char('.'), -1, -1)), QStringLiteral("*.qmltypes"), QStringLiteral("*.so")}, QDir::Files ); } else if (dir.isFile()) { // Import the specific file given in the import statement entries.append(dir); } else if (!m_prebuilding) { m_session->addProblem(node, i18n("Module not found, some types or properties may not be recognized")); return; } // Translate the QFileInfos into QStrings (and replace .so files with // qmlplugindump dumps) lock.unlock(); - QStringList filePaths = QmlJS::Cache::instance().getFileNames(entries); + const QStringList filePaths = QmlJS::Cache::instance().getFileNames(entries); lock.lock(); if (node && !node->importId.isEmpty()) { // Open a namespace that will contain the declarations Identifier identifier(node->importId.toString()); RangeInRevision range = m_session->locationToRange(node->importIdToken); auto* decl = openDeclaration(identifier, range); decl->setKind(Declaration::Namespace); decl->setInternalContext(openContext(node, range, DUContext::Class, QualifiedIdentifier(identifier))); } for (const QString& filePath : filePaths) { if (filePath == currentFilePath) { continue; } ReferencedTopDUContext context = m_session->contextOfFile(filePath); if (context) { currentContext()->addImportedParentContext(context.data()); } } if (node && !node->importId.isEmpty()) { // Close the namespace containing the declarations closeContext(); closeDeclaration(); } } void DeclarationBuilder::importModule(QmlJS::AST::UiImport* node) { QmlJS::AST::UiQualifiedId *part = node->importUri; QString uri; while (part) { if (!uri.isEmpty()) { uri.append(QLatin1Char('.')); } uri.append(part->name.toString()); part = part->next; } // Version of the import QString version = m_session->symbolAt(node->versionToken); // Import the directory containing the module QString modulePath = QmlJS::Cache::instance().modulePath(m_session->url(), uri, version); importDirectory(modulePath, node); } bool DeclarationBuilder::visit(QmlJS::AST::UiImport* node) { if (node->importUri) { importModule(node); } else if (!node->fileName.isEmpty() && node->fileName != QLatin1String(".")) { QUrl currentFileUrl = currentContext()->topContext()->url().toUrl(); QUrl importUrl = QUrl(node->fileName.toString()); importDirectory(currentFileUrl.resolved(importUrl).toLocalFile(), node); } return DeclarationBuilderBase::visit(node); } bool DeclarationBuilder::visit(QmlJS::AST::UiObjectDefinition* node) { setComment(node); // Do not crash if the user has typed an empty object definition if (!node->initializer || !node->initializer->members) { m_skipEndVisit.push(true); return DeclarationBuilderBase::visit(node); } RangeInRevision range(m_session->locationToRange(node->qualifiedTypeNameId->identifierToken)); QString baseclass = node->qualifiedTypeNameId->name.toString(); // "Component" needs special care: a component that appears only in a future // version of this module, or that already appeared in a former version, must // be skipped because it is useless ExportLiteralsAndNames exports; if (baseclass == QLatin1String("Component")) { QmlJS::AST::Statement* statement = QmlJS::getQMLAttribute(node->initializer->members, QStringLiteral("exports")); exports = exportedNames(QmlJS::AST::cast(statement)); if (statement && exports.count() == 0) { // This component has an "exports:" member but no export matched // the version of this module. Skip the component m_skipEndVisit.push(true); return false; } } else if (baseclass == QLatin1String("Module")) { // "Module" is disabled. This allows the declarations of a module // dump to appear in the same namespace as the .qml files in the same // directory. m_skipEndVisit.push(true); return true; } // Declare the component subclass declareComponentSubclass(node->initializer, range, baseclass, node->qualifiedTypeNameId); // If we had a component with exported names, declare these exports if (baseclass == QLatin1String("Component")) { auto* classDecl = currentDeclaration(); if (classDecl) { declareExports(exports, classDecl); } } m_skipEndVisit.push(false); return DeclarationBuilderBase::visit(node); } void DeclarationBuilder::endVisit(QmlJS::AST::UiObjectDefinition* node) { DeclarationBuilderBase::endVisit(node); // Do not crash if the user has typed an empty object definition if (!m_skipEndVisit.pop()) { closeContext(); closeAndAssignType(); } } bool DeclarationBuilder::visit(QmlJS::AST::UiScriptBinding* node) { setComment(node); if (!node->qualifiedId) { return DeclarationBuilderBase::visit(node); } // Special-case some binding names QString bindingName = node->qualifiedId->name.toString(); if (bindingName == QLatin1String("id")) { // Instantiate a QML component: its type is the current type (the anonymous // QML class that surrounds the declaration) declareComponentInstance(QmlJS::AST::cast(node->statement)); } // Use ExpressionVisitor to find the signal/property bound DeclarationPointer bindingDecl = findType(node->qualifiedId).declaration; DUChainPointer signal; // If a Javascript block is used as expression or if the script binding is a // slot, open a subcontext so that variables declared in the binding are kept // local, and the signal parameters can be visible to the slot if (( bindingDecl && (signal = bindingDecl.dynamicCast()) && signal->isSignal() ) || node->statement->kind == QmlJS::AST::Node::Kind_Block) { openContext( node->statement, m_session->locationsToInnerRange( node->statement->firstSourceLocation(), node->statement->lastSourceLocation() ), DUContext::Other ); // If this script binding is a slot, import the parameters of its signal if (signal && signal->isSignal() && signal->internalContext()) { DUChainWriteLocker lock; currentContext()->addIndirectImport(DUContext::Import( signal->internalContext(), nullptr )); } } else { // Check that the type of the value matches the type of the property AbstractType::Ptr expressionType = findType(node->statement).type; DUChainReadLocker lock; if (!m_prebuilding && bindingDecl && !areTypesEqual(bindingDecl->abstractType(), expressionType)) { m_session->addProblem(node->qualifiedId, i18n( "Mismatch between the value type (%1) and the property type (%2)", expressionType->toString(), bindingDecl->abstractType()->toString() ), IProblem::Error); } } return DeclarationBuilderBase::visit(node); } void DeclarationBuilder::endVisit(QmlJS::AST::UiScriptBinding* node) { QmlJS::AST::Visitor::endVisit(node); // If visit(UiScriptBinding) has opened a context, close it if (currentContext()->type() == DUContext::Other) { closeContext(); } } bool DeclarationBuilder::visit(QmlJS::AST::UiObjectBinding* node) { setComment(node); if (!node->qualifiedId || !node->qualifiedTypeNameId || !node->initializer) { return DeclarationBuilderBase::visit(node); } // Declare the component subclass. "Behavior on ... {}" is treated exactly // like "Behavior {}". RangeInRevision range = m_session->locationToRange(node->qualifiedTypeNameId->identifierToken); QString baseclass = node->qualifiedTypeNameId->name.toString(); declareComponentSubclass(node->initializer, range, baseclass, node->qualifiedTypeNameId); return DeclarationBuilderBase::visit(node); } void DeclarationBuilder::endVisit(QmlJS::AST::UiObjectBinding* node) { DeclarationBuilderBase::endVisit(node); if (node->qualifiedId && node->qualifiedTypeNameId && node->initializer) { closeContext(); closeAndAssignType(); } } bool DeclarationBuilder::visit(QmlJS::AST::UiPublicMember* node) { setComment(node); RangeInRevision range = m_session->locationToRange(node->identifierToken); Identifier id(node->name.toString()); QString typeName = node->memberTypeName().toString(); bool res = DeclarationBuilderBase::visit(node); // Build the type of the public member if (node->type == QmlJS::AST::UiPublicMember::Signal) { // Open a function declaration corresponding to this signal declareFunction( node, false, Identifier(node->name.toString()), m_session->locationToRange(node->identifierToken), node->parameters, m_session->locationToRange(node->identifierToken), // The AST does not provide the location of the parens nullptr, m_session->locationToRange(node->identifierToken) // A body range must be provided ); // This declaration is a signal and its return type is void { DUChainWriteLocker lock; currentDeclaration()->setIsSignal(true); currentType()->setReturnType(typeFromName(QStringLiteral("void"))); } } else { AbstractType::Ptr type; if (typeName == QLatin1String("alias")) { // Property aliases take the type of their aliased property type = findType(node->statement).type; res = false; // findType has already explored node->statement } else { type = typeFromName(typeName); if (node->typeModifier == QLatin1String("list")) { // QML list, noted "list" in the source file ArrayType::Ptr array(new ArrayType); array->setElementType(type); type = array.cast(); } } { DUChainWriteLocker lock; Declaration* decl = openDeclaration(id, range); decl->setInSymbolTable(false); } openType(type); } return res; } void DeclarationBuilder::endVisit(QmlJS::AST::UiPublicMember* node) { DeclarationBuilderBase::endVisit(node); closeAndAssignType(); } /* * Utils */ void DeclarationBuilder::setComment(QmlJS::AST::Node* node) { setComment(m_session->commentForLocation(node->firstSourceLocation()).toUtf8()); } void DeclarationBuilder::closeAndAssignType() { closeType(); Declaration* dec = currentDeclaration(); Q_ASSERT(dec); if (auto type = lastType()) { DUChainWriteLocker lock; dec->setType(type); } closeDeclaration(); } AbstractType::Ptr DeclarationBuilder::typeFromName(const QString& name) { auto type = IntegralType::TypeNone; QString realName = name; // Built-in types if (name == QLatin1String("string")) { type = IntegralType::TypeString; } else if (name == QLatin1String("bool")) { type = IntegralType::TypeBoolean; } else if (name == QLatin1String("int")) { type = IntegralType::TypeInt; } else if (name == QLatin1String("float")) { type = IntegralType::TypeFloat; } else if (name == QLatin1String("double") || name == QLatin1String("real")) { type = IntegralType::TypeDouble; } else if (name == QLatin1String("void")) { type = IntegralType::TypeVoid; } else if (name == QLatin1String("var") || name == QLatin1String("variant")) { type = IntegralType::TypeMixed; } else if (m_session->language() == QmlJS::Dialect::Qml) { // In QML files, some Qt type names need to be renamed to the QML equivalent if (name == QLatin1String("QFont")) { realName = QStringLiteral("Font"); } else if (name == QLatin1String("QColor")) { realName = QStringLiteral("color"); } else if (name == QLatin1String("QDateTime")) { realName = QStringLiteral("date"); } else if (name == QLatin1String("QDate")) { realName = QStringLiteral("date"); } else if (name == QLatin1String("QTime")) { realName = QStringLiteral("time"); } else if (name == QLatin1String("QRect") || name == QLatin1String("QRectF")) { realName = QStringLiteral("rect"); } else if (name == QLatin1String("QPoint") || name == QLatin1String("QPointF")) { realName = QStringLiteral("point"); } else if (name == QLatin1String("QSize") || name == QLatin1String("QSizeF")) { realName = QStringLiteral("size"); } else if (name == QLatin1String("QUrl")) { realName = QStringLiteral("url"); } else if (name == QLatin1String("QVector3D")) { realName = QStringLiteral("vector3d"); } else if (name.endsWith(QLatin1String("ScriptString"))) { // Q{Declarative,Qml}ScriptString represents a JS snippet auto func = new QmlJS::FunctionType; func->setReturnType(AbstractType::Ptr(new IntegralType(IntegralType::TypeVoid))); return AbstractType::Ptr(func); } } if (type == IntegralType::TypeNone) { // Not a built-in type, but a class return typeFromClassName(realName); } else { return AbstractType::Ptr(new IntegralType(type)); } } AbstractType::Ptr DeclarationBuilder::typeFromClassName(const QString& name) { DeclarationPointer decl = QmlJS::getDeclaration(QualifiedIdentifier(name), currentContext()); if (!decl) { if (name == QLatin1String("QRegExp")) { decl = QmlJS::NodeJS::instance().moduleMember(QStringLiteral("__builtin_ecmascript"), QStringLiteral("RegExp"), currentContext()->url()); } } if (decl) { return decl->abstractType(); } else { DelayedType::Ptr type(new DelayedType); type->setKind(DelayedType::Unresolved); type->setIdentifier(IndexedTypeIdentifier(name)); return type; } } void DeclarationBuilder::addBaseClass(ClassDeclaration* classDecl, const QString& name) { addBaseClass(classDecl, IndexedType(typeFromClassName(name))); } void DeclarationBuilder::addBaseClass(ClassDeclaration* classDecl, const IndexedType& type) { BaseClassInstance baseClass; baseClass.access = Declaration::Public; baseClass.virtualInheritance = false; baseClass.baseClass = type; classDecl->addBaseClass(baseClass); } void DeclarationBuilder::registerBaseClasses() { auto* classdecl = currentDeclaration(); DUContext *ctx = currentContext(); if (classdecl) { DUChainWriteLocker lock; for (uint i=0; ibaseClassesSize(); ++i) { const BaseClassInstance &baseClass = classdecl->baseClasses()[i]; StructureType::Ptr baseType = StructureType::Ptr::dynamicCast(baseClass.baseClass.abstractType()); TopDUContext* topctx = topContext(); if (baseType && baseType->declaration(topctx)) { QmlJS::importDeclarationInContext(ctx, DeclarationPointer(baseType->declaration(topctx))); } } } } static bool enumContainsEnumerator(const AbstractType::Ptr& a, const AbstractType::Ptr& b) { Q_ASSERT(a->whichType() == AbstractType::TypeEnumeration); auto aEnum = EnumerationType::Ptr::staticCast(a); Q_ASSERT(b->whichType() == AbstractType::TypeEnumerator); auto bEnumerator = EnumeratorType::Ptr::staticCast(b); return bEnumerator->qualifiedIdentifier().beginsWith(aEnum->qualifiedIdentifier()); } static bool isNumeric(const IntegralType::Ptr& type) { return type->dataType() == IntegralType::TypeInt || type->dataType() == IntegralType::TypeIntegral || type->dataType() == IntegralType::TypeFloat || type->dataType() == IntegralType::TypeDouble; } bool DeclarationBuilder::areTypesEqual(const AbstractType::Ptr& a, const AbstractType::Ptr& b) { if (!a || !b) { return true; } if (a->whichType() == AbstractType::TypeUnsure || b->whichType() == AbstractType::TypeUnsure) { // Don't try to guess something if one of the types is unsure return true; } const auto bIntegral = IntegralType::Ptr::dynamicCast(b); if (bIntegral && (bIntegral->dataType() == IntegralType::TypeString || bIntegral->dataType() == IntegralType::TypeMixed)) { // In QML/JS, a string can be converted to nearly everything else, similarly ignore mixed types return true; } const auto aIntegral = IntegralType::Ptr::dynamicCast(a); if (aIntegral && (aIntegral->dataType() == IntegralType::TypeString || aIntegral->dataType() == IntegralType::TypeMixed)) { // In QML/JS, nearly everything can be to a string, similarly ignore mixed types return true; } if (aIntegral && bIntegral) { if (isNumeric(aIntegral) && isNumeric(bIntegral)) { // Casts between integral types is possible return true; } } if (a->whichType() == AbstractType::TypeEnumeration && b->whichType() == AbstractType::TypeEnumerator) { return enumContainsEnumerator(a, b); } else if (a->whichType() == AbstractType::TypeEnumerator && b->whichType() == AbstractType::TypeEnumeration) { return enumContainsEnumerator(b, a); } { auto aId = dynamic_cast(a.constData()); auto bId = dynamic_cast(b.constData()); if (aId && bId && aId->qualifiedIdentifier() == bId->qualifiedIdentifier()) return true; } { auto aStruct = StructureType::Ptr::dynamicCast(a); auto bStruct = StructureType::Ptr::dynamicCast(b); if (aStruct && bStruct) { auto top = currentContext()->topContext(); auto aDecl = dynamic_cast(aStruct->declaration(top)); auto bDecl = dynamic_cast(bStruct->declaration(top)); if (aDecl && bDecl) { if (aDecl->isPublicBaseClass(bDecl, top) || bDecl->isPublicBaseClass(aDecl, top)) { return true; } } } } return a->equals(b.constData()); } diff --git a/plugins/qmljs/qmljsparsejob.cpp b/plugins/qmljs/qmljsparsejob.cpp index 52eb7b61c1..23c26c3867 100644 --- a/plugins/qmljs/qmljsparsejob.cpp +++ b/plugins/qmljs/qmljsparsejob.cpp @@ -1,209 +1,209 @@ /************************************************************************************* * Copyright (C) 2012 by Aleix Pol * * Copyright (C) 2012 by Milian Wolff * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 2 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * *************************************************************************************/ #include "qmljsparsejob.h" #include #include #include #include #include #include #include #include #include #include #include #include "duchain/cache.h" #include "duchain/declarationbuilder.h" #include "duchain/parsesession.h" #include "duchain/usebuilder.h" #include "debug.h" #include using namespace KDevelop; /* * This function has been copied from kdev-clang * * Copyright 2013 Olivier de Gaalon and Milian Wolff * Licensed under the GPL v2+ */ ProjectFileItem* findProjectFileItem(const IndexedString& url) { ProjectFileItem* file = nullptr; const auto& projects = ICore::self()->projectController()->projects(); for (auto project: projects) { - auto files = project->filesForPath(url); + const auto files = project->filesForPath(url); if (files.isEmpty()) { continue; } file = files.last(); // A file might be defined in different targets. // Prefer file items defined inside a target with non-empty includes. for (auto f: files) { if (!dynamic_cast(f->parent())) { continue; } file = f; if (!IDefinesAndIncludesManager::manager()->includes(f, IDefinesAndIncludesManager::ProjectSpecific).isEmpty()) { break; } } } return file; } QmlJsParseJob::QmlJsParseJob(const IndexedString& url, ILanguageSupport* languageSupport) : ParseJob(url, languageSupport) { // Tell the cache that this file has custom include directories if (auto file = findProjectFileItem(url)) { QmlJS::Cache::instance().setFileCustomIncludes( url, IDefinesAndIncludesManager::manager()->includes(file, IDefinesAndIncludesManager::Type( IDefinesAndIncludesManager::ProjectSpecific | IDefinesAndIncludesManager::UserDefined)) ); } else { QmlJS::Cache::instance().setFileCustomIncludes( url, IDefinesAndIncludesManager::manager()->includes(url.str(), IDefinesAndIncludesManager::ProjectSpecific) ); } } void QmlJsParseJob::run(ThreadWeaver::JobPointer pointer, ThreadWeaver::Thread* thread) { Q_UNUSED(pointer) Q_UNUSED(thread) UrlParseLock urlLock(document()); if (abortRequested() || !isUpdateRequired(ParseSession::languageString())) { return; } // Don't parse this file if one of its dependencies is not up to date const auto& dependencies = QmlJS::Cache::instance().dependencies(document()); for (auto& dependency : dependencies) { if (!QmlJS::Cache::instance().isUpToDate(dependency)) { QmlJS::Cache::instance().setUpToDate(document(), false); return; } } qCDebug(KDEV_QMLJS) << "parsing" << document().str(); ProblemPointer p = readContents(); if (p) { //TODO: associate problem with topducontext return; } ParseSession session(document(), QString::fromUtf8(contents().contents), priority()); if (abortRequested()) { return; } ReferencedTopDUContext context; { DUChainReadLocker lock; context = DUChainUtils::standardContextForUrl(document().toUrl()); } if (context) { translateDUChainToRevision(context); context->setRange(RangeInRevision(0, 0, INT_MAX, INT_MAX)); } if (session.ast()) { QReadLocker parseLock(languageSupport()->parseLock()); if (abortRequested()) { abortJob(); return; } DeclarationBuilder builder(&session); context = builder.build(document(), session.ast(), context); if (abortRequested()) { abortJob(); return; } if ( context && minimumFeatures() & TopDUContext::AllDeclarationsContextsAndUses ) { UseBuilder useBuilder(&session); useBuilder.buildUses(session.ast()); } } if (abortRequested()) { abortJob(); return; } if (!context) { DUChainWriteLocker lock; ParsingEnvironmentFile *file = new ParsingEnvironmentFile(document()); file->setLanguage(ParseSession::languageString()); context = new TopDUContext(document(), RangeInRevision(0, 0, INT_MAX, INT_MAX), file); DUChain::self()->addDocumentChain(context); } setDuChain(context); // If the file has become up to date, reparse its importers bool dependenciesOk = session.allDependenciesSatisfied(); QmlJS::Cache::instance().setUpToDate(document(), dependenciesOk); if (dependenciesOk) { session.reparseImporters(); } { DUChainWriteLocker lock; context->setProblems(session.problems()); context->setFeatures(minimumFeatures()); ParsingEnvironmentFilePointer file = context->parsingEnvironmentFile(); Q_ASSERT(file); file->setModificationRevision(contents().modification); DUChain::self()->updateContextEnvironment( context->topContext(), file.data() ); } highlightDUChain(); DUChain::self()->emitUpdateReady(document(), duChain()); if (session.isParsedCorrectly()) { qCDebug(KDEV_QMLJS) << "===Success===" << document().str(); } else { qCDebug(KDEV_QMLJS) << "===Failed===" << document().str() << session.problems(); } }