diff --git a/kdevplatform/tests/testfile.h b/kdevplatform/tests/testfile.h --- a/kdevplatform/tests/testfile.h +++ b/kdevplatform/tests/testfile.h @@ -86,6 +86,23 @@ */ 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. */ diff --git a/kdevplatform/tests/testfile.cpp b/kdevplatform/tests/testfile.cpp --- a/kdevplatform/tests/testfile.cpp +++ b/kdevplatform/tests/testfile.cpp @@ -110,6 +110,17 @@ 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) { diff --git a/plugins/clang/CMakeLists.txt b/plugins/clang/CMakeLists.txt --- a/plugins/clang/CMakeLists.txt +++ b/plugins/clang/CMakeLists.txt @@ -72,6 +72,7 @@ duchain/types/classspecializationtype.cpp duchain/unknowndeclarationproblem.cpp duchain/unsavedfile.cpp + duchain/headerguardassistant.cpp util/clangdebug.cpp util/clangtypes.cpp diff --git a/plugins/clang/duchain/headerguardassistant.h b/plugins/clang/duchain/headerguardassistant.h new file mode 100644 --- /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/headerguardassistant.cpp b/plugins/clang/duchain/headerguardassistant.cpp new file mode 100644 --- /dev/null +++ b/plugins/clang/duchain/headerguardassistant.cpp @@ -0,0 +1,122 @@ +/* + * 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 . + * + */ +#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")); + + 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))); + + const auto representation = KDevelop::createCodeRepresentation(m_path); + const int lastLine = representation->lines() - 1; + changes.addChange(KDevelop::DocumentChange(m_path, KTextEditor::Range(lastLine, 0, lastLine, 0), QString(), + QStringLiteral("#endif // %1").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/parsesession.cpp b/plugins/clang/duchain/parsesession.cpp --- a/plugins/clang/duchain/parsesession.cpp +++ b/plugins/clang/duchain/parsesession.cpp @@ -32,9 +32,11 @@ #include "util/clangdebug.h" #include "util/clangtypes.h" #include "util/clangutils.h" +#include "headerguardassistant.h" #include #include +#include #include @@ -144,7 +146,7 @@ if (url.isEmpty()) { continue; } - + QFileInfo info(url.toLocalFile()); QByteArray path = url.toLocalFile().toUtf8(); @@ -475,15 +477,16 @@ 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 diff --git a/plugins/clang/tests/test_assistants.h b/plugins/clang/tests/test_assistants.h --- a/plugins/clang/tests/test_assistants.h +++ b/plugins/clang/tests/test_assistants.h @@ -39,6 +39,9 @@ void testMoveIntoSource_data(); void testMoveIntoSource(); + + void testHeaderGuardAssistant(); + void testHeaderGuardAssistant_data(); }; #endif diff --git a/plugins/clang/tests/test_assistants.cpp b/plugins/clang/tests/test_assistants.cpp --- a/plugins/clang/tests/test_assistants.cpp +++ b/plugins/clang/tests/test_assistants.cpp @@ -20,6 +20,7 @@ #include "test_assistants.h" #include "codegen/clangrefactoring.h" +#include "codegen/adaptsignatureassistant.h" #include #include @@ -303,6 +304,19 @@ return {}; } +template +ProblemPointer findProblemWithAssistant(const QVector& problems) +{ + const auto problemIterator = std::find_if(problems.cbegin(), problems.cend(), [](const ProblemPointer& p) { + const auto problem = dynamic_cast(p.constData()); + return problem && dynamic_cast(problem->solutionAssistant().constData()); + }); + if (problemIterator != problems.cend()) + return *problemIterator; + + return {}; +} + void TestAssistants::testRenameAssistant() { QFETCH(QString, fileContents); @@ -528,7 +542,7 @@ 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(); } @@ -798,3 +812,77 @@ << 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" + "#endif // SIMPLEHEADERGUARD_H_INCLUDED" + ); + + QTest::newRow("licensed") << QStringLiteral("licensed-headerguard") + << QStringLiteral("/* Copyright 3018 John Doe\n *This software may delete everything*/\n" + "int main()\n{\nreturn 0;\n}\n") + << QStringLiteral("/* Copyright 3018 John Doe\n *This software may delete everything*/\n" + "#pragma once\n\n" + "int main()\n{\nreturn 0;\n}\n") + << QStringLiteral( + "/* Copyright 3018 John Doe\n *This software may delete everything*/\n" + "#ifndef LICENSED_HEADERGUARD_H_INCLUDED\n" + "#define LICENSED_HEADERGUARD_H_INCLUDED\n\n" + "int main()\n{\nreturn 0;\n}\n" + "#endif // LICENSED_HEADERGUARD_H_INCLUDED" + ); +} diff --git a/plugins/clang/util/clangutils.h b/plugins/clang/util/clangutils.h --- a/plugins/clang/util/clangutils.h +++ b/plugins/clang/util/clangutils.h @@ -167,6 +167,11 @@ * 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 diff --git a/plugins/clang/util/clangutils.cpp b/plugins/clang/util/clangutils.cpp --- a/plugins/clang/util/clangutils.cpp +++ b/plugins/clang/util/clangutils.cpp @@ -35,6 +35,9 @@ #include #include +#include +#include + using namespace KDevelop; CXCursor ClangUtils::getCXCursor(int line, int column, const CXTranslationUnit& unit, const CXFile& file) @@ -458,3 +461,21 @@ } return flags; } + +unsigned int ClangUtils::skipTopCommentBlock(CXTranslationUnit unit, CXFile file) +{ + auto nextTokenLocation = clang_getLocation(unit, file, 1, 1); + std::unique_ptr> token ( + clang_getToken(unit, nextTokenLocation), + [&](CXToken* token) { clang_disposeTokens(unit, token, 1); } + ); + + while (token && clang_getTokenKind(*token) == CXToken_Comment){ + token.reset(clang_getToken(unit, nextTokenLocation)); + nextTokenLocation = ClangRange(clang_getTokenExtent(unit, *token)).end(); + } + + unsigned int line; + clang_getExpansionLocation(nextTokenLocation, &file, &line, nullptr, nullptr); + return line; +}