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,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/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,18 @@ 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); @@ -528,7 +541,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 +811,94 @@ << 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/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,15 @@ } 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; +}