File indexing completed on 2024-05-12 04:39:18

0001 /*
0002     SPDX-FileCopyrightText: Kevin Funk <kfunk@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "test_problems.h"
0008 
0009 #include "../duchain/clangindex.h"
0010 #include "../duchain/clangproblem.h"
0011 #include "../duchain/parsesession.h"
0012 #include "../duchain/unknowndeclarationproblem.h"
0013 #include "../duchain/clanghelpers.h"
0014 #include "../util/clangtypes.h"
0015 
0016 #include <language/duchain/duchain.h>
0017 #include <language/duchain/duchainutils.h>
0018 #include <language/duchain/duchainlock.h>
0019 #include <language/codegen/coderepresentation.h>
0020 #include <language/backgroundparser/backgroundparser.h>
0021 
0022 #include <tests/autotestshell.h>
0023 #include <tests/testcore.h>
0024 #include <tests/testfile.h>
0025 #include <tests/testhelpers.h>
0026 #include <tests/testproject.h>
0027 #include <interfaces/ilanguagecontroller.h>
0028 
0029 #include <QTest>
0030 #include <QLoggingCategory>
0031 #include <QVersionNumber>
0032 
0033 Q_DECLARE_METATYPE(KDevelop::IProblem::Severity)
0034 
0035 using namespace KDevelop;
0036 
0037 namespace {
0038 
0039 const QString FileName = 
0040 #ifdef Q_OS_WIN
0041     QStringLiteral("C:/tmp/stdin.cpp");
0042 #else
0043     QStringLiteral("/tmp/stdin.cpp");
0044 #endif
0045 
0046 QList<ProblemPointer> parse(const QByteArray& code)
0047 {
0048     ClangIndex index;
0049     ClangParsingEnvironment environment;
0050     environment.setTranslationUnitUrl(IndexedString(FileName));
0051     ParseSession session(ParseSessionData::Ptr(new ParseSessionData({UnsavedFile(FileName, {code})},
0052                                                                     &index, environment)));
0053     return session.problemsForFile(session.mainFile());
0054 }
0055 
0056 void compareFixitWithoutDescription(const ClangFixit& a, const ClangFixit& b)
0057 {
0058     QCOMPARE(a.replacementText, b.replacementText);
0059     QCOMPARE(a.range, b.range);
0060     QCOMPARE(a.currentText, b.currentText);
0061 }
0062 
0063 void compareFixitsWithoutDescription(const ClangFixits& a, const ClangFixits& b)
0064 {
0065     if (a.size() != b.size()) {
0066         qDebug() << "a:" << a;
0067         qDebug() << "b:" << b;
0068     }
0069     QCOMPARE(a.size(), b.size());
0070     const int size = a.size();
0071     for (int i = 0; i < size; ++i) {
0072         compareFixitWithoutDescription(a.at(i), b.at(i));
0073     }
0074 }
0075 
0076 }
0077 
0078 QTEST_GUILESS_MAIN(TestProblems)
0079 
0080 void TestProblems::initTestCase()
0081 {
0082     QLoggingCategory::setFilterRules(QStringLiteral("*.debug=false\ndefault.debug=true\nkdevelop.plugins.clang.debug=true\n"));
0083     QVERIFY(qputenv("KDEV_CLANG_DISPLAY_DIAGS", "1"));
0084     AutoTestShell::init({"kdevclangsupport"});
0085     TestCore::initialize(Core::NoUi);
0086     DUChain::self()->disablePersistentStorage();
0087     Core::self()->languageController()->backgroundParser()->setDelay(0);
0088     CodeRepresentation::setDiskChangesForbidden(true);
0089 }
0090 
0091 void TestProblems::cleanupTestCase()
0092 {
0093     TestCore::shutdown();
0094 }
0095 
0096 void TestProblems::testNoProblems()
0097 {
0098     const QByteArray code = "int main() {}";
0099     auto problems = parse(code);
0100     QCOMPARE(problems.size(), 0);
0101 }
0102 
0103 void TestProblems::testBasicProblems()
0104 {
0105     // expected:
0106     // <stdin>:1:13: error: expected ';' after class
0107     // class Foo {}
0108     //             ^
0109     //             ;
0110     const QByteArray code = "class Foo {}";
0111     auto problems = parse(code);
0112     QCOMPARE(problems.size(), 1);
0113     QCOMPARE(problems[0]->diagnostics().size(), 0);
0114     auto range = problems[0]->rangeInCurrentRevision();
0115     QCOMPARE(range.start(), KTextEditor::Cursor(0, 12));
0116     QCOMPARE(range.end(), KTextEditor::Cursor(0, 12));
0117 }
0118 
0119 void TestProblems::testBasicRangeSupport()
0120 {
0121     // expected:
0122     // <stdin>:1:17: warning: expression result unused [-Wunused-value]
0123     // int main() { (1 + 1); }
0124     //               ~ ^ ~
0125     const QByteArray code = "int main() { (1 + 1); }";
0126     auto problems = parse(code);
0127     QCOMPARE(problems.size(), 1);
0128     QCOMPARE(problems[0]->diagnostics().size(), 0);
0129     auto range = problems[0]->rangeInCurrentRevision();
0130     QCOMPARE(range.start(), KTextEditor::Cursor(0, 14));
0131     QCOMPARE(range.end(), KTextEditor::Cursor(0, 19));
0132 }
0133 
0134 void TestProblems::testChildDiagnostics()
0135 {
0136     // expected:
0137     // test.cpp:3:14: error: call to 'foo' is ambiguous
0138     // int main() { foo(0); }
0139     //              ^~~
0140     // test.cpp:1:6: note: candidate function
0141     // void foo(unsigned int);
0142     //      ^
0143     // test.cpp:2:6: note: candidate function
0144     // void foo(const char*);
0145     //      ^
0146     const QByteArray code = "void foo(unsigned int);\n"
0147                             "void foo(const char*);\n"
0148                             "int main() { foo(0); }";
0149     auto problems = parse(code);
0150     QCOMPARE(problems.size(), 1);
0151     auto range = problems[0]->rangeInCurrentRevision();
0152     QCOMPARE(range.start(), KTextEditor::Cursor(2, 13));
0153     QCOMPARE(range.end(), KTextEditor::Cursor(2, 16));
0154     QCOMPARE(problems[0]->diagnostics().size(), 2);
0155     IProblem::Ptr p1 = problems[0]->diagnostics()[0];
0156     const ProblemPointer d1 = ProblemPointer(dynamic_cast<Problem*>(p1.data()));
0157     QCOMPARE(d1->url().str(), FileName);
0158     QCOMPARE(d1->rangeInCurrentRevision(), KTextEditor::Range(0, 5, 0, 8));
0159     IProblem::Ptr p2 = problems[0]->diagnostics()[1];
0160     const ProblemPointer d2 = ProblemPointer(dynamic_cast<Problem*>(p2.data()));
0161     QCOMPARE(d2->url().str(), FileName);
0162     QCOMPARE(d2->rangeInCurrentRevision(), KTextEditor::Range(1, 5, 1, 8));
0163 }
0164 
0165 Q_DECLARE_METATYPE(QVector<ClangFixit>)
0166 
0167 /**
0168  * Provides a list of possible fixits: http://blog.llvm.org/2010/04/amazing-feats-of-clang-error-recovery.html
0169  */
0170 void TestProblems::testFixits()
0171 {
0172     QFETCH(QString, code);
0173     QFETCH(int, problemsCount);
0174     QFETCH(QVector<ClangFixit>, fixits);
0175 
0176     auto problems = parse(code.toLatin1());
0177 
0178     qDebug() << problems.last()->description();
0179     QCOMPARE(problems.size(), problemsCount);
0180 
0181     const ClangProblem* p1 = dynamic_cast<ClangProblem*>(problems[0].data());
0182     QVERIFY(p1);
0183     auto* a1 = qobject_cast<ClangFixitAssistant*>(p1->solutionAssistant().data());
0184     QVERIFY(a1);
0185 
0186     QCOMPARE(p1->allFixits(), fixits);
0187 }
0188 
0189 void TestProblems::testFixits_data()
0190 {
0191     QTest::addColumn<QString>("code"); // input
0192     QTest::addColumn<int>("problemsCount");
0193     QTest::addColumn<QVector<ClangFixit>>("fixits");
0194 
0195     // expected:
0196     // test -Wextra-tokens
0197     // /home/krf/test.cpp:2:8: warning: extra tokens at end of #endif directive [-Wextra-tokens]
0198     // #endif FOO
0199     //        ^
0200     //        //
0201     QTest::newRow("extra-tokens")
0202         << "#ifdef FOO\n#endif FOO\n"
0203         << 1
0204         << QVector<ClangFixit>{ ClangFixit{"//", DocumentRange(IndexedString(FileName), KTextEditor::Range(1, 7, 1, 7)), QString(), QString()} };
0205 
0206     const auto clangVersion9OrHigher = QVersionNumber::fromString(ClangHelpers::clangVersion()) >= QVersionNumber(9, 0, 0);
0207     // expected:
0208     // test.cpp:1:19: warning: empty parentheses interpreted as a function declaration [-Wvexing-parse]
0209     //     int a();
0210     //          ^~
0211     // test.cpp:1:19: note: replace parentheses with an initializer to declare a variable
0212     //     int a();
0213     //          ^~
0214     //           = 0
0215     QTest::newRow("vexing-parse")
0216         << "int main() { int a(); }\n"
0217         << 1
0218         << QVector<ClangFixit>{ ClangFixit{" = 0", DocumentRange(IndexedString(FileName),
0219             KTextEditor::Range(0, 18, 0, 20)), QString(),
0220             clangVersion9OrHigher ? QStringLiteral("()") : QString()} };
0221 
0222     // expected:
0223     // test.cpp:2:21: error: no member named 'someVariablf' in 'C'; did you mean 'someVariable'?
0224     // int main() { C c; c.someVariablf = 1; }
0225     //                     ^~~~~~~~~~~~
0226     //                     someVariable
0227     QTest::newRow("spell-check")
0228         << "class C{ int someVariable; };\n"
0229            "int main() { C c; c.someVariablf = 1; }\n"
0230         << 1
0231         << QVector<ClangFixit>{ ClangFixit{"someVariable", DocumentRange(IndexedString(FileName), KTextEditor::Range(1, 20, 1, 32)),
0232             QString(), clangVersion9OrHigher ? QStringLiteral("someVariablf") : QString()} };
0233 }
0234 
0235 struct Replacement
0236 {
0237     QString string;
0238     QString replacement;
0239 };
0240 using Replacements = QVector<Replacement>;
0241 
0242 ClangFixits resolveFilenames(const ClangFixits& fixits, const Replacements& replacements)
0243 {
0244     ClangFixits ret;
0245     for (const auto& fixit : fixits) {
0246         ClangFixit copy = fixit;
0247         for (const auto& replacement : replacements) {
0248             copy.replacementText.replace(replacement.string, replacement.replacement);
0249             copy.range.document = IndexedString(copy.range.document.str().replace(replacement.string, replacement.replacement));
0250         }
0251         ret << copy;
0252     }
0253     return ret;
0254 }
0255 
0256 void TestProblems::testMissingInclude()
0257 {
0258     QFETCH(QString, includeFileContent);
0259     QFETCH(QString, workingFileContent);
0260     QFETCH(QString, dummyFileName);
0261     QFETCH(QVector<ClangFixit>, fixits);
0262 
0263     TestFile include(includeFileContent, QStringLiteral("h"));
0264     include.parse(TopDUContext::AllDeclarationsAndContexts);
0265 
0266     QScopedPointer<QTemporaryFile> dummyFile;
0267     if (!dummyFileName.isEmpty()) {
0268         dummyFile.reset(new QTemporaryFile(QDir::tempPath() + dummyFileName));
0269         QVERIFY(dummyFile->open());
0270 
0271         workingFileContent.replace(QLatin1String("dummyInclude"), dummyFile->fileName());
0272     }
0273 
0274     TestFile workingFile(workingFileContent, QStringLiteral("cpp"));
0275     workingFile.parse(TopDUContext::AllDeclarationsAndContexts);
0276 
0277     QCOMPARE(include.url().toUrl().adjusted(QUrl::RemoveFilename), workingFile.url().toUrl().adjusted(QUrl::RemoveFilename));
0278     QVERIFY(include.waitForParsed());
0279     QVERIFY(workingFile.waitForParsed());
0280 
0281     DUChainReadLocker lock;
0282 
0283     QVERIFY(include.topContext());
0284     TopDUContext* includeTop = DUChainUtils::contentContextFromProxyContext(include.topContext().data());
0285     QVERIFY(includeTop);
0286 
0287     QVERIFY(workingFile.topContext());
0288     TopDUContext* top = DUChainUtils::contentContextFromProxyContext(workingFile.topContext());
0289     QVERIFY(top);
0290     QCOMPARE(top->problems().size(), 1);
0291 
0292     auto problem = dynamic_cast<UnknownDeclarationProblem*>(top->problems().first().data());
0293     auto assistant = problem->solutionAssistant();
0294     auto clangFixitAssistant = qobject_cast<ClangFixitAssistant*>(assistant.data());
0295     QVERIFY(clangFixitAssistant);
0296 
0297     auto resolvedFixits = resolveFilenames(fixits, {
0298        {"includeFile.h", include.url().toUrl().fileName()},
0299        {"workingFile.h", workingFile.url().toUrl().fileName()}
0300     });
0301     compareFixitsWithoutDescription(clangFixitAssistant->fixits(), resolvedFixits);
0302 }
0303 
0304 void TestProblems::testMissingInclude_data()
0305 {
0306     QTest::addColumn<QString>("includeFileContent");
0307     QTest::addColumn<QString>("workingFileContent");
0308     QTest::addColumn<QString>("dummyFileName");
0309     QTest::addColumn<QVector<ClangFixit>>("fixits");
0310 
0311     const QString workingFile = QDir::tempPath() + "/workingFile.h";
0312 
0313     QTest::newRow("basic") << "class A {};\n"
0314                            << "int main() { A a; }\n"
0315                            << QString()
0316                            << QVector<ClangFixit>{
0317                                   ClangFixit{"class A;\n",
0318                                              DocumentRange(IndexedString(workingFile), KTextEditor::Range(0, 0, 0, 0)),
0319                                              QString(), QString()},
0320                                   ClangFixit{"#include \"includeFile.h\"\n",
0321                                              DocumentRange(IndexedString(workingFile), KTextEditor::Range(0, 0, 0, 0)),
0322                                              QString(), QString()}};
0323 
0324     // cf. bug 375274
0325     QTest::newRow("ignore-moc-at-end")
0326         << "class Foo {};\n"
0327         << "#include <vector>\nint main() { Foo foo; }\n#include \"dummyInclude\"\n"
0328         << "/moc_fooXXXXXX.cpp"
0329         << QVector<ClangFixit>{ClangFixit{"class Foo;\n",
0330                                           DocumentRange(IndexedString(workingFile), KTextEditor::Range(0, 0, 0, 0)),
0331                                           QString(), QString()},
0332                                ClangFixit{"#include \"includeFile.h\"\n",
0333                                           DocumentRange(IndexedString(workingFile), KTextEditor::Range(1, 0, 1, 0)),
0334                                           QString(), QString()}};
0335     QTest::newRow("ignore-moc-at-end2")
0336         << "class Foo {};\n"
0337         << "int main() { Foo foo; }\n#include \"dummyInclude\"\n"
0338         << "/fooXXXXXX.moc"
0339         << QVector<ClangFixit>{ClangFixit{"class Foo;\n",
0340                                           DocumentRange(IndexedString(workingFile), KTextEditor::Range(0, 0, 0, 0)),
0341                                           QString(), QString()},
0342                                ClangFixit{"#include \"includeFile.h\"\n",
0343                                           DocumentRange(IndexedString(workingFile), KTextEditor::Range(0, 0, 0, 0)),
0344                                           QString(), QString()}};
0345 }
0346 
0347 struct ExpectedTodo
0348 {
0349     QString description;
0350     KTextEditor::Cursor start;
0351     KTextEditor::Cursor end;
0352 };
0353 using ExpectedTodos = QVector<ExpectedTodo>;
0354 Q_DECLARE_METATYPE(ExpectedTodos)
0355 
0356 void TestProblems::testTodoProblems()
0357 {
0358     QFETCH(QString, code);
0359     QFETCH(ExpectedTodos, expectedTodos);
0360 
0361     TestFile file(code, QStringLiteral("cpp"));
0362     QVERIFY(file.parseAndWait());
0363 
0364     DUChainReadLocker lock;
0365     auto top = file.topContext();
0366     QVERIFY(top);
0367     auto problems = top->problems();
0368     QCOMPARE(problems.size(), expectedTodos.size());
0369 
0370     for (int i = 0; i < problems.size(); ++i) {
0371         auto problem = problems[i];
0372         auto expectedTodo = expectedTodos[i];
0373         QCOMPARE(problem->description(), expectedTodo.description);
0374         QCOMPARE(problem->finalLocation().start(), expectedTodo.start);
0375         QCOMPARE(problem->finalLocation().end(), expectedTodo.end);
0376     }
0377 }
0378 
0379 void TestProblems::testTodoProblems_data()
0380 {
0381     QTest::addColumn<QString>("code");
0382     QTest::addColumn<ExpectedTodos>("expectedTodos");
0383 
0384     // we have two problems here:
0385     // - we cannot search for comments without declarations,
0386     //   that means: we can only search inside doxygen-style comments
0387     //   possible fix: -fparse-all-comments -- however: libclang API is lacking here again.
0388     //   Can only search through comments attached to a valid entity in the AST
0389     // - we cannot detect the correct location of the comment yet
0390     // see more comments inside TodoExtractor
0391     QTest::newRow("simple1")
0392         << "/** TODO: Something */\n/** notodo */\n"
0393         << ExpectedTodos{{"TODO: Something", {0, 4}, {0, 19}}};
0394     QTest::newRow("simple2")
0395         << "/// FIXME: Something\n"
0396         << ExpectedTodos{{"FIXME: Something", {0, 4}, {0, 20}}};
0397     QTest::newRow("mixed-content")
0398         << "/// FIXME: Something\n///Uninteresting content\n"
0399         << ExpectedTodos{{"FIXME: Something", {0, 4}, {0, 20}}};
0400     QTest::newRow("multi-line1")
0401         << "/**\n* foo\n*\n* FIXME: Something\n*/\n"
0402         << ExpectedTodos{{"FIXME: Something", {3, 2}, {3, 18}}};
0403     QTest::newRow("multi-line2")
0404         << "/// FIXME: Something\n///Uninteresting content\n"
0405         << ExpectedTodos{{"FIXME: Something", {0, 4}, {0, 20}}};
0406     QTest::newRow("multiple-todos-line2")
0407         << "/**\n* FIXME: one\n*foo bar\n* FIXME: two */\n"
0408         << ExpectedTodos{
0409             {"FIXME: one", {1, 2}, {1, 12}},
0410             {"FIXME: two", {3, 2}, {3, 12}}
0411         };
0412     QTest::newRow("todo-later-in-the-document")
0413         << "///foo\n\n///FIXME: bar\n"
0414         << ExpectedTodos{{"FIXME: bar", {2, 3}, {2, 13}}};
0415     QTest::newRow("non-ascii-todo")
0416         << "/* TODO: 例えば */"
0417         << ExpectedTodos{{"TODO: 例えば", {0, 3}, {0, 12}}};
0418 }
0419 
0420 void TestProblems::testProblemsForIncludedFiles()
0421 {
0422     TestFile header(QStringLiteral("#pragma once\n//TODO: header\n"), QStringLiteral("h"));
0423     TestFile file("#include \"" + header.url().str() + "\"\n//TODO: source\n", QStringLiteral("cpp"));
0424 
0425     file.parse(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::AST | TopDUContext::ForceUpdate);
0426     QVERIFY(file.waitForParsed(5000));
0427 
0428     {
0429         DUChainReadLocker lock;
0430         QVERIFY(file.topContext());
0431 
0432         auto context = DUChain::self()->chainForDocument(file.url());
0433         QVERIFY(context);
0434         QCOMPARE(context->problems().size(), 1);
0435         QCOMPARE(context->problems()[0]->description(), QStringLiteral("TODO: source"));
0436         QCOMPARE(context->problems()[0]->finalLocation().document, file.url());
0437 
0438         context = DUChain::self()->chainForDocument(header.url());
0439         QVERIFY(context);
0440         QCOMPARE(context->problems().size(), 1);
0441         QCOMPARE(context->problems()[0]->description(), QStringLiteral("TODO: header"));
0442         QCOMPARE(context->problems()[0]->finalLocation().document, header.url());
0443     }
0444 }
0445 
0446 using RangeList = QVector<KTextEditor::Range>;
0447 
0448 void TestProblems::testRanges_data()
0449 {
0450     QTest::addColumn<QByteArray>("code");
0451     QTest::addColumn<RangeList>("ranges");
0452 
0453     {
0454         // expected:
0455         // test.cpp:4:1: error: C++ requires a type specifier for all declarations
0456         // operator[](int){return string;}
0457         // ^
0458         //
0459         // test.cpp:4:24: error: 'string' does not refer to a value
0460         // operator[](int){return string;}
0461         //                        ^
0462         const QByteArray code = "struct string{};\nclass Test{\npublic:\noperator[](int){return string;}\n};";
0463         QTest::newRow("operator") << code << RangeList{{3, 0, 3, 8}, {3, 23, 3, 29}};
0464     }
0465     {
0466         const QByteArray code = "#include \"/some/file/that/does/not/exist.h\"\nint main() { return 0; }";
0467         QTest::newRow("badInclude") << code << RangeList{{0, 9, 0, 43}};
0468     }
0469     {
0470         const QByteArray code = "int main() const\n{ return 0; }";
0471         QTest::newRow("badConst") << code << RangeList{{0, 11, 0, 16}};
0472     }
0473 }
0474 
0475 void TestProblems::testRanges()
0476 {
0477     QFETCH(QByteArray, code);
0478     QFETCH(RangeList, ranges);
0479 
0480     const auto problems = parse(code);
0481     RangeList actualRanges;
0482     for (auto problem : problems) {
0483         actualRanges << problem->rangeInCurrentRevision();
0484     }
0485     qDebug() << actualRanges << ranges;
0486     QCOMPARE(actualRanges, ranges);
0487 }
0488 
0489 void TestProblems::testSeverity()
0490 {
0491     QFETCH(QByteArray, code);
0492     QFETCH(IProblem::Severity, severity);
0493 
0494     const auto problems = parse(code);
0495     QCOMPARE(problems.size(), 1);
0496     QCOMPARE(problems.at(0)->severity(), severity);
0497 }
0498 
0499 void TestProblems::testSeverity_data()
0500 {
0501     QTest::addColumn<QByteArray>("code");
0502     QTest::addColumn<IProblem::Severity>("severity");
0503 
0504     QTest::newRow("error") << QByteArray("class foo {}") << IProblem::Error;
0505     QTest::newRow("warning") << QByteArray("int main() { int foo = 1 / 0; return foo; }") << IProblem::Warning;
0506     QTest::newRow("hint-unused-variable") << QByteArray("int main() { int foo = 0; return 0; }") << IProblem::Hint;
0507     QTest::newRow("hint-unused-parameter") << QByteArray("int main(int argc, char**) { return 0; }") << IProblem::Hint;
0508 }
0509 
0510 #include "moc_test_problems.cpp"