diff --git a/.github/workflows/selfcheck.yml b/.github/workflows/selfcheck.yml index ec52b15f939..ff112927598 100644 --- a/.github/workflows/selfcheck.yml +++ b/.github/workflows/selfcheck.yml @@ -121,7 +121,7 @@ jobs: - name: Self check (unusedFunction / no test / no gui) run: | - supprs="--suppress=unusedFunction:lib/errorlogger.h:197 --suppress=unusedFunction:lib/importproject.cpp:1531 --suppress=unusedFunction:lib/importproject.cpp:1555" + supprs="--suppress=unusedFunction:lib/errorlogger.h:197 --suppress=unusedFunction:lib/importproject.cpp:1584 --suppress=unusedFunction:lib/importproject.cpp:1608" ./cppcheck -q --template=selfcheck --error-exitcode=1 --library=cppcheck-lib -D__CPPCHECK__ -D__GNUC__ --enable=unusedFunction,information --exception-handling -rp=. --project=cmake.output.notest_nogui/compile_commands.json --suppressions-list=.selfcheck_unused_suppressions --inline-suppr $supprs env: DISABLE_VALUEFLOW: 1 diff --git a/cli/cmdlineparser.cpp b/cli/cmdlineparser.cpp index 11f735e1ae5..bb6b9cbabd5 100644 --- a/cli/cmdlineparser.cpp +++ b/cli/cmdlineparser.cpp @@ -1188,7 +1188,9 @@ CmdLineParser::Result CmdLineParser::parseFromArgs(int argc, const char* const a } if (projectType == ImportProject::Type::COMPILE_DB) mSettings.maxConfigsProject = 1; - if (projectType == ImportProject::Type::VS_SLN || projectType == ImportProject::Type::VS_VCXPROJ) { + if (projectType == ImportProject::Type::VS_SLN || + projectType == ImportProject::Type::VS_SLNX || + projectType == ImportProject::Type::VS_VCXPROJ) { mSettings.libraries.emplace_back("windows"); } for (const auto &error : project.errors) @@ -1214,7 +1216,9 @@ CmdLineParser::Result CmdLineParser::parseFromArgs(int argc, const char* const a mLogger.printError("--project-configuration parameter is empty."); return Result::Fail; } - if (projectType != ImportProject::Type::VS_SLN && projectType != ImportProject::Type::VS_VCXPROJ) { + if (projectType != ImportProject::Type::VS_SLN && + projectType != ImportProject::Type::VS_SLNX && + projectType != ImportProject::Type::VS_VCXPROJ) { mLogger.printError("--project-configuration has no effect - no Visual Studio project provided."); return Result::Fail; } @@ -1649,7 +1653,9 @@ CmdLineParser::Result CmdLineParser::parseFromArgs(int argc, const char* const a mSettings.platform.defaultSign = defaultSign; if (!mSettings.analyzeAllVsConfigs) { - if (projectType != ImportProject::Type::VS_SLN && projectType != ImportProject::Type::VS_VCXPROJ) { + if (projectType != ImportProject::Type::VS_SLN && + projectType != ImportProject::Type::VS_SLNX && + projectType != ImportProject::Type::VS_VCXPROJ) { if (mAnalyzeAllVsConfigsSetOnCmdLine) { mLogger.printError("--no-analyze-all-vs-configs has no effect - no Visual Studio project provided."); return Result::Fail; @@ -1935,13 +1941,13 @@ void CmdLineParser::printHelp() const oss << " --project= Run Cppcheck on project. The can be a Visual\n" - " Studio Solution (*.sln), Visual Studio Project\n" + " Studio Solution (*.sln) or (*.slnx), Visual Studio Project\n" " (*.vcxproj), compile database (compile_commands.json),\n" " or Borland C++ Builder 6 (*.bpr). The files to analyse,\n" " include paths, defines, platform and undefines in\n" " the specified file will be used.\n" " --project-configuration=\n" - " If used together with a Visual Studio Solution (*.sln)\n" + " If used together with a Visual Studio Solution (*.sln) or (*.slnx)\n" " or Visual Studio Project (*.vcxproj) you can limit\n" " the configuration cppcheck should check.\n" " For example: '--project-configuration=Release|Win32'\n" diff --git a/gui/mainwindow.cpp b/gui/mainwindow.cpp index 63917bdc912..161eec381b3 100644 --- a/gui/mainwindow.cpp +++ b/gui/mainwindow.cpp @@ -768,7 +768,7 @@ QStringList MainWindow::selectFilesToAnalyze(QFileDialog::FileMode mode) QMap filters; filters[tr("C/C++ Source")] = FileList::getDefaultFilters().join(" "); filters[tr("Compile database")] = compile_commands_json; - filters[tr("Visual Studio")] = "*.sln *.vcxproj"; + filters[tr("Visual Studio")] = "*.sln *.slnx *.vcxproj"; filters[tr("Borland C++ Builder 6")] = "*.bpr"; QString lastFilter = mSettings->value(SETTINGS_LAST_ANALYZE_FILES_FILTER).toString(); selected = QFileDialog::getOpenFileNames(this, @@ -811,13 +811,14 @@ void MainWindow::analyzeFiles() const QString file0 = (!selected.empty() ? selected[0].toLower() : QString()); if (file0.endsWith(".sln") + || file0.endsWith(".slnx") || file0.endsWith(".vcxproj") || file0.endsWith(compile_commands_json) || file0.endsWith(".bpr")) { ImportProject p; p.import(selected[0].toStdString()); - if (file0.endsWith(".sln")) { + if (file0.endsWith(".sln") || file0.endsWith(".slnx")) { QStringList configs; for (auto it = p.fileSettings.cbegin(); it != p.fileSettings.cend(); ++it) { const QString cfg(QString::fromStdString(it->cfg)); @@ -1968,6 +1969,7 @@ void MainWindow::analyzeProject(const ProjectFile *projectFile, const QStringLis switch (result) { case ImportProject::Type::COMPILE_DB: case ImportProject::Type::VS_SLN: + case ImportProject::Type::VS_SLNX: case ImportProject::Type::VS_VCXPROJ: case ImportProject::Type::BORLAND: case ImportProject::Type::CPPCHECK_GUI: diff --git a/gui/projectfiledialog.cpp b/gui/projectfiledialog.cpp index 21618dbb017..d46eb5c3489 100644 --- a/gui/projectfiledialog.cpp +++ b/gui/projectfiledialog.cpp @@ -111,7 +111,7 @@ static std::string suppressionAsText(const SuppressionList::Suppression& s) QStringList ProjectFileDialog::getProjectConfigs(const QString &fileName) { - if (!fileName.endsWith(".sln") && !fileName.endsWith(".vcxproj")) + if (!fileName.endsWith(".sln") && !fileName.endsWith(".slnx") && !fileName.endsWith(".vcxproj")) return QStringList(); QStringList ret; ImportProject importer; @@ -596,7 +596,7 @@ void ProjectFileDialog::updatePathsAndDefines() { const QString &fileName = mUI->mEditImportProject->text(); const bool importProject = !fileName.isEmpty(); - const bool hasConfigs = fileName.endsWith(".sln") || fileName.endsWith(".vcxproj"); + const bool hasConfigs = fileName.endsWith(".sln") || fileName.endsWith(".slnx") || fileName.endsWith(".vcxproj"); mUI->mBtnClearImportProject->setEnabled(importProject); mUI->mListCheckPaths->setEnabled(!importProject); mUI->mListIncludeDirs->setEnabled(!importProject); @@ -626,7 +626,7 @@ void ProjectFileDialog::browseImportProject() const QFileInfo inf(mProjectFile->getFilename()); const QDir &dir = inf.absoluteDir(); QMap filters; - filters[tr("Visual Studio")] = "*.sln *.vcxproj"; + filters[tr("Visual Studio")] = "*.sln *.slnx *.vcxproj"; filters[tr("Compile database")] = "compile_commands.json"; filters[tr("Borland C++ Builder 6")] = "*.bpr"; QString fileName = QFileDialog::getOpenFileName(this, tr("Import Project"), diff --git a/lib/importproject.cpp b/lib/importproject.cpp index 33b75b5dc7e..4033b678d02 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -330,6 +330,11 @@ ImportProject::Type ImportProject::import(const std::string &filename, Settings setRelativePaths(filename); return ImportProject::Type::VS_SLN; } + } else if (endsWith(filename, ".slnx")) { + if (importSlnx(filename, fileFilters)) { + setRelativePaths(filename); + return ImportProject::Type::VS_SLNX; + } } else if (endsWith(filename, ".vcxproj")) { std::map variables; std::vector sharedItemsProjects; @@ -503,6 +508,54 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const return true; } +bool ImportProject::importSlnx(const std::string& filename, const std::vector& fileFilters) +{ + tinyxml2::XMLDocument doc; + const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); + if (error != tinyxml2::XML_SUCCESS) { + errors.emplace_back(std::string("Visual Studio solution file is not a valid XML - ") + tinyxml2::XMLDocument::ErrorIDToName(error)); + return false; + } + + const tinyxml2::XMLElement* const rootnode = doc.FirstChildElement(); + if (rootnode == nullptr) { + errors.emplace_back("Visual Studio solution file has no XML root node"); + return false; + } + + std::map variables; + variables["SolutionDir"] = Path::simplifyPath(Path::getPathFromFilename(filename)); + + bool found = false; + std::vector sharedItemsProjects; + + for (const tinyxml2::XMLElement* node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { + const char* name = node->Name(); + if (std::strcmp(name, "Project") == 0) { + const char* labelAttribute = node->Attribute("Path"); + if (labelAttribute) { + std::string vcxproj(labelAttribute); + vcxproj = Path::toNativeSeparators(std::move(vcxproj)); + if (!Path::isAbsolute(vcxproj)) + vcxproj = variables["SolutionDir"] + vcxproj; + vcxproj = Path::fromNativeSeparators(std::move(vcxproj)); + if (!importVcxproj(vcxproj, variables, "", fileFilters, sharedItemsProjects)) { + errors.emplace_back("failed to load '" + vcxproj + "' from Visual Studio solution"); + return false; + } + found = true; + } + } + } + + if (!found) { + errors.emplace_back("no projects found in Visual Studio solution file"); + return false; + } + + return true; +} + namespace { struct ProjectConfiguration { explicit ProjectConfiguration(const tinyxml2::XMLElement *cfg) { diff --git a/lib/importproject.h b/lib/importproject.h index ba873b69ae5..a5ea6026af0 100644 --- a/lib/importproject.h +++ b/lib/importproject.h @@ -63,6 +63,7 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { FAILURE, COMPILE_DB, VS_SLN, + VS_SLNX, VS_VCXPROJ, BORLAND, CPPCHECK_GUI @@ -120,6 +121,7 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { void setRelativePaths(const std::string &filename); bool importSln(std::istream &istr, const std::string &path, const std::vector &fileFilters); + bool importSlnx(const std::string& filename, const std::vector& fileFilters); SharedItemsProject importVcxitems(const std::string &filename, const std::vector &fileFilters, std::vector &cache); bool importBcb6Prj(const std::string &projectFilename); diff --git a/man/manual.md b/man/manual.md index dc07026dc2b..5b2f43eb047 100644 --- a/man/manual.md +++ b/man/manual.md @@ -309,12 +309,16 @@ To ignore certain folders you can use `-i`. This will skip analysis of source fi ## Visual Studio -You can run Cppcheck on individual project files (\*.vcxproj) or on a whole solution (\*.sln) +You can run Cppcheck on individual project files (\*.vcxproj) or on a whole solution (\*.sln) or (\*.slnx) Running Cppcheck on an entire Visual Studio solution: cppcheck --project=foobar.sln +Running Cppcheck on an entire Visual Studio 2026 solution: + + cppcheck --project=foobar.slnx + Running Cppcheck on a Visual Studio project: cppcheck --project=foobar.vcxproj diff --git a/test/cli/helloworld/helloworld.slnx b/test/cli/helloworld/helloworld.slnx new file mode 100644 index 00000000000..ae14b4c329b --- /dev/null +++ b/test/cli/helloworld/helloworld.slnx @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test/cli/helloworld/helloworld_slnx.cppcheck b/test/cli/helloworld/helloworld_slnx.cppcheck new file mode 100644 index 00000000000..cf0bff648cc --- /dev/null +++ b/test/cli/helloworld/helloworld_slnx.cppcheck @@ -0,0 +1,6 @@ + + + + helloworld.slnx + false + diff --git a/test/cli/helloworld_test.py b/test/cli/helloworld_test.py index 7ccc113af43..65c0e4921a0 100644 --- a/test/cli/helloworld_test.py +++ b/test/cli/helloworld_test.py @@ -223,11 +223,12 @@ def test_cppcheck_project_local_path_select_one_multiple(): def test_cppcheck_project_local_path_analyze_all(): __test_cppcheck_project_local_path(['--analyze-all-vs-configs'], 'Debug|Win32 Debug|x64 Release|Win32 Release|x64') -def test_cppcheck_project_relative_path(): +@pytest.mark.parametrize("project_file", ["helloworld.cppcheck", "helloworld_slnx.cppcheck"]) +def test_cppcheck_project_relative_path(project_file): args = [ '--template=cppcheck1', '--platform=win64', - '--project=' + os.path.join('helloworld', 'helloworld.cppcheck') + '--project=' + os.path.join('helloworld', project_file) ] ret, stdout, stderr = cppcheck(args, cwd=__script_dir) filename = os.path.join('helloworld', 'main.c') @@ -235,11 +236,12 @@ def test_cppcheck_project_relative_path(): assert __getVsConfigs(stdout, filename) == 'Debug|x64' assert stderr == '[%s:5]: (error) Division by zero.\n' % filename -def test_cppcheck_project_absolute_path(): +@pytest.mark.parametrize("project_file", ["helloworld.cppcheck", "helloworld_slnx.cppcheck"]) +def test_cppcheck_project_absolute_path(project_file): args = [ '--template=cppcheck1', '--platform=win64', - '--project=' + os.path.join(__proj_dir, 'helloworld.cppcheck') + '--project=' + os.path.join(__proj_dir, project_file) ] ret, stdout, stderr = cppcheck(args) filename = os.path.join(__proj_dir, 'main.c') @@ -296,11 +298,12 @@ def test_suppress_project_absolute(tmp_path): assert ret == 0, stdout assert stderr == '' -def test_exclude(): +@pytest.mark.parametrize("project_file", ["helloworld.cppcheck", "helloworld_slnx.cppcheck"]) +def test_exclude(project_file): args = [ '-i' + 'helloworld', '--platform=win64', - '--project=' + os.path.join('helloworld', 'helloworld.cppcheck') + '--project=' + os.path.join('helloworld', project_file) ] ret, stdout, _ = cppcheck(args, cwd=__script_dir) assert ret == 1 diff --git a/test/cli/project_test.py b/test/cli/project_test.py index e8c120f2c08..7421ca406a6 100644 --- a/test/cli/project_test.py +++ b/test/cli/project_test.py @@ -7,7 +7,7 @@ from testutils import cppcheck -@pytest.mark.parametrize("project_ext", ["json", "sln", "vcxproj", "bpr", "cppcheck"]) +@pytest.mark.parametrize("project_ext", ["json", "sln", "slnx", "vcxproj", "bpr", "cppcheck"]) def test_missing_project(project_ext): project_file = "file.{}".format(project_ext) @@ -33,6 +33,7 @@ def __test_project_error(tmpdir, ext, content, expected): @pytest.mark.parametrize("project_ext, expected", [ ("json", "compilation database is not a JSON array"), ("sln", "Visual Studio solution file is empty"), + ("slnx", "Visual Studio solution file is not a valid XML - XML_ERROR_EMPTY_DOCUMENT"), ("vcxproj", "Visual Studio project file is not a valid XML - XML_ERROR_EMPTY_DOCUMENT"), ("bpr", "Borland project file is not a valid XML - XML_ERROR_EMPTY_DOCUMENT"), ("cppcheck", "Cppcheck GUI project file is not a valid XML - XML_ERROR_EMPTY_DOCUMENT") @@ -138,6 +139,46 @@ def test_sln_project_file_not_found(tmpdir): __test_project_error(tmpdir, "sln", content, expected) +def test_slnx_no_xml_root(tmpdir): + content = '' + + expected = "Visual Studio solution file has no XML root node" + + __test_project_error(tmpdir, "slnx", content, expected) + + +def test_slnx_no_projects(tmpdir): + content = '\r\n' \ + "\r\n" \ + " \r\n" \ + ' \r\n' \ + ' \r\n' \ + " \r\n" \ + "\r\n" + + expected = "no projects found in Visual Studio solution file" + + __test_project_error(tmpdir, "slnx", content, expected) + + +def test_slnx_project_file_not_found(tmpdir): + content = '\r\n' \ + "\r\n" \ + " \r\n" \ + ' \r\n' \ + ' \r\n' \ + " \r\n" \ + ' \r\n' \ + "\r\n" + + expected = "Visual Studio project file is not a valid XML - XML_ERROR_FILE_NOT_FOUND\n" \ + "cppcheck: error: failed to load '{}' from Visual Studio solution".format(os.path.join(tmpdir, "test.vcxproj")) + if sys.platform == "win32": + expected = expected.replace('\\', '/') + + __test_project_error(tmpdir, "slnx", content, expected) + + def test_vcxproj_no_xml_root(tmpdir): content = ''