File indexing completed on 2024-05-05 04:39:30

0001 /*
0002     SPDX-FileCopyrightText: 2016 Anton Anikin <anton.anikin@htower.ru>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "parameters.h"
0008 
0009 #include "globalsettings.h"
0010 #include "projectsettings.h"
0011 
0012 #include <interfaces/iplugin.h>
0013 #include <interfaces/iproject.h>
0014 #include <project/interfaces/ibuildsystemmanager.h>
0015 #include <project/projectmodel.h>
0016 
0017 #include <KShell>
0018 #include <KLocalizedString>
0019 
0020 #include <QFile>
0021 #include <QRegularExpression>
0022 
0023 namespace cppcheck
0024 {
0025 
0026 void includesForItem(KDevelop::ProjectBaseItem* parent, QSet<KDevelop::Path>& includes)
0027 {
0028     // FIXME: only one build system manager - QMakeManager - ever returns a nonempty include directory list
0029     // for a target item. With any other project build system manager, cppcheck::Parameters::m_includeDirectories
0030     // is always empty. For example, during the loop below CMakeManager prints the following debug message:
0031     //     kdevelop.plugins.cmake: no information found for ""
0032     // because a target item's path() is always empty.
0033     // The algorithm here attempts to collect include directories for all project targets and use them to analyze any
0034     // file. This algorithm can be patched up by passing the first child of a target item instead of the target item
0035     // itself to IBuildSystemManager::includeDirectories(). However, the include directories would needlessly contain
0036     // include paths for all unit tests then. Also each file in a target can potentially have different include paths.
0037     // So perhaps the current algorithm should be replaced with something else altogether:
0038     // 1. When KDevelop runs cppcheck on a single file, that file's project item should be passed to
0039     // IBuildSystemManager::includeDirectories().
0040     // 2. When KDevelop runs cppcheck on an entire project, it passes the project's root directory to cppcheck
0041     // on the command line. cppcheck checks all source files in the given directory recursively.
0042     // db0d8027749ba8c94702981ccb3062fa6c6006eb even implemented a workaround to skip cppcheck-ing files in
0043     // <current build dir>/CMakeFiles/. That workaround is not perfect: what if there are multiple build
0044     // subdirectories (e.g. debug and release) within the project's directory? Instead of running a single
0045     // cppcheck command, we could run a separate cppcheck command for each not-filtered-out source file in
0046     // the project. This way we could pass each file's project item to IBuildSystemManager::includeDirectories()
0047     // and use a separate include directory list to analyze every file. But this would require determining which
0048     // files should and which shouldn't be analyzed (C and C++ MIME types?), and might be slower.
0049     // Apparently fixing this issue properly requires substantial refactoring and testing effort.
0050 
0051     const auto children = parent->children();
0052     for (auto* child : children) {
0053         if (child->type() == KDevelop::ProjectBaseItem::ProjectItemType::File) {
0054             continue;
0055         }
0056 
0057         else if (child->type() == KDevelop::ProjectBaseItem::ProjectItemType::ExecutableTarget ||
0058                  child->type() == KDevelop::ProjectBaseItem::ProjectItemType::LibraryTarget ||
0059                  child->type() == KDevelop::ProjectBaseItem::ProjectItemType::Target) {
0060 
0061             if (auto buildSystemManager = child->project()->buildSystemManager()) {
0062                 const auto includeDirectories = buildSystemManager->includeDirectories(child);
0063                 for (auto& dir : includeDirectories) {
0064                     includes.insert(dir);
0065                 }
0066             }
0067         }
0068 
0069         includesForItem(child, includes);
0070     }
0071 }
0072 
0073 QList<KDevelop::Path> includesForProject(KDevelop::IProject* project)
0074 {
0075     QSet<KDevelop::Path> includesSet;
0076     includesForItem(project->projectItem(), includesSet);
0077 
0078     return includesSet.values();
0079 }
0080 
0081 Parameters::Parameters(KDevelop::IProject* project)
0082     : m_project(project)
0083 {
0084     executablePath = KDevelop::Path(GlobalSettings::executablePath()).toLocalFile();
0085     hideOutputView = GlobalSettings::hideOutputView();
0086     showXmlOutput  = GlobalSettings::showXmlOutput();
0087 
0088     if (!project) {
0089         checkStyle           = defaults::checkStyle;
0090         checkPerformance     = defaults::checkPerformance;
0091         checkPortability     = defaults::checkPortability;
0092         checkInformation     = defaults::checkInformation;
0093         checkUnusedFunction  = defaults::checkUnusedFunction;
0094         checkMissingInclude  = defaults::checkMissingInclude;
0095         inconclusiveAnalysis = defaults::inconclusiveAnalysis;
0096         forceCheck           = defaults::forceCheck;
0097         checkConfig          = defaults::checkConfig;
0098 
0099         useProjectIncludes   = defaults::useProjectIncludes;
0100         useSystemIncludes    = defaults::useSystemIncludes;
0101 
0102         return;
0103     }
0104 
0105     ProjectSettings projectSettings;
0106     projectSettings.setSharedConfig(project->projectConfiguration());
0107     projectSettings.load();
0108 
0109     checkStyle           = projectSettings.checkStyle();
0110     checkPerformance     = projectSettings.checkPerformance();
0111     checkPortability     = projectSettings.checkPortability();
0112     checkInformation     = projectSettings.checkInformation();
0113     checkUnusedFunction  = projectSettings.checkUnusedFunction();
0114     checkMissingInclude  = projectSettings.checkMissingInclude();
0115     inconclusiveAnalysis = projectSettings.inconclusiveAnalysis();
0116     forceCheck           = projectSettings.forceCheck();
0117     checkConfig          = projectSettings.checkConfig();
0118 
0119     useProjectIncludes   = projectSettings.useProjectIncludes();
0120     useSystemIncludes    = projectSettings.useSystemIncludes();
0121     ignoredIncludes      = projectSettings.ignoredIncludes();
0122 
0123     extraParameters      = projectSettings.extraParameters();
0124 
0125     m_projectRootPath    = m_project->path();
0126 
0127     if (auto buildSystemManager = m_project->buildSystemManager()) {
0128         m_projectBuildPath   = buildSystemManager->buildDirectory(m_project->projectItem());
0129     }
0130     m_includeDirectories = includesForProject(project);
0131 }
0132 
0133 QStringList Parameters::commandLine() const
0134 {
0135     QString temp;
0136     return commandLine(temp);
0137 }
0138 
0139 QStringList Parameters::commandLine(QString& infoMessage) const
0140 {
0141     static const auto mocHeaderRegex = QRegularExpression(QStringLiteral("#define\\s+Q_MOC_OUTPUT_REVISION\\s+(.+)"));
0142     static const auto mocParametersRegex = QRegularExpression(QStringLiteral("-DQ_MOC_OUTPUT_REVISION=\\d{2,}"));
0143 
0144     const QString mocMessage = i18n(
0145         "It seems that this project uses Qt library. For correctly work of cppcheck "
0146         "the value for define Q_MOC_OUTPUT_REVISION must be set. Unfortunately, the plugin is unable "
0147         "to find this value automatically - you should set it manually by adding "
0148         "'-DQ_MOC_OUTPUT_REVISION=XX' to extra parameters. The 'XX' value can be found in any project's "
0149         "moc-generated file or in the <QtCore/qobjectdefs.h> header file.");
0150 
0151     QStringList result;
0152 
0153     result << executablePath;
0154     result << QStringLiteral("--xml-version=2");
0155 
0156     if (checkStyle) {
0157         result << QStringLiteral("--enable=style");
0158     }
0159 
0160     if (checkPerformance) {
0161         result << QStringLiteral("--enable=performance");
0162     }
0163 
0164     if (checkPortability) {
0165         result << QStringLiteral("--enable=portability");
0166     }
0167 
0168     if (checkInformation) {
0169         result << QStringLiteral("--enable=information");
0170     }
0171 
0172     if (checkUnusedFunction) {
0173         result << QStringLiteral("--enable=unusedFunction");
0174     }
0175 
0176     if (checkMissingInclude) {
0177         result << QStringLiteral("--enable=missingInclude");
0178     }
0179 
0180     if (inconclusiveAnalysis) {
0181         result << QStringLiteral("--inconclusive");
0182     }
0183 
0184     if (forceCheck) {
0185         result << QStringLiteral("--force");
0186     }
0187 
0188     if (checkConfig) {
0189         result << QStringLiteral("--check-config");
0190     }
0191 
0192     // Try to automatically get value of Q_MOC_OUTPUT_REVISION for Qt-projects.
0193     // If such define is not correctly set, cppcheck 'fails' on files with moc-includes
0194     // and not return any errors, even if the file contains them.
0195     if (!mocParametersRegex.match(extraParameters).hasMatch()) {
0196         bool qtUsed = false;
0197         bool mocDefineFound = false;
0198         for (const auto& dir : m_includeDirectories) {
0199             if (dir.path().endsWith(QLatin1String("QtCore"))) {
0200                 qtUsed = true;
0201 
0202                 QFile qtHeader(dir.path() + QStringLiteral("/qobjectdefs.h"));
0203                 if (!qtHeader.open(QIODevice::ReadOnly)) {
0204                     break;
0205                 }
0206 
0207                 while(!qtHeader.atEnd()) {
0208                     auto match = mocHeaderRegex.match(QString::fromUtf8(qtHeader.readLine()));
0209                     if (match.hasMatch()) {
0210                         mocDefineFound = true;
0211                         result << QLatin1String("-DQ_MOC_OUTPUT_REVISION=") + match.capturedRef(1);
0212                         break;
0213                     }
0214                 }
0215                 break;
0216             }
0217         }
0218 
0219         if (qtUsed && !mocDefineFound) {
0220             infoMessage = mocMessage;
0221         }
0222     }
0223 
0224     if (!extraParameters.isEmpty()) {
0225         result << KShell::splitArgs(applyPlaceholders(extraParameters));
0226     }
0227 
0228     if (m_project && useProjectIncludes) {
0229         QList<KDevelop::Path> ignored;
0230 
0231         const auto elements = applyPlaceholders(ignoredIncludes).split(QLatin1Char(';'));
0232         for (const QString& element : elements) {
0233             if (!element.trimmed().isEmpty()) {
0234                 ignored.append(KDevelop::Path(element));
0235             }
0236         }
0237 
0238         for (const auto& dir : m_includeDirectories) {
0239             if (ignored.contains(dir)) {
0240                 continue;
0241             }
0242 
0243             else if (useSystemIncludes ||
0244                      dir == m_projectRootPath || m_projectRootPath.isParentOf(dir) ||
0245                      dir == m_projectBuildPath || m_projectBuildPath.isParentOf(dir)) {
0246 
0247                 result << QStringLiteral("-I");
0248                 result << dir.toLocalFile();
0249             }
0250         }
0251     }
0252 
0253     if (m_project && m_project->managerPlugin()) {
0254         if (m_project->managerPlugin()->componentName() == QLatin1String("kdevcmakemanager")) {
0255             result << QStringLiteral("-i")
0256                    << m_projectBuildPath.toLocalFile() + QLatin1String("/CMakeFiles");
0257         }
0258     }
0259 
0260     result << checkPath;
0261 
0262     return result;
0263 }
0264 
0265 KDevelop::Path Parameters::projectRootPath() const
0266 {
0267     return m_projectRootPath;
0268 }
0269 
0270 QString Parameters::applyPlaceholders(const QString& text) const
0271 {
0272     QString result(text);
0273 
0274     if (m_project) {
0275         result.replace(QLatin1String("%p"), m_projectRootPath.toLocalFile());
0276         result.replace(QLatin1String("%b"), m_projectBuildPath.toLocalFile());
0277     }
0278 
0279     return result;
0280 }
0281 
0282 }