diff --git a/kdevplatform/tests/testfile.cpp b/kdevplatform/tests/testfile.cpp index 4be1d79541..aab8906f3a 100644 --- a/kdevplatform/tests/testfile.cpp +++ b/kdevplatform/tests/testfile.cpp @@ -1,188 +1,199 @@ /* This file is part of KDevelop Copyright 2010 Niko Sams Copyright 2011 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 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 "testfile.h" #include "testproject.h" #include #include #include #include #include #include #include #include #include using namespace KDevelop; class KDevelop::TestFilePrivate { public: TestFilePrivate() { } void updateReady(const IndexedString& _url, const ReferencedTopDUContext& _topContext) { Q_ASSERT(_url == url); Q_UNUSED(_url); topContext = _topContext; ready = true; } void init(const QString& fileName, const QString& contents, TestProject* _project) { file = fileName; setFileContents(contents); QFileInfo info(file); Q_ASSERT(info.exists()); Q_ASSERT(info.isFile()); url = IndexedString(info.absoluteFilePath()); project = _project; if (project) { fileItem.reset(new ProjectFileItem(_project, Path(file), _project->projectItem())); } } void setFileContents(const QString& contents) { QFile file(this->file); file.open(QIODevice::WriteOnly | QIODevice::Truncate); Q_ASSERT(file.isOpen()); Q_ASSERT(file.isWritable()); file.write(contents.toUtf8()); ready = false; } QString file; QString suffix; bool ready = false; ReferencedTopDUContext topContext; IndexedString url; TestProject* project; QScopedPointer fileItem; bool keepDUChainData = false; }; TestFile::TestFile(const QString& contents, const QString& fileExtension, TestProject* project, const QString& dir) : d(new TestFilePrivate()) { d->suffix = QLatin1Char('.') + fileExtension; QTemporaryFile file((!dir.isEmpty() ? dir : QDir::tempPath()) + QLatin1String("/testfile_XXXXXX") + d->suffix); file.setAutoRemove(false); file.open(); Q_ASSERT(file.isOpen()); d->init(file.fileName(), contents, project); } TestFile::TestFile(const QString& contents, const QString& fileExtension, const TestFile* base) : d(new TestFilePrivate) { QString fileName = base->d->file.mid(0, base->d->file.length() - base->d->suffix.length()); d->suffix = QLatin1Char('.') + fileExtension; fileName += d->suffix; d->init(fileName, contents, base->d->project); } +TestFile::TestFile(const QString& contents, const QString& fileExtension, const QString& fileName, + KDevelop::TestProject* project, const QString& dir) + : d(new TestFilePrivate) +{ + d->suffix = QLatin1Char('.') + fileExtension; + const QString file = (!dir.isEmpty() ? dir : QDir::tempPath()) + + QLatin1Char('/') + fileName + d->suffix; + d->init(file, contents, project); +} + + TestFile::~TestFile() { if (d->topContext && !d->keepDUChainData) { DUChainWriteLocker lock; DUChain::self()->removeDocumentChain(d->topContext.data()); } QFile::remove(d->file); } IndexedString TestFile::url() const { return d->url; } void TestFile::parse(TopDUContext::Features features, int priority) { d->ready = false; DUChain::self()->updateContextForUrl(d->url, features, this, priority); } bool TestFile::parseAndWait(TopDUContext::Features features, int priority, int timeout) { parse(features, priority); return waitForParsed(timeout); } bool TestFile::waitForParsed(int timeout) { if (!d->ready) { // optimize: we don't want to wait the usual timeout before parsing documents here ICore::self()->languageController()->backgroundParser()->parseDocuments(); } QTime t; t.start(); while (!d->ready && t.elapsed() < timeout) { QTest::qWait(10); } return d->ready; } bool TestFile::isReady() const { return d->ready; } ReferencedTopDUContext TestFile::topContext() { waitForParsed(); return d->topContext; } void TestFile::setFileContents(const QString& contents) { d->setFileContents(contents); } QString TestFile::fileContents() const { QFile file(d->file); file.open(QIODevice::ReadOnly); Q_ASSERT(file.isOpen()); Q_ASSERT(file.isReadable()); return QString::fromUtf8(file.readAll()); } void TestFile::setKeepDUChainData(bool keep) { d->keepDUChainData = keep; } bool TestFile::keepDUChainData() { return d->keepDUChainData; } #include "moc_testfile.cpp" diff --git a/kdevplatform/tests/testfile.h b/kdevplatform/tests/testfile.h index a148a5a510..6d8bd2af20 100644 --- a/kdevplatform/tests/testfile.h +++ b/kdevplatform/tests/testfile.h @@ -1,165 +1,182 @@ /* This file is part of KDevelop Copyright 2010 Niko Sams Copyright 2011 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 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. */ #ifndef KDEVPLATFORM_TESTFILE_H #define KDEVPLATFORM_TESTFILE_H #include #include #include "testsexport.h" namespace KDevelop { class TestProject; /** * Helper file to parse a file using the full KDevelop architecture. * * The file will be added to the background parser, and eventually * parsed by a fitting language plugin, just like a normal file * would be in an actual KDevelop instance. * * Example usage: * \code * TestFile file("... * @endcode */ TestFile(const QString& contents, const QString& fileExtension, KDevelop::TestProject* project = nullptr, const QString& dir = QString()); /** * Create a temporary file from @p contents with the same file basename as * @p base but with the given @p fileExtension. * * @param fileExtension the new file extension without the dot. * @param base a different TestFile which is used for this file's basename * * This can be used to create e.g. .cpp/.h file pairs: * * @code * TestFile header("...", "h"); * TestFile impl("...", "cpp", &header); * @endcode */ TestFile(const QString& contents, const QString& fileExtension, const TestFile* base); + /** + * Create a temporary file named @p fileName from @p contents with file extension @p extension. + * + * @param fileExtension the file extension without the dot. + * @param fileName the name to use for the file + * @param project this file will be added to the project's fileset and gets + * removed from there on destruction + * @param dir optional path to a (sub-) directory in which this file should + * be created. The directory must exist. + * + * Example: + * @code + * TestFile file("int i = 0;", "h", "guard_test"); + * @endcode + */ TestFile(const QString& contents, const QString& fileExtension, const QString& fileName, + KDevelop::TestProject* project = nullptr, const QString& dir = QString()); + /** * Removes temporary file and cleans up. */ ~TestFile() override; /** * Returns the URL to this file. */ IndexedString url() const; /** * Trigger (re-)parsing of this file with given @p features and @p priority. * * @see KDevelop::DUChain::updateContextForUrl */ void parse(TopDUContext::Features features = TopDUContext::AllDeclarationsContextsAndUses, int priority = 1); /** * Convenience method: * Trigger parse and wait for the file to be parsed. Internally calls waitForParsed() * * @see waitForParsed() * @see parse() */ bool parseAndWait(TopDUContext::Features features = TopDUContext::AllDeclarationsContextsAndUses, int priority = 1, int timeout = 5000); /** * Blocks current thread and waits until the file has been parsed. * * If it has waited longer than @p timeout ms, we return false * and assume something went wrong. * * Otherwise true is returned, indicating parsing finished * within the timeout interval. */ bool waitForParsed(int timeout = 5000); /** * Check whether the file has been processed after the last call to @c parse(). */ bool isReady() const; /** * Returns the @c TopDUContext for the current file, if it has been successfully parsed. */ KDevelop::ReferencedTopDUContext topContext(); /** * Change the file contents to @p contents. * * Use this to test behavior of your parsing code over * file changes. */ void setFileContents(const QString& contents); /** * Read the files contents and return them. */ QString fileContents() const; /** * Set to true when you want to keep the DUChain data. * * By default the DUChain data is removed on destruction of the TestFile. */ void setKeepDUChainData(bool keep); bool keepDUChainData(); private: const QScopedPointer d; Q_PRIVATE_SLOT(d, void updateReady(const KDevelop::IndexedString& url, KDevelop::ReferencedTopDUContext topContext)) }; } #endif // KDEVPLATFORM_TESTFILE_H diff --git a/plugins/clang/CMakeLists.txt b/plugins/clang/CMakeLists.txt index ebd9ca8082..161692b457 100644 --- a/plugins/clang/CMakeLists.txt +++ b/plugins/clang/CMakeLists.txt @@ -1,145 +1,146 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevclang\") add_definitions(${LLVM_CFLAGS}) include_directories(${CLANG_INCLUDE_DIRS}) configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/libclang_include_path.h.cmake" "${CMAKE_CURRENT_BINARY_DIR}/libclang_include_path.h" @ONLY ) if(BUILD_TESTING) add_subdirectory(tests) endif() # TODO: Move to kdevplatform function(add_private_library target) set(options) set(oneValueArgs) set(multiValueArgs SOURCES) cmake_parse_arguments(KDEV_ADD_PRIVATE "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) string(REPLACE "KDev" "" shortTargetName ${target}) if (${shortTargetName} STREQUAL ${target}) message(FATAL_ERROR "Target passed to add_private_library needs to start with \"KDev\", was \"${target}\"") endif() string(TOLOWER ${shortTargetName} shortTargetNameToLower) add_library(${target} SHARED ${KDEV_ADD_PRIVATE_SOURCES}) generate_export_header(${target} EXPORT_FILE_NAME ${shortTargetNameToLower}export.h) set_target_properties(${target} PROPERTIES VERSION ${KDEV_PLUGIN_VERSION} SOVERSION ${KDEV_PLUGIN_VERSION} ) install(TARGETS ${target} ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) endfunction() set(kdevclangprivate_SRCS clangsettings/clangsettingsmanager.cpp clangsettings/sessionsettings/sessionsettings.cpp codecompletion/completionhelper.cpp codecompletion/context.cpp codecompletion/includepathcompletioncontext.cpp codecompletion/model.cpp codegen/adaptsignatureaction.cpp codegen/adaptsignatureassistant.cpp codegen/codegenhelper.cpp codegen/clangrefactoring.cpp codegen/clangclasshelper.cpp codegen/sourcemanipulation.cpp duchain/builder.cpp duchain/clangdiagnosticevaluator.cpp duchain/clangducontext.cpp duchain/clanghelpers.cpp duchain/clangindex.cpp duchain/clangparsingenvironment.cpp duchain/clangparsingenvironmentfile.cpp duchain/clangpch.cpp duchain/clangproblem.cpp duchain/debugvisitor.cpp duchain/documentfinderhelpers.cpp duchain/duchainutils.cpp duchain/macrodefinition.cpp duchain/macronavigationcontext.cpp duchain/missingincludepathproblem.cpp duchain/navigationwidget.cpp duchain/parsesession.cpp duchain/todoextractor.cpp duchain/types/classspecializationtype.cpp duchain/unknowndeclarationproblem.cpp duchain/unsavedfile.cpp + duchain/headerguardassistant.cpp util/clangdebug.cpp util/clangtypes.cpp util/clangutils.cpp ) # dummy call to add the data to KDevelopCategories # util/clangdebug.* cannot easily be generated with ecm_qt_declare_logging_category # as the current code does not use Q_DECLARE_LOGGING_CATEGORY but instead # has explicit code to tag KDEV_CLANG() as KDEVCLANGPRIVATE_EXPORT # Keep in sync with util/clangdebug.* declare_qt_logging_category(dummy_kdevclangprivate_SRCS TYPE PLUGIN HEADER dummy_debug.h IDENTIFIER KDEV_CLANG CATEGORY_BASENAME "clang" DESCRIPTION "clang-based language support" ) include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) ki18n_wrap_ui(kdevclangprivate_SRCS clangsettings/sessionsettings/sessionsettings.ui ) kconfig_add_kcfg_files(kdevclangprivate_SRCS clangsettings/sessionsettings/sessionconfig.kcfgc) add_private_library(KDevClangPrivate SOURCES ${kdevclangprivate_SRCS}) target_link_libraries(KDevClangPrivate LINK_PRIVATE Qt5::Core KF5::TextEditor KF5::ThreadWeaver KDev::DefinesAndIncludesManager KDev::Util LINK_PUBLIC KDev::Language KDev::Project KDev::Util Clang::clang ) install(DIRECTORY duchain/wrappedQtHeaders DESTINATION ${KDE_INSTALL_DATADIR}/kdevclangsupport DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_WRITE GROUP_EXECUTE WORLD_READ WORLD_EXECUTE FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ) set(kdevclangsupport_SRCS clangparsejob.cpp clangsupport.cpp clanghighlighting.cpp ) qt5_add_resources(kdevclangsupport_SRCS kdevclangsupport.qrc) kdevplatform_add_plugin(kdevclangsupport JSON kdevclangsupport.json SOURCES ${kdevclangsupport_SRCS}) target_link_libraries(kdevclangsupport KDevClangPrivate KF5::ThreadWeaver KF5::TextEditor KDev::Util KDev::Project KDev::DefinesAndIncludesManager ) install(FILES kdevclang.xml DESTINATION ${KDE_INSTALL_MIMEDIR}) update_xdg_mimetypes(${KDE_INSTALL_MIMEDIR}) diff --git a/plugins/clang/duchain/headerguardassistant.cpp b/plugins/clang/duchain/headerguardassistant.cpp new file mode 100644 index 0000000000..ae35e88db1 --- /dev/null +++ b/plugins/clang/duchain/headerguardassistant.cpp @@ -0,0 +1,128 @@ +/* + * Copyright 2018, 2019 Amish K. Naidu + * + * 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 "headerguardassistant.h" +#include "util/clangutils.h" +#include "util/clangtypes.h" + +#include +#include +#include + +#include +#include + +#include + + +enum class GuardType +{ + Pragma, + Macro +}; + +class AddHeaderGuardAction + : public KDevelop::IAssistantAction +{ +public: + AddHeaderGuardAction(const GuardType type, const int startLine, const KDevelop::IndexedString& path) + : m_type(type) + , m_startLine(startLine) + , m_path(path) + { + } + + virtual ~AddHeaderGuardAction() override = default; + + QString description() const override + { + switch (m_type) { + case GuardType::Pragma: return i18n("Add #pragma once"); + case GuardType::Macro: return i18n("Add macro-based #ifndef/#define/#endif heard guard"); + } + return {}; + } + + void execute() override + { + KDevelop::DocumentChangeSet changes; + switch (m_type) { + case GuardType::Pragma: + { + KDevelop::DocumentChange change(m_path, KTextEditor::Range(m_startLine, 0, m_startLine, 0), QString(), + QStringLiteral("#pragma once\n\n")); + changes.addChange(change); + break; + } + case GuardType::Macro: + { + const QString macro = m_path.toUrl() + .fileName(QUrl::PrettyDecoded) + .replace(QRegularExpression(QStringLiteral("[^a-zA-Z0-9]")), QLatin1String(" ")) + .simplified() + .toUpper() + .replace(QLatin1Char(' '), QLatin1Char('_')) + .append(QLatin1String("_INCLUDED")); + + const auto representation = KDevelop::createCodeRepresentation(m_path); + const auto lastLine = representation->lines() - 1; + const auto lastColumn = representation->line(lastLine).length(); + + // Add the #endif change before so that it applies correctly in case lastLine == m_startline + changes.addChange(KDevelop::DocumentChange(m_path, + KTextEditor::Range(lastLine, lastColumn, lastLine, lastColumn), + QString(), + QStringLiteral("\n#endif // %1").arg(macro))); + changes.addChange(KDevelop::DocumentChange(m_path, + KTextEditor::Range(m_startLine, 0, m_startLine, 0), + QString(), + QStringLiteral("#ifndef %1\n#define %1\n\n").arg(macro))); + break; + } + } + + KDevelop::DUChainReadLocker lock; + changes.setReplacementPolicy(KDevelop::DocumentChangeSet::WarnOnFailedChange); + changes.applyAllChanges(); + emit executed(this); + } + +private: + const GuardType m_type; + const int m_startLine; + const KDevelop::IndexedString m_path; +}; + +HeaderGuardAssistant::HeaderGuardAssistant(const CXTranslationUnit unit, const CXFile file) + : m_line(std::max(ClangUtils::skipTopCommentBlock(unit, file), 1u) - 1) // skip license etc + , m_path(QDir(ClangString(clang_getFileName(file)).toString()).canonicalPath()) +{ +} + +QString HeaderGuardAssistant::title() const +{ + return QStringLiteral("Fix-Header"); +} + +void HeaderGuardAssistant::createActions() +{ + addAction(KDevelop::IAssistantAction::Ptr{new AddHeaderGuardAction(GuardType::Pragma, m_line, m_path)}); + addAction(KDevelop::IAssistantAction::Ptr{new AddHeaderGuardAction(GuardType::Macro, m_line, m_path)}); +} diff --git a/plugins/clang/duchain/headerguardassistant.h b/plugins/clang/duchain/headerguardassistant.h new file mode 100644 index 0000000000..a62d2f83c7 --- /dev/null +++ b/plugins/clang/duchain/headerguardassistant.h @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Amish K. Naidu + * + * 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 . + * + */ +#ifndef HEADERGUARDASSISTANT_H +#define HEADERGUARDASSISTANT_H + +#include +#include + +#include + +class HeaderGuardAssistant + : public KDevelop::IAssistant +{ +public: + HeaderGuardAssistant(const CXTranslationUnit unit, const CXFile file); + virtual ~HeaderGuardAssistant() override = default; + + QString title() const override; + + void createActions() override; + +private: + const int m_line; + const KDevelop::IndexedString m_path; +}; + +#endif // HEADERGUARDASSISTANT_H diff --git a/plugins/clang/duchain/parsesession.cpp b/plugins/clang/duchain/parsesession.cpp index 5ef8899ffe..543a0716ee 100644 --- a/plugins/clang/duchain/parsesession.cpp +++ b/plugins/clang/duchain/parsesession.cpp @@ -1,534 +1,537 @@ /* 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 "clangproblem.h" #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() == QStringLiteral("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)) { return {QByteArrayLiteral("-xcuda")}; } 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(); smartArgs << writeDefinesFile(environment.defines()); clangArguments << "-imacros" << smartArgs.last().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"; foreach (const auto& arg, 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 << QStringLiteral("#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; } 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); // 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; } auto& problem = d->m_diagnosticsCache[i]; if (!problem) { problem = ClangDiagnosticEvaluator::createProblem(diagnostic, d->m_unit); } problems << problem; 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))) { - ProblemPointer problem(new Problem); + 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.")); - problem->setFinalLocation({indexedPath, KTextEditor::Range()}); + 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; - // TODO: Easy to add an assistant here that adds the guards -- any takers? } #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_assistants.cpp b/plugins/clang/tests/test_assistants.cpp index a98a379765..71fdbf6f06 100644 --- a/plugins/clang/tests/test_assistants.cpp +++ b/plugins/clang/tests/test_assistants.cpp @@ -1,800 +1,904 @@ /* This file is part of KDevelop Copyright 2012 Olivier de Gaalon 2014 David Stevens 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_assistants.h" #include "codegen/clangrefactoring.h" +#include "codegen/adaptsignatureassistant.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 using namespace KDevelop; using namespace KTextEditor; QTEST_MAIN(TestAssistants) ForegroundLock *globalTestLock = nullptr; StaticAssistantsManager *staticAssistantsManager() { return Core::self()->languageController()->staticAssistantsManager(); } void TestAssistants::initTestCase() { QLoggingCategory::setFilterRules(QStringLiteral( "*.debug=false\n" "default.debug=true\n" "kdevelop.plugins.clang.debug=true\n" )); QVERIFY(qputenv("KDEV_CLANG_DISPLAY_DIAGS", "1")); AutoTestShell::init({QStringLiteral("kdevclangsupport"), QStringLiteral("kdevproblemreporter")}); TestCore::initialize(); DUChain::self()->disablePersistentStorage(); Core::self()->languageController()->backgroundParser()->setDelay(0); Core::self()->sourceFormatterController()->disableSourceFormatting(true); CodeRepresentation::setDiskChangesForbidden(true); globalTestLock = new ForegroundLock; } void TestAssistants::cleanupTestCase() { Core::self()->cleanup(); delete globalTestLock; globalTestLock = nullptr; } static QUrl createFile(const QString& fileContents, const QString& extension, int id) { static QTemporaryDir tempDirA; Q_ASSERT(tempDirA.isValid()); static QDir dirA(tempDirA.path()); QFile file(dirA.filePath(QString::number(id) + extension)); file.open(QIODevice::WriteOnly | QIODevice::Text); file.write(fileContents.toUtf8()); file.close(); return QUrl::fromLocalFile(file.fileName()); } class Testbed { public: enum TestDoc { HeaderDoc, CppDoc }; enum IncludeBehavior { NoAutoInclude, AutoInclude, }; Testbed(const QString& headerContents, const QString& cppContents, IncludeBehavior include = AutoInclude) : m_includeBehavior(include) { static int i = 0; int id = i; ++i; m_headerDocument.url = createFile(headerContents,QStringLiteral(".h"),id); m_headerDocument.textDoc = openDocument(m_headerDocument.url); QString preamble; if (include == AutoInclude) preamble = QStringLiteral("#include \"%1\"\n").arg(m_headerDocument.url.toLocalFile()); m_cppDocument.url = createFile(preamble + cppContents,QStringLiteral(".cpp"),id); m_cppDocument.textDoc = openDocument(m_cppDocument.url); } ~Testbed() { Core::self()->documentController()->documentForUrl(m_cppDocument.url)->textDocument(); Core::self()->documentController()->documentForUrl(m_cppDocument.url)->close(KDevelop::IDocument::Discard); Core::self()->documentController()->documentForUrl(m_headerDocument.url)->close(KDevelop::IDocument::Discard); } void changeDocument(TestDoc which, Range where, const QString& what, bool waitForUpdate = false) { TestDocument document; if (which == CppDoc) { document = m_cppDocument; if (m_includeBehavior == AutoInclude) { where = Range(where.start().line() + 1, where.start().column(), where.end().line() + 1, where.end().column()); //The include adds a line } } else { document = m_headerDocument; } // we must activate the document, otherwise we cannot find the correct active view auto kdevdoc = ICore::self()->documentController()->documentForUrl(document.url); QVERIFY(kdevdoc); ICore::self()->documentController()->activateDocument(kdevdoc); auto view = ICore::self()->documentController()->activeTextDocumentView(); QCOMPARE(view->document(), document.textDoc); view->setSelection(where); view->removeSelectionText(); view->setCursorPosition(where.start()); view->insertText(what); QCoreApplication::processEvents(); if (waitForUpdate) { DUChain::self()->waitForUpdate(IndexedString(document.url), KDevelop::TopDUContext::AllDeclarationsAndContexts); } } QString documentText(TestDoc which) { if (which == CppDoc) { //The CPP document text shouldn't include the autogenerated include line QString text = m_cppDocument.textDoc->text(); return m_includeBehavior == AutoInclude ? text.mid(text.indexOf(QLatin1String("\n")) + 1) : text; } else return m_headerDocument.textDoc->text(); } QString includeFileName() const { return m_headerDocument.url.toLocalFile(); } KTextEditor::Document *document(TestDoc which) const { return Core::self()->documentController()->documentForUrl( which == CppDoc ? m_cppDocument.url : m_headerDocument.url)->textDocument(); } private: struct TestDocument { QUrl url; Document *textDoc; }; Document* openDocument(const QUrl& url) { Core::self()->documentController()->openDocument(url); DUChain::self()->waitForUpdate(IndexedString(url), KDevelop::TopDUContext::AllDeclarationsAndContexts); return Core::self()->documentController()->documentForUrl(url)->textDocument(); } IncludeBehavior m_includeBehavior; TestDocument m_headerDocument; TestDocument m_cppDocument; }; /** * A StateChange describes an insertion/deletion/replacement and the expected result **/ struct StateChange { StateChange(){}; StateChange(Testbed::TestDoc document, const Range& range, const QString& newText, const QString& result) : document(document) , range(range) , newText(newText) , result(result) { } Testbed::TestDoc document; Range range; QString newText; QString result; }; Q_DECLARE_METATYPE(StateChange) Q_DECLARE_METATYPE(QList) void TestAssistants::testRenameAssistant_data() { QTest::addColumn("fileContents"); QTest::addColumn("oldDeclarationName"); QTest::addColumn >("stateChanges"); QTest::addColumn("finalFileContents"); QTest::newRow("Prepend Text") << "int foo(int i)\n { i = 0; return i; }" << "i" << QList{ StateChange(Testbed::CppDoc, Range(0,12,0,12), "u", "ui"), StateChange(Testbed::CppDoc, Range(0,13,0,13), "z", "uzi"), } << "int foo(int uzi)\n { uzi = 0; return uzi; }"; QTest::newRow("Append Text") << "int foo(int i)\n { i = 0; return i; }" << "i" << (QList() << StateChange(Testbed::CppDoc, Range(0,13,0,13), QStringLiteral("d"), QStringLiteral("id"))) << "int foo(int id)\n { id = 0; return id; }"; QTest::newRow("Replace Text") << "int foo(int i)\n { i = 0; return i; }" << "i" << (QList() << StateChange(Testbed::CppDoc, Range(0,12,0,13), QStringLiteral("u"), QStringLiteral("u"))) << "int foo(int u)\n { u = 0; return u; }"; QTest::newRow("Paste Replace") << "int foo(int abg)\n { abg = 0; return abg; }" << "abg" << (QList() << StateChange(Testbed::CppDoc, Range(0,12,0,15), QStringLiteral("abcdefg"), QStringLiteral("abcdefg"))) << "int foo(int abcdefg)\n { abcdefg = 0; return abcdefg; }"; QTest::newRow("Paste Insert") << "int foo(int abg)\n { abg = 0; return abg; }" << "abg" << (QList() << StateChange(Testbed::CppDoc, Range(0,14,0,14), QStringLiteral("cdef"), QStringLiteral("abcdefg"))) << "int foo(int abcdefg)\n { abcdefg = 0; return abcdefg; }"; QTest::newRow("Letter-by-Letter Prepend") << "int foo(int i)\n { i = 0; return i; }" << "i" << (QList() << StateChange(Testbed::CppDoc, Range(0,12,0,12), QStringLiteral("a"), QStringLiteral("ai")) << StateChange(Testbed::CppDoc, Range(0,13,0,13), QStringLiteral("b"), QStringLiteral("abi")) << StateChange(Testbed::CppDoc, Range(0,14,0,14), QStringLiteral("c"), QStringLiteral("abci")) ) << "int foo(int abci)\n { abci = 0; return abci; }"; QTest::newRow("Letter-by-Letter Insert") << "int foo(int abg)\n { abg = 0; return abg; }" << "abg" << (QList() << StateChange(Testbed::CppDoc, Range(0,14,0,14), QStringLiteral("c"), QStringLiteral("abcg")) << StateChange(Testbed::CppDoc, Range(0,15,0,15), QStringLiteral("d"), QStringLiteral("abcdg")) << StateChange(Testbed::CppDoc, Range(0,16,0,16), QStringLiteral("e"), QStringLiteral("abcdeg")) << StateChange(Testbed::CppDoc, Range(0,17,0,17), QStringLiteral("f"), QStringLiteral("abcdefg")) ) << "int foo(int abcdefg)\n { abcdefg = 0; return abcdefg; }"; } ProblemPointer findStaticAssistantProblem(const QVector& problems) { const auto renameProblemIt = std::find_if(problems.cbegin(), problems.cend(), [](const ProblemPointer& p) { return dynamic_cast(p.constData()); }); if (renameProblemIt != problems.cend()) return *renameProblemIt; return {}; } +template +ProblemPointer findProblemWithAssistant(const QVector& problems) +{ + const auto problemIterator = std::find_if(problems.cbegin(), problems.cend(), [](const ProblemPointer& p) { + return dynamic_cast(p->solutionAssistant().constData()); + }); + if (problemIterator != problems.cend()) + return *problemIterator; + + return {}; +} + void TestAssistants::testRenameAssistant() { QFETCH(QString, fileContents); Testbed testbed(QString(), fileContents); const auto document = testbed.document(Testbed::CppDoc); QVERIFY(document); QExplicitlySharedDataPointer assistant; QFETCH(QString, oldDeclarationName); QFETCH(QList, stateChanges); foreach(StateChange stateChange, stateChanges) { testbed.changeDocument(Testbed::CppDoc, stateChange.range, stateChange.newText, true); DUChainReadLocker lock; auto topCtx = DUChain::self()->chainForDocument(document->url()); QVERIFY(topCtx); const auto problem = findStaticAssistantProblem(DUChainUtils::allProblemsForContext(topCtx)); if (problem) assistant = problem->solutionAssistant(); if (stateChange.result.isEmpty()) { QVERIFY(!assistant || !assistant->actions().size()); } else { qWarning() << assistant.data() << stateChange.result; QVERIFY(assistant && assistant->actions().size()); auto *r = qobject_cast(assistant->actions().first().data()); QCOMPARE(r->oldDeclarationName(), oldDeclarationName); QCOMPARE(r->newDeclarationName(), stateChange.result); } } if (assistant && assistant->actions().size()) { assistant->actions().first()->execute(); } QFETCH(QString, finalFileContents); QCOMPARE(testbed.documentText(Testbed::CppDoc), finalFileContents); } void TestAssistants::testRenameAssistantUndoRename() { Testbed testbed(QString(), QStringLiteral("int foo(int i)\n { i = 0; return i; }")); testbed.changeDocument(Testbed::CppDoc, Range(0,13,0,13), QStringLiteral("d"), true); const auto document = testbed.document(Testbed::CppDoc); QVERIFY(document); DUChainReadLocker lock; auto topCtx = DUChain::self()->chainForDocument(document->url()); QVERIFY(topCtx); auto firstProblem = findStaticAssistantProblem(DUChainUtils::allProblemsForContext(topCtx)); QVERIFY(firstProblem); auto assistant = firstProblem->solutionAssistant(); QVERIFY(assistant); QVERIFY(assistant->actions().size() > 0); auto *r = qobject_cast(assistant->actions().first().data()); qWarning() << topCtx->problems() << assistant->actions().first().data() << assistant->actions().size(); QVERIFY(r); // now rename the variable back to its original identifier testbed.changeDocument(Testbed::CppDoc, Range(0,13,0,14), QString()); // there should be no assistant anymore QVERIFY(!assistant || assistant->actions().isEmpty()); } const QString SHOULD_ASSIST = QStringLiteral("SHOULD_ASSIST"); //An assistant will be visible const QString NO_ASSIST = QStringLiteral("NO_ASSIST"); //No assistant visible void TestAssistants::testSignatureAssistant_data() { QTest::addColumn("headerContents"); QTest::addColumn("cppContents"); QTest::addColumn >("stateChanges"); QTest::addColumn("finalHeaderContents"); QTest::addColumn("finalCppContents"); QTest::newRow("change_argument_type") << "class Foo {\nint bar(int a, char* b, int c = 10); \n};" << "int Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }" << (QList() << StateChange(Testbed::HeaderDoc, Range(1,8,1,11), QStringLiteral("char"), SHOULD_ASSIST)) << "class Foo {\nint bar(char a, char* b, int c = 10); \n};" << "int Foo::bar(char a, char* b, int c)\n{ a = c; b = new char; return a + *b; }"; QTest::newRow("prepend_arg_header") << "class Foo { void bar(int i); };" << "void Foo::bar(int i)\n{}" << (QList() << StateChange(Testbed::HeaderDoc, Range(0, 21, 0, 21), QStringLiteral("char c, "), SHOULD_ASSIST)) << "class Foo { void bar(char c, int i); };" << "void Foo::bar(char c, int i)\n{}"; QTest::newRow("prepend_arg_cpp") << "class Foo { void bar(int i); };" << "void Foo::bar(int i)\n{}" << (QList() << StateChange(Testbed::CppDoc, Range(0, 14, 0, 14), QStringLiteral("char c, "), SHOULD_ASSIST)) << "class Foo { void bar(char c, int i); };" << "void Foo::bar(char c, int i)\n{}"; QTest::newRow("change_default_parameter") << "class Foo {\nint bar(int a, char* b, int c = 10); \n};" << "int Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }" << (QList() << StateChange(Testbed::HeaderDoc, Range(1,29,1,34), QString(), NO_ASSIST)) << "class Foo {\nint bar(int a, char* b, int c); \n};" << "int Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }"; QTest::newRow("change_function_type") << "class Foo {\nint bar(int a, char* b, int c = 10); \n};" << "int Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }" << (QList() << StateChange(Testbed::CppDoc, Range(0,0,0,3), QStringLiteral("char"), SHOULD_ASSIST)) << "class Foo {\nchar bar(int a, char* b, int c = 10); \n};" << "char Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }"; QTest::newRow("swap_args_definition_side") << "class Foo {\nint bar(int a, char* b, int c = 10); \n};" << "int Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }" << (QList() << StateChange(Testbed::CppDoc, Range(0,13,0,28), QStringLiteral("char* b, int a,"), SHOULD_ASSIST)) << "class Foo {\nint bar(char* b, int a, int c = 10); \n};" << "int Foo::bar(char* b, int a, int c)\n{ a = c; b = new char; return a + *b; }"; // see https://bugs.kde.org/show_bug.cgi?id=299393 // actually related to the whitespaces in the header... QTest::newRow("change_function_constness") << "class Foo {\nvoid bar(const Foo&) const;\n};" << "void Foo::bar(const Foo&) const\n{}" << (QList() << StateChange(Testbed::CppDoc, Range(0,25,0,31), QString(), SHOULD_ASSIST)) << "class Foo {\nvoid bar(const Foo&);\n};" << "void Foo::bar(const Foo&)\n{}"; // see https://bugs.kde.org/show_bug.cgi?id=356179 QTest::newRow("keep_static_cpp") << "class Foo { static void bar(int i); };" << "void Foo::bar(int i)\n{}" << (QList() << StateChange(Testbed::CppDoc, Range(0, 19, 0, 19), QStringLiteral(", char c"), SHOULD_ASSIST)) << "class Foo { static void bar(int i, char c); };" << "void Foo::bar(int i, char c)\n{}"; QTest::newRow("keep_static_header") << "class Foo { static void bar(int i); };" << "void Foo::bar(int i)\n{}" << (QList() << StateChange(Testbed::HeaderDoc, Range(0, 33, 0, 33), QStringLiteral(", char c"), SHOULD_ASSIST)) << "class Foo { static void bar(int i, char c); };" << "void Foo::bar(int i, char c)\n{}"; // see https://bugs.kde.org/show_bug.cgi?id=356178 QTest::newRow("keep_default_args_cpp_before") << "class Foo { void bar(bool b, int i = 0); };" << "void Foo::bar(bool b, int i)\n{}" << (QList() << StateChange(Testbed::CppDoc, Range(0, 14, 0, 14), QStringLiteral("char c, "), SHOULD_ASSIST)) << "class Foo { void bar(char c, bool b, int i = 0); };" << "void Foo::bar(char c, bool b, int i)\n{}"; QTest::newRow("keep_default_args_cpp_after") << "class Foo { void bar(bool b, int i = 0); };" << "void Foo::bar(bool b, int i)\n{}" << (QList() << StateChange(Testbed::CppDoc, Range(0, 27, 0, 27), QStringLiteral(", char c"), SHOULD_ASSIST)) << "class Foo { void bar(bool b, int i = 0, char c = {} /* TODO */); };" << "void Foo::bar(bool b, int i, char c)\n{}"; QTest::newRow("keep_default_args_header_before") << "class Foo { void bar(bool b, int i = 0); };" << "void Foo::bar(bool b, int i)\n{}" << (QList() << StateChange(Testbed::HeaderDoc, Range(0, 29, 0, 29), QStringLiteral("char c = 'A', "), SHOULD_ASSIST)) << "class Foo { void bar(bool b, char c = 'A', int i = 0); };" << "void Foo::bar(bool b, char c, int i)\n{}"; QTest::newRow("keep_default_args_header_after") << "class Foo { void bar(bool b, int i = 0); };" << "void Foo::bar(bool b, int i)\n{}" << (QList() << StateChange(Testbed::HeaderDoc, Range(0, 38, 0, 38), QStringLiteral(", char c = 'A'"), SHOULD_ASSIST)) << "class Foo { void bar(bool b, int i = 0, char c = 'A'); };" << "void Foo::bar(bool b, int i, char c)\n{}"; // see https://bugs.kde.org/show_bug.cgi?id=355356 QTest::newRow("no_retval_on_ctor") << "class Foo { Foo(); };" << "Foo::Foo()\n{}" << (QList() << StateChange(Testbed::HeaderDoc, Range(0, 16, 0, 16), QStringLiteral("char c"), SHOULD_ASSIST)) << "class Foo { Foo(char c); };" << "Foo::Foo(char c)\n{}"; // see https://bugs.kde.org/show_bug.cgi?id=365420 QTest::newRow("no_retval_on_ctor_while_editing_definition") << "class Foo {\nFoo(int a); \n};" << "Foo::Foo(int a)\n{}" << (QList() << StateChange(Testbed::CppDoc, Range(0,13,0,14), QStringLiteral("b"), SHOULD_ASSIST)) << "class Foo {\nFoo(int b); \n};" << "Foo::Foo(int b)\n{}"; // see https://bugs.kde.org/show_bug.cgi?id=298511 QTest::newRow("change_return_type_header") << "struct Foo { int bar(); };" << "int Foo::bar()\n{}" << (QList() << StateChange(Testbed::HeaderDoc, Range(0, 13, 0, 16), QStringLiteral("char"), SHOULD_ASSIST)) << "struct Foo { char bar(); };" << "char Foo::bar()\n{}"; QTest::newRow("change_return_type_impl") << "struct Foo { int bar(); };" << "int Foo::bar()\n{}" << (QList() << StateChange(Testbed::CppDoc, Range(0, 0, 0, 3), QStringLiteral("char"), SHOULD_ASSIST)) << "struct Foo { char bar(); };" << "char Foo::bar()\n{}"; } void TestAssistants::testSignatureAssistant() { QFETCH(QString, headerContents); QFETCH(QString, cppContents); Testbed testbed(headerContents, cppContents); QExplicitlySharedDataPointer assistant; QFETCH(QList, stateChanges); foreach (StateChange stateChange, stateChanges) { testbed.changeDocument(stateChange.document, stateChange.range, stateChange.newText, true); const auto document = testbed.document(stateChange.document); QVERIFY(document); DUChainReadLocker lock; auto topCtx = DUChain::self()->chainForDocument(document->url()); QVERIFY(topCtx); - const auto problem = findStaticAssistantProblem(DUChainUtils::allProblemsForContext(topCtx)); + const auto problem = findProblemWithAssistant(DUChainUtils::allProblemsForContext(topCtx)); if (problem) { assistant = problem->solutionAssistant(); } if (stateChange.result == SHOULD_ASSIST) { #if CINDEX_VERSION_MINOR < 35 QEXPECT_FAIL("change_function_type", "Clang sees that return type of out-of-line definition differs from that in the declaration and won't parse the code...", Abort); QEXPECT_FAIL("change_return_type_impl", "Clang sees that return type of out-of-line definition differs from that in the declaration and won't include the function's AST and thus we never get updated about the new return type...", Abort); #endif QVERIFY(assistant && !assistant->actions().isEmpty()); } else { QVERIFY(!assistant || assistant->actions().isEmpty()); } } if (assistant && !assistant->actions().isEmpty()) assistant->actions().first()->execute(); QFETCH(QString, finalHeaderContents); QFETCH(QString, finalCppContents); QCOMPARE(testbed.documentText(Testbed::HeaderDoc), finalHeaderContents); QCOMPARE(testbed.documentText(Testbed::CppDoc), finalCppContents); } enum UnknownDeclarationAction { NoUnknownDeclarationAction = 0x0, ForwardDecls = 0x1, MissingInclude = 0x2 }; Q_DECLARE_FLAGS(UnknownDeclarationActions, UnknownDeclarationAction) Q_DECLARE_METATYPE(UnknownDeclarationActions) void TestAssistants::testUnknownDeclarationAssistant_data() { QTest::addColumn("headerContents"); QTest::addColumn("globalText"); QTest::addColumn("functionText"); QTest::addColumn("actions"); QTest::newRow("unincluded_struct") << "struct test{};" << "" << "test" << UnknownDeclarationActions(ForwardDecls | MissingInclude); QTest::newRow("forward_declared_struct") << "struct test{};" << "struct test;" << "test *f; f->" << UnknownDeclarationActions(MissingInclude); QTest::newRow("unknown_struct") << "" << "" << "test" << UnknownDeclarationActions(); QTest::newRow("not a class type") << "void test();" << "" << "test" << UnknownDeclarationActions(); } void TestAssistants::testUnknownDeclarationAssistant() { QFETCH(QString, headerContents); QFETCH(QString, globalText); QFETCH(QString, functionText); QFETCH(UnknownDeclarationActions, actions); static const auto cppContents = QStringLiteral("%1\nvoid f_u_n_c_t_i_o_n() {\n}"); Testbed testbed(headerContents, cppContents.arg(globalText), Testbed::NoAutoInclude); const auto document = testbed.document(Testbed::CppDoc); QVERIFY(document); const int line = document->lines() - 1; testbed.changeDocument(Testbed::CppDoc, Range(line, 0, line, 0), functionText, true); DUChainReadLocker lock; auto topCtx = DUChain::self()->chainForDocument(document->url()); QVERIFY(topCtx); const auto problems = topCtx->problems(); if (actions == NoUnknownDeclarationAction) { QVERIFY(!problems.isEmpty()); return; } auto firstProblem = problems.first(); auto assistant = firstProblem->solutionAssistant(); QVERIFY(assistant); const auto assistantActions = assistant->actions(); QStringList actionDescriptions; for (auto action: assistantActions) { actionDescriptions << action->description(); } { const bool hasForwardDecls = actionDescriptions.contains(i18n("Forward declare as 'struct'")) || actionDescriptions.contains(i18n("Forward declare as 'class'")); QCOMPARE(hasForwardDecls, static_cast(actions & ForwardDecls)); } { auto fileName = testbed.includeFileName(); fileName.remove(0, fileName.lastIndexOf('/') + 1); const auto directive = QStringLiteral("#include \"%1\"").arg(fileName); const auto description = i18n("Insert \'%1\'", directive); const bool hasMissingInclude = actionDescriptions.contains(description); QCOMPARE(hasMissingInclude, static_cast(actions & MissingInclude)); } } void TestAssistants::testMoveIntoSource() { QFETCH(QString, origHeader); QFETCH(QString, origImpl); QFETCH(QString, newHeader); QFETCH(QString, newImpl); QFETCH(QualifiedIdentifier, id); TestFile header(origHeader, QStringLiteral("h")); TestFile impl("#include \"" + header.url().byteArray() + "\"\n" + origImpl, QStringLiteral("cpp"), &header); { TopDUContext* headerCtx = nullptr; { DUChainReadLocker lock; headerCtx = DUChain::self()->chainForDocument(header.url()); } // Here is a problem: when launching tests one by one, we can reuse the same tmp file for headers. // But because of document chain for header wasn't unloaded properly in previous run we reuse it here // Therefore when using headerCtx->findDeclarations below we find declarations from the previous launch -> tests fail if (headerCtx) { // TODO: Investigate why this chain doesn't get updated when parsing source file DUChainWriteLocker lock; DUChain::self()->removeDocumentChain(headerCtx); } } impl.parse(KDevelop::TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(impl.waitForParsed()); IndexedDeclaration declaration; { DUChainReadLocker lock; auto headerCtx = DUChain::self()->chainForDocument(header.url()); QVERIFY(headerCtx); auto decls = headerCtx->findDeclarations(id); Q_ASSERT(!decls.isEmpty()); declaration = IndexedDeclaration(decls.first()); QVERIFY(declaration.isValid()); } CodeRepresentation::setDiskChangesForbidden(false); ClangRefactoring refactoring; QCOMPARE(refactoring.moveIntoSource(declaration), QString()); CodeRepresentation::setDiskChangesForbidden(true); QCOMPARE(header.fileContents(), newHeader); QVERIFY(impl.fileContents().endsWith(newImpl)); } void TestAssistants::testMoveIntoSource_data() { QTest::addColumn("origHeader"); QTest::addColumn("origImpl"); QTest::addColumn("newHeader"); QTest::addColumn("newImpl"); QTest::addColumn("id"); const QualifiedIdentifier fooId(QStringLiteral("foo")); QTest::newRow("globalfunction") << QStringLiteral("int foo()\n{\n int i = 0;\n return 0;\n}\n") << QString() << QStringLiteral("int foo();\n") << QStringLiteral("\nint foo()\n{\n int i = 0;\n return 0;\n}\n") << fooId; QTest::newRow("staticfunction") << QStringLiteral("static int foo()\n{\n int i = 0;\n return 0;\n}\n") << QString() << QStringLiteral("static int foo();\n") << QStringLiteral("\nint foo()\n{\n int i = 0;\n return 0;\n}\n") << fooId; QTest::newRow("funcsameline") << QStringLiteral("int foo() {\n int i = 0;\n return 0;\n}\n") << QString() << QStringLiteral("int foo();\n") << QStringLiteral("\nint foo() {\n int i = 0;\n return 0;\n}\n") << fooId; QTest::newRow("func-comment") << QStringLiteral("int foo()\n/* foobar */ {\n int i = 0;\n return 0;\n}\n") << QString() << QStringLiteral("int foo()\n/* foobar */;\n") << QStringLiteral("\nint foo() {\n int i = 0;\n return 0;\n}\n") << fooId; QTest::newRow("func-comment2") << QStringLiteral("int foo()\n/*asdf*/\n{\n int i = 0;\n return 0;\n}\n") << QString() << QStringLiteral("int foo()\n/*asdf*/;\n") << QStringLiteral("\nint foo()\n{\n int i = 0;\n return 0;\n}\n") << fooId; const QualifiedIdentifier aFooId(QStringLiteral("a::foo")); QTest::newRow("class-method") << QStringLiteral("class a {\n int foo(){\n return 0;\n }\n};\n") << QString() << QStringLiteral("class a {\n int foo();\n};\n") << QStringLiteral("\nint a::foo() {\n return 0;\n }\n") << aFooId; QTest::newRow("class-method-const") << QStringLiteral("class a {\n int foo() const\n {\n return 0;\n }\n};\n") << QString() << QStringLiteral("class a {\n int foo() const;\n};\n") << QStringLiteral("\nint a::foo() const\n {\n return 0;\n }\n") << aFooId; QTest::newRow("class-method-const-sameline") << QStringLiteral("class a {\n int foo() const{\n return 0;\n }\n};\n") << QString() << QStringLiteral("class a {\n int foo() const;\n};\n") << QStringLiteral("\nint a::foo() const {\n return 0;\n }\n") << aFooId; QTest::newRow("elaborated-type") << QStringLiteral("namespace NS{class C{};} class a {\nint foo(const NS::C c) const{\nreturn 0;\n}\n};\n") << QString() << QStringLiteral("namespace NS{class C{};} class a {\nint foo(const NS::C c) const;\n};\n") << QStringLiteral("\nint a::foo(const NS::C c) const {\nreturn 0;\n}\n") << aFooId; QTest::newRow("add-into-namespace") << QStringLiteral("namespace NS{class a {\nint foo() const {\nreturn 0;\n}\n};\n}") << QStringLiteral("namespace NS{\n}") << QStringLiteral("namespace NS{class a {\nint foo() const;\n};\n}") << QStringLiteral("namespace NS{\n\nint a::foo() const {\nreturn 0;\n}\n}") << QualifiedIdentifier(QStringLiteral("NS::a::foo")); QTest::newRow("class-template-parameter") << QStringLiteral(R"( namespace first { template class Test{}; namespace second { template class List; } class MoveIntoSource { public: void f(const second::List>*>& param){} };} )") << QString() << QStringLiteral(R"( namespace first { template class Test{}; namespace second { template class List; } class MoveIntoSource { public: void f(const second::List>*>& param); };} )") << QStringLiteral("namespace first {\nvoid MoveIntoSource::f(const first::second::List< const volatile first::Test< first::second::List< int* > >* >& param) {}}\n\n") << QualifiedIdentifier(QStringLiteral("first::MoveIntoSource::f")); QTest::newRow("move-unexposed-type") << QStringLiteral("namespace std { template class basic_string; \ntypedef basic_string string;}\n void move(std::string i){}") << QString() << QStringLiteral("namespace std { template class basic_string; \ntypedef basic_string string;}\n void move(std::string i);") << QStringLiteral("void move(std::string i) {}\n") << QualifiedIdentifier(QStringLiteral("move")); QTest::newRow("move-constructor") << QStringLiteral("class Class{Class(){}\n};") << QString() << QStringLiteral("class Class{Class();\n};") << QStringLiteral("Class::Class() {}\n") << QualifiedIdentifier(QStringLiteral("Class::Class")); } + +void TestAssistants::testHeaderGuardAssistant() +{ + CodeRepresentation::setDiskChangesForbidden(false); + + QFETCH(QString, filename); + QFETCH(QString, code); + QFETCH(QString, pragmaExpected); + QFETCH(QString, macroExpected); + + TestFile pragmaFile (code, QStringLiteral("h")); + TestFile macroFile (code, QStringLiteral("h"), filename); + + QExplicitlySharedDataPointer pragmaAssistant; + QExplicitlySharedDataPointer macroAssistant; + + pragmaFile.parse(TopDUContext::Empty); + macroFile.parse(TopDUContext::Empty); + QVERIFY(pragmaFile.waitForParsed()); + QVERIFY(macroFile.waitForParsed()); + + DUChainReadLocker lock; + const auto pragmaTopContext = DUChain::self()->chainForDocument(pragmaFile.url()); + const auto macroTopContext = DUChain::self()->chainForDocument(macroFile.url()); + QVERIFY(pragmaTopContext); + QVERIFY(macroTopContext); + + const auto pragmaProblem = findStaticAssistantProblem(DUChainUtils::allProblemsForContext(pragmaTopContext)); + const auto macroProblem = findStaticAssistantProblem(DUChainUtils::allProblemsForContext(macroTopContext)); + QVERIFY(pragmaProblem && macroProblem); + pragmaAssistant = pragmaProblem->solutionAssistant(); + macroAssistant = macroProblem->solutionAssistant(); + QVERIFY(pragmaAssistant && macroAssistant); + + pragmaAssistant->actions()[0]->execute(); + macroAssistant->actions()[1]->execute(); + + QCOMPARE(pragmaFile.fileContents(), pragmaExpected); + QCOMPARE(macroFile.fileContents(), macroExpected); + + CodeRepresentation::setDiskChangesForbidden(true); +} + +void TestAssistants::testHeaderGuardAssistant_data() +{ + QTest::addColumn("filename"); + QTest::addColumn("code"); + QTest::addColumn("pragmaExpected"); + QTest::addColumn("macroExpected"); + + QTest::newRow("simple") << QStringLiteral("simpleheaderguard") + << QStringLiteral("int main()\n{\nreturn 0;\n}\n") + << QStringLiteral("#pragma once\n\nint main()\n{\nreturn 0;\n}\n") + << QStringLiteral( + "#ifndef SIMPLEHEADERGUARD_H_INCLUDED\n" + "#define SIMPLEHEADERGUARD_H_INCLUDED\n\n" + "int main()\n{\nreturn 0;\n}\n\n" + "#endif // SIMPLEHEADERGUARD_H_INCLUDED" + ); + + QTest::newRow("licensed") << QStringLiteral("licensed-headerguard") + << QStringLiteral("/* Copyright 3019 John Doe\n */\n// Some comment\n" + "int main()\n{\nreturn 0;\n}\n") + << QStringLiteral("/* Copyright 3019 John Doe\n */\n// Some comment\n" + "#pragma once\n\n" + "int main()\n{\nreturn 0;\n}\n") + << QStringLiteral( + "/* Copyright 3019 John Doe\n */\n// Some comment\n" + "#ifndef LICENSED_HEADERGUARD_H_INCLUDED\n" + "#define LICENSED_HEADERGUARD_H_INCLUDED\n\n" + "int main()\n{\nreturn 0;\n}\n\n" + "#endif // LICENSED_HEADERGUARD_H_INCLUDED" + ); + + QTest::newRow("empty") << QStringLiteral("empty-file") + << QStringLiteral("") + << QStringLiteral("#pragma once\n\n") + << QStringLiteral("#ifndef EMPTY_FILE_H_INCLUDED\n" + "#define EMPTY_FILE_H_INCLUDED\n\n\n" + "#endif // EMPTY_FILE_H_INCLUDED" + ); + + QTest::newRow("no-trailinig-newline") << QStringLiteral("no-endline-file") + << QStringLiteral("int foo;") + << QStringLiteral("#pragma once\n\nint foo;") + << QStringLiteral("#ifndef NO_ENDLINE_FILE_H_INCLUDED\n" + "#define NO_ENDLINE_FILE_H_INCLUDED\n\n" + "int foo;\n" + "#endif // NO_ENDLINE_FILE_H_INCLUDED" + ); +} diff --git a/plugins/clang/tests/test_assistants.h b/plugins/clang/tests/test_assistants.h index 1235de5502..fb9fccc00f 100644 --- a/plugins/clang/tests/test_assistants.h +++ b/plugins/clang/tests/test_assistants.h @@ -1,44 +1,47 @@ /* This file is part of KDevelop Copyright 2012 Olivier de Gaalon 2014 David Stevens 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. */ #ifndef TESTASSISTANTS_H #define TESTASSISTANTS_H #include class TestAssistants : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void testRenameAssistant_data(); void testRenameAssistant(); void testRenameAssistantUndoRename(); void testSignatureAssistant_data(); void testSignatureAssistant(); void testUnknownDeclarationAssistant_data(); void testUnknownDeclarationAssistant(); void testMoveIntoSource_data(); void testMoveIntoSource(); + + void testHeaderGuardAssistant(); + void testHeaderGuardAssistant_data(); }; #endif diff --git a/plugins/clang/util/clangutils.cpp b/plugins/clang/util/clangutils.cpp index 5b175c270c..0c09bb68bd 100644 --- a/plugins/clang/util/clangutils.cpp +++ b/plugins/clang/util/clangutils.cpp @@ -1,460 +1,475 @@ /* * Copyright 2014 Kevin Funk * * 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 "clangutils.h" #include "../util/clangdebug.h" #include "../util/clangtypes.h" #include "../duchain/cursorkindtraits.h" #include "../duchain/documentfinderhelpers.h" #include #include #include #include #include #include +#include +#include + using namespace KDevelop; CXCursor ClangUtils::getCXCursor(int line, int column, const CXTranslationUnit& unit, const CXFile& file) { if (!file) { clangDebug() << "getCXCursor couldn't find file: " << clang_getFileName(file); return clang_getNullCursor(); } CXSourceLocation location = clang_getLocation(unit, file, line + 1, column + 1); if (clang_equalLocations(clang_getNullLocation(), location)) { clangDebug() << "getCXCursor given invalid position " << line << ", " << column << " for file " << clang_getFileName(file); return clang_getNullCursor(); } return clang_getCursor(unit, location); } QVector ClangUtils::unsavedFiles() { QVector ret; foreach(auto document, ICore::self()->documentController()->openDocuments()) { auto textDocument = document->textDocument(); // TODO: Introduce a cache so we don't have to re-read all the open documents // which were not changed since the last run if (!textDocument || !textDocument->url().isLocalFile() || !DocumentFinderHelpers::mimeTypesList().contains(textDocument->mimeType())) { continue; } if (!textDocument->isModified()) { continue; } ret << UnsavedFile(textDocument->url().toLocalFile(), textDocument->textLines(textDocument->documentRange())); } return ret; } KTextEditor::Range ClangUtils::rangeForIncludePathSpec(const QString& line, const KTextEditor::Range& originalRange) { static const QRegularExpression pattern(QStringLiteral("^\\s*(#\\s*include|#\\s*import)")); if (!line.contains(pattern)) { return KTextEditor::Range::invalid(); } KTextEditor::Range range = originalRange; int pos = 0; char term_char = 0; for (; pos < line.size(); ++pos) { if (line[pos] == QLatin1Char('"') || line[pos] == QLatin1Char('<')) { term_char = line[pos] == QLatin1Char('"') ? '"' : '>'; range.setStart({ range.start().line(), ++pos }); break; } } for (; pos < line.size(); ++pos) { if (line[pos] == QLatin1Char('\\')) { ++pos; continue; } else if(line[pos] == QLatin1Char(term_char)) { range.setEnd({ range.start().line(), pos }); break; } } return range; } namespace { struct FunctionInfo { KTextEditor::Range range; QString fileName; CXTranslationUnit unit; QStringList stringParts; }; CXChildVisitResult paramVisitor(CXCursor cursor, CXCursor /*parent*/, CXClientData data) { //Ignore the type of the parameter CXCursorKind kind = clang_getCursorKind(cursor); if (kind == CXCursor_TypeRef || kind == CXCursor_TemplateRef || kind == CXCursor_NamespaceRef) { return CXChildVisit_Continue; } auto *info = static_cast(data); ClangRange range(clang_getCursorExtent(cursor)); CXFile file; clang_getFileLocation(clang_getCursorLocation(cursor),&file,nullptr,nullptr,nullptr); if (!file) { clangDebug() << "Couldn't find file associated with default parameter cursor!"; //We keep going, because getting an error because we accidentally duplicated //a default parameter is better than deleting a default parameter } QString fileName = ClangString(clang_getFileName(file)).toString(); //Clang doesn't make a distinction between the default arguments being in //the declaration or definition, and the default arguments don't have lexical //parents. So this range check is the only thing that really works. if ((info->fileName.isEmpty() || fileName == info->fileName) && info->range.contains(range.toRange())) { const ClangTokens tokens(info->unit, range.range()); info->stringParts.reserve(info->stringParts.size() + tokens.size()); for (CXToken token : tokens) { info->stringParts.append(ClangString(clang_getTokenSpelling(info->unit, token)).toString()); } } return CXChildVisit_Continue; } } QVector ClangUtils::getDefaultArguments(CXCursor cursor, DefaultArgumentsMode mode) { if (!CursorKindTraits::isFunction(clang_getCursorKind(cursor))) { return QVector(); } int numArgs = clang_Cursor_getNumArguments(cursor); QVector arguments(mode == FixedSize ? numArgs : 0); QString fileName; CXFile file; clang_getFileLocation(clang_getCursorLocation(cursor),&file,nullptr,nullptr,nullptr); if (!file) { clangDebug() << "Couldn't find file associated with default parameter cursor!"; //The empty string serves as a wildcard string, because it's better to //duplicate a default parameter than delete one } else { fileName = ClangString(clang_getFileName(file)).toString(); } FunctionInfo info{ClangRange(clang_getCursorExtent(cursor)).toRange(), fileName, clang_Cursor_getTranslationUnit(cursor), QStringList()}; for (int i = 0; i < numArgs; i++) { CXCursor arg = clang_Cursor_getArgument(cursor, i); info.stringParts.clear(); clang_visitChildren(arg, paramVisitor, &info); //Clang includes the equal sign sometimes, but not other times. if (!info.stringParts.isEmpty() && info.stringParts.first() == QLatin1String("=")) { info.stringParts.removeFirst(); } //Clang seems to include the , or ) at the end of the param, so delete that if (!info.stringParts.isEmpty() && (info.stringParts.last() == QLatin1String(",") || info.stringParts.last() == QLatin1String(")"))) { info.stringParts.removeLast(); } const QString result = info.stringParts.join(QString()); if (mode == FixedSize) { arguments.replace(i, result); } else if (!result.isEmpty()) { arguments << result; } } return arguments; } bool ClangUtils::isScopeKind(CXCursorKind kind) { return kind == CXCursor_Namespace || kind == CXCursor_StructDecl || kind == CXCursor_UnionDecl || kind == CXCursor_ClassDecl || kind == CXCursor_ClassTemplate || kind == CXCursor_ClassTemplatePartialSpecialization; } QString ClangUtils::getScope(CXCursor cursor, CXCursor context) { QStringList scope; if (clang_Cursor_isNull(context)) { context = clang_getCursorLexicalParent(cursor); } context = clang_getCanonicalCursor(context); CXCursor search = clang_getCursorSemanticParent(cursor); while (isScopeKind(clang_getCursorKind(search)) && !clang_equalCursors(search, context)) { scope.prepend(ClangString(clang_getCursorDisplayName(search)).toString()); search = clang_getCursorSemanticParent(search); } return scope.join(QStringLiteral("::")); } QString ClangUtils::getCursorSignature(CXCursor cursor, const QString& scope, const QVector& defaultArgs) { CXCursorKind kind = clang_getCursorKind(cursor); //Get the return type QString ret; ret.reserve(128); QTextStream stream(&ret); if (kind != CXCursor_Constructor && kind != CXCursor_Destructor) { stream << ClangString(clang_getTypeSpelling(clang_getCursorResultType(cursor))).toString() << ' '; } //Build the function name, with scope and parameters if (!scope.isEmpty()) { stream << scope << "::"; } QString functionName = ClangString(clang_getCursorSpelling(cursor)).toString(); if (functionName.contains(QLatin1Char('<'))) { stream << functionName.left(functionName.indexOf(QLatin1Char('<'))); } else { stream << functionName; } //Add the parameters and such stream << '('; int numArgs ; QVector args; // SEE https://bugs.kde.org/show_bug.cgi?id=368544 // clang_Cursor_getNumArguments returns -1 for FunctionTemplate // clang checks if cursor's Decl is ObjCMethodDecl or FunctionDecl // CXCursor_FunctionTemplate is neither of them instead it has a FunctionTemplateDecl // HACK Get function template arguments by visiting children if (kind == CXCursor_FunctionTemplate) { clang_visitChildren(cursor, [] (CXCursor cursor, CXCursor /*parent*/, CXClientData data) { if (clang_getCursorKind(cursor) == CXCursor_ParmDecl) { (static_cast*>(data))->push_back(cursor); } return CXChildVisit_Continue; }, &args); numArgs = args.size(); } else { numArgs = clang_Cursor_getNumArguments(cursor); args.reserve(numArgs); for (int i = 0; i < numArgs; i++) { CXCursor arg = clang_Cursor_getArgument(cursor, i); args.push_back(arg); } } for (int i = 0; i < numArgs; i++) { CXCursor arg = args[i]; //Clang formats pointer types as "t *x" and reference types as "t &x", while //KDevelop formats them as "t* x" and "t& x". Make that adjustment. const QString type = ClangString(clang_getTypeSpelling(clang_getCursorType(arg))).toString(); if (type.endsWith(QLatin1String(" *")) || type.endsWith(QLatin1String(" &"))) { stream << type.left(type.length() - 2) << type.at(type.length() - 1); } else { stream << type; } const QString id = ClangString(clang_getCursorDisplayName(arg)).toString(); if (!id.isEmpty()) { stream << ' ' << id; } if (i < defaultArgs.count() && !defaultArgs.at(i).isEmpty()) { stream << " = " << defaultArgs.at(i); } if (i < numArgs - 1) { stream << ", "; } } if (clang_Cursor_isVariadic(cursor)) { if (numArgs > 0) { stream << ", "; } stream << "..."; } stream << ')'; if (clang_CXXMethod_isConst(cursor)) { stream << " const"; } return ret; } QStringList ClangUtils::templateArgumentTypes(CXCursor cursor) { CXType typeList = clang_getCursorType(cursor); int templateArgCount = clang_Type_getNumTemplateArguments(typeList); QStringList types; types.reserve(templateArgCount); for (int i = 0; i < templateArgCount; ++i) { ClangString clangString(clang_getTypeSpelling(clang_Type_getTemplateArgumentAsType(typeList, i))); types.append(clangString.toString()); } return types; } QByteArray ClangUtils::getRawContents(CXTranslationUnit unit, CXSourceRange range) { const auto rangeStart = clang_getRangeStart(range); const auto rangeEnd = clang_getRangeEnd(range); unsigned int start, end; clang_getFileLocation(rangeStart, nullptr, nullptr, nullptr, &start); clang_getFileLocation(rangeEnd, nullptr, nullptr, nullptr, &end); QByteArray result; const ClangTokens tokens(unit, range); for (CXToken token : tokens) { const auto location = ClangLocation(clang_getTokenLocation(unit, token)); unsigned int offset; clang_getFileLocation(location, nullptr, nullptr, nullptr, &offset); if (offset < start) // TODO: Sometimes hit, see bug 357585 return {}; const int fillCharacters = offset - start - result.size(); Q_ASSERT(fillCharacters >= 0); if (fillCharacters < 0) return {}; result.append(QByteArray(fillCharacters, ' ')); const auto spelling = clang_getTokenSpelling(unit, token); result.append(clang_getCString(spelling)); clang_disposeString(spelling); } // Clang always appends the full range of the last token, even if this exceeds the end of the requested range. // Fix this. result.chop(result.size() - (end - start)); return result; } bool ClangUtils::isExplicitlyDefaultedOrDeleted(CXCursor cursor) { if (clang_getCursorAvailability(cursor) == CXAvailability_NotAvailable) { return true; } #if CINDEX_VERSION_MINOR >= 34 if (clang_CXXMethod_isDefaulted(cursor)) { return true; } #else auto declCursor = clang_getCanonicalCursor(cursor); CXTranslationUnit tu = clang_Cursor_getTranslationUnit(declCursor); ClangTokens tokens(tu, clang_getCursorExtent(declCursor)); bool lastTokenWasDeleteOrDefault = false; for (auto it = tokens.rbegin(), end = tokens.rend(); it != end; ++it) { CXToken token = *it; auto kind = clang_getTokenKind(token); switch (kind) { case CXToken_Comment: break; case CXToken_Identifier: case CXToken_Literal: lastTokenWasDeleteOrDefault = false; break; case CXToken_Punctuation: { ClangString spelling(clang_getTokenSpelling(tu, token)); const char* spellingCStr = spelling.c_str(); if (strcmp(spellingCStr, ")") == 0) { // a closing parent means we have reached the end of the function parameter list // therefore this function can't be explicitly deleted/defaulted return false; } else if (strcmp(spellingCStr, "=") == 0) { if (lastTokenWasDeleteOrDefault) { return true; } #if CINDEX_VERSION_MINOR < 31 // HACK: on old clang versions, we don't get the default/delete // so there, assume the function is defaulted or deleted // when the last token is an equal sign if (it == tokens.rbegin()) { return true; } #endif } lastTokenWasDeleteOrDefault = false; break; } case CXToken_Keyword: { ClangString spelling(clang_getTokenSpelling(tu, token)); const char* spellingCStr = spelling.c_str(); if (strcmp(spellingCStr, "default") == 0 #if CINDEX_VERSION_MINOR < 31 || strcmp(spellingCStr, "delete") == 0 #endif ) { lastTokenWasDeleteOrDefault = true; } else { lastTokenWasDeleteOrDefault = false; } break; } } } #endif return false; } KDevelop::ClassFunctionFlags ClangUtils::specialAttributes(CXCursor cursor) { // check for our injected attributes to detect Qt signals and slots // see also the contents of wrappedQtHeaders/QtCore/qobjectdefs.h ClassFunctionFlags flags = {}; if (cursor.kind == CXCursor_CXXMethod) { clang_visitChildren(cursor, [] (CXCursor cursor, CXCursor /*parent*/, CXClientData data) -> CXChildVisitResult { auto& flags = *static_cast(data); switch (cursor.kind) { case CXCursor_AnnotateAttr: { ClangString attribute(clang_getCursorDisplayName(cursor)); if (attribute.c_str() == QByteArrayLiteral("qt_signal")) { flags |= FunctionSignalFlag; } else if (attribute.c_str() == QByteArrayLiteral("qt_slot")) { flags |= FunctionSlotFlag; } break; } case CXCursor_CXXFinalAttr: flags |= FinalFunctionFlag; break; default: break; } return CXChildVisit_Break; }, &flags); } return flags; } + +unsigned int ClangUtils::skipTopCommentBlock(CXTranslationUnit unit, CXFile file) +{ + const auto fileRange = clang_getRange(clang_getLocation(unit, file, 1, 1), + clang_getLocation(unit, file, std::numeric_limits::max(), 1)); + const ClangTokens tokens (unit, fileRange); + const auto nonCommentToken = std::find_if(tokens.begin(), tokens.end(), + [&](CXToken token) { return clang_getTokenKind(token) != CXToken_Comment; }); + + const auto location = (nonCommentToken != tokens.end()) ? clang_getTokenExtent(unit, *nonCommentToken) : fileRange; + return KTextEditor::Cursor(ClangRange(location).end()).line() + 1; +} diff --git a/plugins/clang/util/clangutils.h b/plugins/clang/util/clangutils.h index f449cd761e..72cf95d493 100644 --- a/plugins/clang/util/clangutils.h +++ b/plugins/clang/util/clangutils.h @@ -1,172 +1,177 @@ /* * Copyright 2014 Kevin Funk * * 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 . * */ #ifndef CLANGUTILS_H #define CLANGUTILS_H #include #include #include "clangprivateexport.h" #include "../duchain/unsavedfile.h" #include namespace ClangUtils { /** * Finds the most specific CXCursor which applies to the specified line and column * in the given translation unit and file. * * @param line The 0-indexed line number at which to search. * @param column The 0-indexed column number at which to search. * @param unit The translation unit to examine. * @param file The file in the translation unit to examine. * * @return The cursor at the specified location */ CXCursor getCXCursor(int line, int column, const CXTranslationUnit& unit, const CXFile& file); enum DefaultArgumentsMode { FixedSize, ///< The vector will have length equal to the number of arguments to the function /// and any arguments without a default parameter will be represented with an empty string. MinimumSize ///< The vector will have a length equal to the number of default values }; /** * Given a cursor representing a function, returns a vector containing the string * representations of the default arguments of the function which are defined at * the occurrence of the cursor. Note that this is not necessarily all of the default * arguments of the function. * * @param cursor The cursor to examine * @return a vector of QStrings representing the default arguments, or an empty * vector if cursor does not represent a function */ QVector getDefaultArguments(CXCursor cursor, DefaultArgumentsMode mode = FixedSize); /** * @return true when the cursor kind references a named scope. */ bool isScopeKind(CXCursorKind kind); /** * @brief Retrieve a list of all unsaved files. * * @note Since this reads text from the editor widget, it must be called from the * GUI thread or with the foreground lock held. * * @return vector of all unsaved files and their current contents */ KDEVCLANGPRIVATE_EXPORT QVector unsavedFiles(); /** * Given a cursor and destination context, returns the string representing the * cursor's scope at its current location. * * @param cursor The cursor to examine * @param context The destination context from which the cursor should be referenced. * By default this will be set to the cursors lexical parent. * @return the cursor's scope as a string */ KDEVCLANGPRIVATE_EXPORT QString getScope(CXCursor cursor, CXCursor context = clang_getNullCursor()); /** * Given a cursor representing some sort of function, returns its signature. The * effect of this function when passed a non-function cursor is undefined. * * @param cursor The cursor to work with * @param scope The scope of the cursor (e.g. "SomeNS::SomeClass") * @return A QString of the function's signature */ QString getCursorSignature(CXCursor cursor, const QString& scope, const QVector& defaultArgs = QVector()); /** * Given a cursor representing the template argument list, return a * list of the argument types. * * @param cursor The cursor to work with * @return A QStringList of the template's arguments */ KDEVCLANGPRIVATE_EXPORT QStringList templateArgumentTypes(CXCursor cursor); /** * Extract the raw contents of the range @p range * * @note This will return the exact textual representation of the code, * no whitespace stripped, etc. * * TODO: It would better if we'd be able to just memcpy parts of the file buffer * that's stored inside Clang (cf. llvm::MemoryBuffer for files), but libclang * doesn't offer API for that. This implementation here is a lot more expensive. * * @param unit Translation unit this range is part of */ KDEVCLANGPRIVATE_EXPORT QByteArray getRawContents(CXTranslationUnit unit, CXSourceRange range); /** * @brief Return true if file @p file1 and file @p file2 are equal * * @see clang_File_isEqual */ inline bool isFileEqual(CXFile file1, CXFile file2) { #if CINDEX_VERSION_MINOR >= 28 return clang_File_isEqual(file1, file2); #else // note: according to the implementation of clang_File_isEqual, file1 and file2 can still be equal, // regardless of whether file1 == file2 is true or not // however, we didn't see any problems with pure pointer comparisons until now, so fall back to that return file1 == file2; #endif } /** * @brief Return true if the cursor @p cursor refers to an explicitly deleted/defaulted function * such as the default constructor in "struct Foo { Foo() = delete; }" * * TODO: do we need isExplicitlyDefaulted() + isExplicitlyDeleted()? * Currently this is only used by the implements completion to hide deleted+defaulted functions so * we don't need to know the difference. We need to tokenize the source code because there is no * such API in libclang so having one function to check both cases is more efficient (only tokenize once) */ bool isExplicitlyDefaultedOrDeleted(CXCursor cursor); /** * Extract the range of the path-spec inside the include-directive in line @p line * * Example: line = "#include " => returns {0, 10, 0, 16} * * @param originalRange This is the range that the resulting range will be based on * * @return Range pointing to the path-spec of the include or invalid range if there is no #include directive on the line. */ KDEVCLANGPRIVATE_EXPORT KTextEditor::Range rangeForIncludePathSpec(const QString& line, const KTextEditor::Range& originalRange = KTextEditor::Range()); /** * Returns special attributes (isFinal, isQtSlot, ...) given a @p cursor representing a CXXmethod */ KDevelop::ClassFunctionFlags specialAttributes(CXCursor cursor); + + /** + * @return the top most line in a file skipping any comment block + */ + unsigned int skipTopCommentBlock(CXTranslationUnit unit, CXFile file); } #endif // CLANGUTILS_H