diff --git a/languages/plugins/custom-definesandincludes/compilerprovider/gcclikecompiler.cpp b/languages/plugins/custom-definesandincludes/compilerprovider/gcclikecompiler.cpp --- a/languages/plugins/custom-definesandincludes/compilerprovider/gcclikecompiler.cpp +++ b/languages/plugins/custom-definesandincludes/compilerprovider/gcclikecompiler.cpp @@ -28,6 +28,8 @@ #include #include +#include + #include "../debugarea.h" using namespace KDevelop; @@ -37,42 +39,71 @@ // compilers don't deduplicate QStringLiteral QString minusXC() { return QStringLiteral("-xc"); } QString minusXCPlusPlus() { return QStringLiteral("-xc++"); } +QString minusXCL() { return QStringLiteral("-xcl"); } +QString minusXCuda() { return QStringLiteral("-xcuda"); } -QStringList languageOptions(const QString& arguments) +QStringList languageOptions(const QStringList& arguments) { - - // TODO: handle -ansi flag: In C mode, this is equivalent to -std=c90. In C++ mode, it is equivalent to -std=c++98. - // TODO: check for -x flag on command line - const QRegularExpression regexp(QStringLiteral("-std=(\\S+)")); - // see gcc manpage or llvm/tools/clang/include/clang/Frontend/LangStandards.def for list of valid language options - auto result = regexp.match(arguments); - if(result.hasMatch()){ - auto standard = result.captured(0); - QString mode = result.captured(1); - QString language; - if (mode.startsWith(QLatin1String("c++")) || mode.startsWith(QLatin1String("gnu++"))) { - language = minusXCPlusPlus(); - } else if (mode.startsWith(QLatin1String("iso9899:"))) { - // all iso9899:xxxxx modes are C standards - language = minusXC(); - } else { - // check for c11, gnu99, etc: all of them have a digit after the c/gnu - const QRegularExpression cRegexp(QStringLiteral("(c|gnu)\\d.*")); - if (cRegexp.match(mode).hasMatch()) { + QString language; + bool languageDefined = false; + QString standardFlag; + QStringList result = arguments; + for (int i = 0; i < arguments.size(); ++i) { + const QString& arg = arguments.at(i); + if (arg.startsWith(QLatin1String("-std="))) { + standardFlag = arg; + QStringRef standardVersion = arg.midRef(strlen("-std=")); + if (standardVersion.startsWith(QLatin1String("c++")) || standardVersion.startsWith(QLatin1String("gnu++"))) { + language = minusXCPlusPlus(); + } else if (standardVersion.startsWith(QLatin1String("iso9899:"))) { + // all iso9899:xxxxx modes are C standards language = minusXC(); + } else if (standardVersion.startsWith(QLatin1String("CL"))) { + language = minusXCL(); + } else if (standardVersion.startsWith(QLatin1String("cuda"))) { + language = minusXCuda(); + } else { + // check for c11, gnu99, etc: all of them have a digit after the c/gnu + const QRegularExpression cRegexp("(c|gnu)\\d.*"); + if (cRegexp.match(standardVersion).hasMatch()) { + language = minusXC(); + } + if (language.isEmpty()) { + qCWarning(DEFINESANDINCLUDES) << "Failed to determine language from -std= flag:" << arguments; + } + } + } else if (arg == QLatin1String("-ansi")) { + standardFlag = arg; // depends on whether we compile C or C++ + // assume C unless there is a -x flag + language = minusXC(); + } else if (arg.startsWith(QLatin1String("-x"))) { + languageDefined = true; // No need to pass an additional language flag + if (arg == QLatin1String("-x")) { + // it is split into two arguments -> next argument is the language + if (i + 1 < arguments.size()) { + language = arg + arguments[i + 1]; + } + } else { + language = arg; } } + } + // We need to pass a language option because the compiler will error out when reading /dev/null without a -x option + if (!languageDefined) { if (language.isEmpty()) { - qCWarning(DEFINESANDINCLUDES) << "Failed to determine language from -std= flag:" << arguments; + if (standardFlag.isEmpty()) { + standardFlag = QStringLiteral("-std=c++11"); + result.append(standardFlag); + } + qCWarning(DEFINESANDINCLUDES) << "Could not detect language from" + << arguments << "-> assuming C++ with " << standardFlag; language = minusXCPlusPlus(); } - return {standard, language}; - + qCDebug(DEFINESANDINCLUDES) << "Lang+Standard for" << arguments << "are:" << standardFlag << language; + result.append(language); } - // no -std= flag passed -> assume c++11 - return {QStringLiteral("-std=c++11"), minusXCPlusPlus()}; + return result; } - } Defines GccLikeCompiler::defines(const QString& arguments) const @@ -88,8 +119,9 @@ QProcess proc; proc.setProcessChannelMode( QProcess::MergedChannels ); - // TODO: what about -mXXX or -target= flags, some of these change search paths/defines - auto compilerArguments = languageOptions(arguments); + // We need to pass all arguments because e.g. -m and -f flags change the default defines + const QStringList splitArguments = KShell::splitArgs(arguments); + auto compilerArguments = languageOptions(splitArguments); compilerArguments.append(QStringLiteral("-dM")); compilerArguments.append(QStringLiteral("-E")); compilerArguments.append(QProcess::nullDevice()); @@ -134,7 +166,8 @@ // /usr/include // End of search list. - auto compilerArguments = languageOptions(arguments); + const QStringList splitArguments = KShell::splitArgs(arguments); + auto compilerArguments = languageOptions(splitArguments); compilerArguments.append(QStringLiteral("-E")); compilerArguments.append(QStringLiteral("-v")); compilerArguments.append(QProcess::nullDevice()); diff --git a/languages/plugins/custom-definesandincludes/compilerprovider/tests/test_compilerprovider.h b/languages/plugins/custom-definesandincludes/compilerprovider/tests/test_compilerprovider.h --- a/languages/plugins/custom-definesandincludes/compilerprovider/tests/test_compilerprovider.h +++ b/languages/plugins/custom-definesandincludes/compilerprovider/tests/test_compilerprovider.h @@ -26,6 +26,8 @@ #include +#include "../compilerprovider.h" + class TestCompilerProvider : public QObject { Q_OBJECT @@ -37,6 +39,10 @@ void testStorageBackwardsCompatible(); void testCompilerIncludesAndDefinesForProject(); void testStorageNewSystem(); + void testLanguageAndStandard_data(); + void testLanguageAndStandard(); +private: + CompilerPointer m_clang; }; #endif diff --git a/languages/plugins/custom-definesandincludes/compilerprovider/tests/test_compilerprovider.cpp b/languages/plugins/custom-definesandincludes/compilerprovider/tests/test_compilerprovider.cpp --- a/languages/plugins/custom-definesandincludes/compilerprovider/tests/test_compilerprovider.cpp +++ b/languages/plugins/custom-definesandincludes/compilerprovider/tests/test_compilerprovider.cpp @@ -24,6 +24,7 @@ #include "test_compilerprovider.h" #include +#include #include #include @@ -37,7 +38,6 @@ #include -#include "../compilerprovider.h" #include "../settingsmanager.h" #include "../tests/projectsgenerator.h" @@ -86,6 +86,19 @@ { AutoTestShell::init(); TestCore::initialize(); + + auto settings = SettingsManager::globalInstance(); + auto provider = settings->provider(); + + QString clang = QStandardPaths::findExecutable("clang"); + if (!clang.isEmpty()) { + for (auto c : provider->compilerFactories()) { + if (c->name() == QLatin1String("Clang")) { + m_clang = c->createCompiler("Unitest-clang", clang); + break; + } + } + } } void TestCompilerProvider::cleanupTestCase() @@ -251,4 +264,178 @@ ICore::self()->projectController()->closeProject(project); } +namespace { +const char NOT_DEFINED[] = "__THIS_SHOULD_NOT_BE_DEFINED__"; + +Defines addUndefined(const Defines &defines, std::initializer_list vars) +{ + Defines result = defines; + for (auto var : vars) { + result.insert(var, NOT_DEFINED); + } + return result; +} + +void checkCStandard(const char* std, const char* versionNumber, bool isAnsi, + const Defines& extra = Defines()) +{ + Defines expected{{"__STDC__", "1"}, {"__STDC_VERSION__", versionNumber}, + {"__STRICT_ANSI__", isAnsi ? "1" : NOT_DEFINED}}; + expected = addUndefined(expected, {"__cplusplus", "__OBJC__", "__ASSEMBLER__"}); + expected.unite(extra); + QTest::newRow(std) << QString(QLatin1String("-std=") + std) << expected << QStringList{}; +}; + +void checkCppStandard(const char* std, const char* versionNumber, bool isAnsi, + const Defines& extra = Defines()) { + Defines expected{{"__cplusplus", versionNumber}, {"__STDC__", "1"}, + {"__STRICT_ANSI__", isAnsi ? "1" : NOT_DEFINED}}; + expected = addUndefined(expected, {"__OBJC__", "__ASSEMBLER__", "__STDC_VERSION__"}); + expected.unite(extra); + QTest::newRow(std) << QString(QLatin1String("-std=") + std) << expected << QStringList{}; +}; + +bool supportsFlags(const CompilerPointer clang, const QStringList &flags) { + if (!clang) { + return false; + } + QStringList args = flags; + args.append(QStringList{"-dM", "-E", "/dev/null"}); + int result = QProcess::execute(clang->path(), args); + qDebug() << "result of " << flags << "is" << result; + return result == 0; +} + +} + +void TestCompilerProvider::testLanguageAndStandard_data() +{ + QTest::addColumn("arguments"); + QTest::addColumn("expectedDefines"); + QTest::addColumn("expectedIncludes"); + + + // See clang/Frontend/LangStandards.def + // C standards: + // C90 doesn't define STDC_VERSION + checkCStandard("c89", NOT_DEFINED, true); // "ISO C 1990" + checkCStandard("c90", NOT_DEFINED, true); // "ISO C 1990" + checkCStandard("gnu89", NOT_DEFINED, false); // "ISO C 1990 with GNU extensions" + checkCStandard("gnu90", NOT_DEFINED, false); // "ISO C 1990 with GNU extensions" + checkCStandard("iso9899:199409", "199409L", true); // "ISO C 1990 with amendment 1" + + + checkCStandard("c99", "199901L", true); // "ISO C 1999" + checkCStandard("c9x", "199901L", true); // "ISO C 1999" + checkCStandard("iso9899:1999", "199901L", true); // "ISO C 1999" + checkCStandard("iso9899:199x", "199901L", true); // "ISO C 1999" + checkCStandard("gnu99", "199901L", false); // "ISO C 1999 with GNU extensions" + checkCStandard("gnu9x", "199901L", false); // "ISO C 1999 with GNU extensions" + + checkCStandard("c11", "201112L", true); // "ISO C 2011" + checkCStandard("c1x", "201112L", true); // "ISO C 2011" + checkCStandard("iso9899:2011", "201112L", true); // "ISO C 2011" + // Not all clang versions support this one: + // checkCStandard("iso9899:201x", "201112L", true); // "ISO C 2011" + checkCStandard("gnu11", "201112L", false); // "ISO C 2011 with GNU extensions" + checkCStandard("gnu1x", "201112L", false); // "ISO C 2011 with GNU extensions" + + checkCStandard("CL", "199901L", true, {{"__OPENCL_C_VERSION__", "100"}}); + checkCStandard("CL1.1", "199901L", true, {{"__OPENCL_C_VERSION__", "110"}}); + checkCStandard("CL1.2", "199901L", true, {{"__OPENCL_C_VERSION__", "120"}}); + if (supportsFlags(m_clang, {"-std=CL2.0", "-xcl"})) { + checkCStandard("CL2.0", "199901L", true, {{"__OPENCL_C_VERSION__", "200"}}); + } + // check that we can parse all C++ standards + checkCppStandard("c++98", "199711L", true); // "ISO C++ 1998 with amendments" + checkCppStandard("c++03", "199711L", true); // "ISO C++ 1998 with amendments" + checkCppStandard("gnu++98", "199711L", false); // "ISO C++ 1998 with amendments and GNU extensions" + + checkCppStandard("c++0x", "201103L", true); // "ISO C++ 2011 with amendments" + checkCppStandard("c++11", "201103L", true); // "ISO C++ 2011 with amendments" + checkCppStandard("gnu++0x", "201103L", false); // "ISO C++ 2011 with amendments and GNU extensions" + checkCppStandard("gnu++11", "201103L", false); // "ISO C++ 2011 with amendments and GNU extensions" + + if (supportsFlags(m_clang, {"-std=c++14", "-xc++"})) { + checkCppStandard("c++1y", "201402L", true); // "ISO C++ 2014 with amendments" + checkCppStandard("c++14", "201402L", true); // "ISO C++ 2014 with amendments" + checkCppStandard("gnu++1y", "201402L", false); // "ISO C++ 2014 with amendments and GNU extensions" + checkCppStandard("gnu++14", "201402L", false); // "ISO C++ 2014 with amendments and GNU extensions" + } + if (supportsFlags(m_clang, {"-std=c++17", "-xc++"})) { + // TODO: update value once C++17 is finalized + checkCppStandard("c++1z", "201703L", true); // "ISO C++ 2017 with amendments" + checkCppStandard("c++17", "201703L", true); // "ISO C++ 2017 with amendments" + checkCppStandard("gnu++1z", "201703L", false); // "ISO C++ 2017 with amendments and GNU extensions" + checkCppStandard("gnu++17", "201703L", false); // "ISO C++ 2017 with amendments and GNU extensions" + } + if (supportsFlags(m_clang, {"-std=cuda", "-xcuda"})) { + checkCppStandard("cuda", "199711L", true, {{"__CUDA__", "1"}}); // "NVIDIA CUDA(tm)" + } + // check the -ansi flag (should default to C unless -xc++ is set: + { + Defines expected{{"__STDC__", "1"}, {"__STDC_VERSION__", NOT_DEFINED}, + {"__STRICT_ANSI__", "1"}}; + expected = addUndefined(expected, {"__cplusplus", "__OBJC__", "__ASSEMBLER__"}); + QTest::newRow("-ansi") << "-ansi" << expected << QStringList{}; + } + // -ansi with -xc++ + { + Defines expected{{"__STDC__", "1"}, {"__cplusplus", "199711L"}, + {"__STRICT_ANSI__", "1"}}; + expected = addUndefined(expected, {"__STDC_VERSION__", "__OBJC__", "__ASSEMBLER__"}); + QTest::newRow("-ansi -xc++") << "-ansi -xc++" << expected << QStringList{}; + } + // check that -target flag changes defines + QTest::newRow("target linux") << "-target x86_64-unknown-linux-gnu -std=c++11" + << Defines{{"__linux__", "1"}, {"__FreeBSD__", NOT_DEFINED}} << QStringList{}; + QTest::newRow("target freebsd") << "-target x86_64-unknown-freebsd10 -std=c++11" + << Defines{{"__linux__", NOT_DEFINED}, {"__FreeBSD__", "10"}} << QStringList{}; + + // check that -f flags such as -fexceptions are also handled + QTest::newRow("-fexceptions") << "-std=c++11 -fexceptions" + << Defines{{"__EXCEPTIONS", "1"}} << QStringList{}; + QTest::newRow("-fno-exceptions") << "-std=c++11 -fno-exceptions" + << Defines{{"__EXCEPTIONS", NOT_DEFINED}} << QStringList{}; + + // check that -m flags such as -m32 change the defines + QTest::newRow("m64") << "-target x86_64-unknown-linux-gnu -std=c++11 -m64" + << Defines{{"__x86_64__", "1"}, {"__i386__", NOT_DEFINED}, + {"_ILP32", NOT_DEFINED}, {"_LP64", "1"}} << QStringList{}; + QTest::newRow("m32") << "-target x86_64-unknown-linux-gnu -std=c++11 -m32" + << Defines{{"__x64_64__", NOT_DEFINED}, {"__i386__", "1"}, {"_ILP32", "1"}, + {"_LP64", NOT_DEFINED}} << QStringList{}; +} + +void TestCompilerProvider::testLanguageAndStandard() +{ + QFETCH(QString, arguments); + QFETCH(Defines, expectedDefines); + QFETCH(QStringList, expectedIncludes); + + if (!m_clang) { + QSKIP("Test requires a Clang compiler"); + } + + auto defines = m_clang->defines(arguments); + auto includes = m_clang->includes(arguments); + // check that we have all the expected defines + for (auto key : expectedDefines.keys()) { + QString expectedValue = expectedDefines.value(key); + if (expectedValue == QLatin1String(NOT_DEFINED)) { + QString msg = key + " should not be defined for " + arguments + " but got '" + + defines.value(key) + "'"; + QVERIFY2(!defines.contains(key), qPrintable(msg)); + } else { + QVERIFY2(defines.contains(key), qPrintable(key + " should be defined for " + arguments)); + QCOMPARE(QString(key + "=" + expectedValue), QString(key + "=" + defines.value(key))); + } + + } + for (auto path : expectedIncludes) { + QVERIFY2(includes.contains(Path(path)), + qPrintable(path + " should an include for " + arguments)); + } +} + QTEST_MAIN(TestCompilerProvider)