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"