From 60d9d147b26834b1e3f69f68587f4fa83336b974 Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Sat, 10 Jan 2026 12:07:33 -0500 Subject: [PATCH 1/5] Add suport for Visual Studio 2026 slnx importing. --- cli/cmdlineparser.cpp | 6 ++--- gui/mainwindow.cpp | 1 + gui/projectfiledialog.cpp | 6 ++--- lib/importproject.cpp | 54 +++++++++++++++++++++++++++++++++++++++ lib/importproject.h | 2 ++ 5 files changed, 63 insertions(+), 6 deletions(-) diff --git a/cli/cmdlineparser.cpp b/cli/cmdlineparser.cpp index 47759b75196..4db871dde55 100644 --- a/cli/cmdlineparser.cpp +++ b/cli/cmdlineparser.cpp @@ -1184,7 +1184,7 @@ 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) @@ -1210,7 +1210,7 @@ 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_SLN && projectType != ImportProject::Type::VS_VCXPROJ) { mLogger.printError("--project-configuration has no effect - no Visual Studio project provided."); return Result::Fail; } @@ -1645,7 +1645,7 @@ 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; diff --git a/gui/mainwindow.cpp b/gui/mainwindow.cpp index 6926183974b..d4c89178934 100644 --- a/gui/mainwindow.cpp +++ b/gui/mainwindow.cpp @@ -1969,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 bfa351722ec..55daf0948b8 100644 --- a/gui/projectfiledialog.cpp +++ b/gui/projectfiledialog.cpp @@ -112,7 +112,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; @@ -612,7 +612,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); @@ -642,7 +642,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 acd7842224f..30c7f16503d 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -187,6 +187,12 @@ 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; @@ -488,6 +494,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 project 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 project 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 45d17a819f7..ddf46902d29 100644 --- a/lib/importproject.h +++ b/lib/importproject.h @@ -64,6 +64,7 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { FAILURE, COMPILE_DB, VS_SLN, + VS_SLNX, VS_VCXPROJ, BORLAND, CPPCHECK_GUI @@ -113,6 +114,7 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { }; 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 importVcxproj(const std::string &filename, std::map &variables, const std::string &additionalIncludeDirectories, const std::vector &fileFilters, std::vector &cache); bool importVcxproj(const std::string &filename, const tinyxml2::XMLDocument &doc, std::map &variables, const std::string &additionalIncludeDirectories, const std::vector &fileFilters, std::vector &cache); From bd1f45d242bbba5178299657a5e2c24430f324b8 Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Sat, 10 Jan 2026 18:34:15 -0500 Subject: [PATCH 2/5] Fix a cut and paste error and a missing file sxtension check and update help. --- cli/cmdlineparser.cpp | 6 +++--- gui/mainwindow.cpp | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cli/cmdlineparser.cpp b/cli/cmdlineparser.cpp index 4db871dde55..97f1c9c72d7 100644 --- a/cli/cmdlineparser.cpp +++ b/cli/cmdlineparser.cpp @@ -1210,7 +1210,7 @@ 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_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; } @@ -1931,13 +1931,13 @@ void CmdLineParser::printHelp(bool premium) 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 d4c89178934..8561fb79086 100644 --- a/gui/mainwindow.cpp +++ b/gui/mainwindow.cpp @@ -767,7 +767,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, @@ -810,6 +810,7 @@ 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")) { From 100cc8ad9158412ded1d3459ea8b9051b9beb2c7 Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Sat, 10 Jan 2026 19:46:11 -0500 Subject: [PATCH 3/5] add python helloworld2 test --- test/cli/helloworld2/helloworld2.cppcheck | 6 + test/cli/helloworld2/helloworld2.slnx | 7 + test/cli/helloworld2/helloworld2.vcxproj | 123 +++++++ test/cli/helloworld2/main.c | 11 + test/cli/helloworld2_test.py | 394 ++++++++++++++++++++++ 5 files changed, 541 insertions(+) create mode 100644 test/cli/helloworld2/helloworld2.cppcheck create mode 100644 test/cli/helloworld2/helloworld2.slnx create mode 100644 test/cli/helloworld2/helloworld2.vcxproj create mode 100644 test/cli/helloworld2/main.c create mode 100644 test/cli/helloworld2_test.py diff --git a/test/cli/helloworld2/helloworld2.cppcheck b/test/cli/helloworld2/helloworld2.cppcheck new file mode 100644 index 00000000000..23e05c5db09 --- /dev/null +++ b/test/cli/helloworld2/helloworld2.cppcheck @@ -0,0 +1,6 @@ + + + + helloworld2.slnx + false + diff --git a/test/cli/helloworld2/helloworld2.slnx b/test/cli/helloworld2/helloworld2.slnx new file mode 100644 index 00000000000..9ddfb399c43 --- /dev/null +++ b/test/cli/helloworld2/helloworld2.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/cli/helloworld2/helloworld2.vcxproj b/test/cli/helloworld2/helloworld2.vcxproj new file mode 100644 index 00000000000..8b863be69a8 --- /dev/null +++ b/test/cli/helloworld2/helloworld2.vcxproj @@ -0,0 +1,123 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {7319858B-261C-4F0D-B022-92BB896242DD} + helloworld2 + 10.0.16299.0 + + + + Application + true + v141 + MultiByte + + + Application + false + v141 + true + MultiByte + + + Application + true + v141 + MultiByte + + + Application + false + v141 + true + MultiByte + + + + + + + + + + + + + + + + + + + + + + + Level3 + Disabled + true + true + + + + + Level3 + Disabled + true + true + + + + + Level3 + MaxSpeed + true + true + true + true + + + true + true + + + + + Level3 + MaxSpeed + true + true + true + true + + + true + true + + + + + + + + + \ No newline at end of file diff --git a/test/cli/helloworld2/main.c b/test/cli/helloworld2/main.c new file mode 100644 index 00000000000..c5b94db7aa0 --- /dev/null +++ b/test/cli/helloworld2/main.c @@ -0,0 +1,11 @@ +#include + +int main(void) { + (void)printf("Hello world!\n"); + int x = 3 / 0; (void)x; // ERROR + return 0; +} + +#ifdef SOME_CONFIG +void foo(); +#endif diff --git a/test/cli/helloworld2_test.py b/test/cli/helloworld2_test.py new file mode 100644 index 00000000000..39a8cdf3228 --- /dev/null +++ b/test/cli/helloworld2_test.py @@ -0,0 +1,394 @@ + +# python -m pytest helloworld2_test.py + +import os +import re +import glob +import json +import shutil +import xml.etree.ElementTree as ET + +import pytest + +from testutils import create_gui_project_file, cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) +__proj_dir = os.path.join(__script_dir, 'helloworld2') + + +# Get Visual Studio configurations checking a file +# Checking {file} {config}... +def __getVsConfigs(stdout, filename): + ret = [] + for line in stdout.split('\n'): + if not line.startswith('Checking %s ' % filename): + continue + if not line.endswith('...'): + continue + res = re.match(r'.* ([A-Za-z0-9|]+)...', line) + if res: + ret.append(res.group(1)) + ret.sort() + return ' '.join(ret) + +def test_relative_path(): + args = [ + '--template=cppcheck1', + 'helloworld2' + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + filename = os.path.join('helloworld2', 'main.c') + assert ret == 0, stdout + assert stderr == '[%s:5]: (error) Division by zero.\n' % filename + + +def test_local_path(): + args = [ + '--template=cppcheck1', + '.' + ] + ret, stdout, stderr = cppcheck(args, cwd=__proj_dir) + assert ret == 0, stdout + assert stderr == '[main.c:5]: (error) Division by zero.\n' + +def test_absolute_path(): + args = [ + '--template=cppcheck1', + __proj_dir + ] + ret, stdout, stderr = cppcheck(args) + filename = os.path.join(__proj_dir, 'main.c') + assert ret == 0, stdout + assert stderr == '[%s:5]: (error) Division by zero.\n' % filename + +def test_addon_local_path(): + args = [ + '--addon=misra', + '--enable=style', + '--template=cppcheck1', + '.' + ] + ret, stdout, stderr = cppcheck(args, cwd=__proj_dir) + assert ret == 0, stdout + assert stderr == ('[main.c:5]: (error) Division by zero.\n' + '[main.c:4]: (style) misra violation (use --rule-texts= to get proper output)\n') + +def test_addon_local_path_not_enable(): + args = [ + '--addon=misra', + '--template=cppcheck1', + '.' + ] + ret, stdout, stderr = cppcheck(args, cwd=__proj_dir) + assert ret == 0, stdout + assert stderr == '[main.c:5]: (error) Division by zero.\n' + +def test_addon_absolute_path(): + args = [ + '--addon=misra', + '--enable=style', + '--template=cppcheck1', + __proj_dir + ] + ret, stdout, stderr = cppcheck(args) + filename = os.path.join(__proj_dir, 'main.c') + assert ret == 0, stdout + assert stderr == ('[%s:5]: (error) Division by zero.\n' + '[%s:4]: (style) misra violation (use --rule-texts= to get proper output)\n' % (filename, filename)) + +def test_addon_relative_path(): + args = [ + '--addon=misra', + '--enable=style', + '--template=cppcheck1', + 'helloworld2' + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + filename = os.path.join('helloworld2', 'main.c') + assert ret == 0, stdout + assert stdout == ('Checking %s ...\n' + 'Checking %s: SOME_CONFIG...\n' % (filename, filename)) + assert stderr == ('[%s:5]: (error) Division by zero.\n' + '[%s:4]: (style) misra violation (use --rule-texts= to get proper output)\n' % (filename, filename)) + +def test_addon_with_gui_project(tmp_path): + shutil.copytree(os.path.join(__script_dir, 'helloworld2'), tmp_path / 'helloworld2') + project_file = os.path.join('helloworld2', 'test.cppcheck') + create_gui_project_file(tmp_path / project_file, paths=['.'], addon='misra') + args = [ + '--template=cppcheck1', + '--enable=style', + '--project={}'.format(project_file) + ] + ret, stdout, stderr = cppcheck(args, cwd=tmp_path) + filename = os.path.join('helloworld2', 'main.c') + assert ret == 0, stdout + assert stdout.strip().split('\n') == [ + 'Checking %s ...' % filename, + 'Checking %s: SOME_CONFIG...' % filename + ] + assert stderr == ('[%s:5]: (error) Division by zero.\n' + '[%s:4]: (style) misra violation (use --rule-texts= to get proper output)\n' % (filename, filename)) + +def test_basepath_relative_path(): + args = [ + 'helloworld2', + '--template=cppcheck1', + '-rp=helloworld2' + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + assert stderr == '[main.c:5]: (error) Division by zero.\n' + +def test_basepath_absolute_path(): + args = [ + '--template=cppcheck1', + __proj_dir, + '-rp=' + __proj_dir + ] + ret, stdout, stderr = cppcheck(args) + assert ret == 0, stdout + assert stderr == '[main.c:5]: (error) Division by zero.\n' + +def __test_vs_project_local_path(extra_args=None, exp_vs_cfg='Debug|Win32 Debug|x64 Release|Win32 Release|x64'): + args = [ + '--template=cppcheck1', + '--project=helloworld2.vcxproj' + ] + if extra_args: + args += extra_args + ret, stdout, stderr = cppcheck(args, cwd=__proj_dir) + assert ret == 0, stdout + assert __getVsConfigs(stdout, 'main.c') == exp_vs_cfg + assert stderr == '[main.c:5]: (error) Division by zero.\n' + +def test_vs_project_local_path(): + __test_vs_project_local_path() + +def test_vs_project_local_path_select_one(): + __test_vs_project_local_path(['--project-configuration=Release|Win32'], 'Release|Win32') + +def test_vs_project_local_path_select_one_multiple(): + __test_vs_project_local_path(['--project-configuration=Debug|Win32', '--project-configuration=Release|Win32'], 'Release|Win32') + +def test_vs_project_local_path_no_analyze_all(): + __test_vs_project_local_path(['--no-analyze-all-vs-configs'], 'Debug|Win32') + +def test_vs_project_relative_path(): + args = [ + '--template=cppcheck1', + '--project=' + os.path.join(__proj_dir, 'helloworld2.vcxproj') + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + filename = os.path.join(__proj_dir, 'main.c') + assert ret == 0, stdout + assert __getVsConfigs(stdout, filename) == 'Debug|Win32 Debug|x64 Release|Win32 Release|x64' + assert stderr == '[%s:5]: (error) Division by zero.\n' % filename + +def test_vs_project_absolute_path(): + args = [ + '--template=cppcheck1', + '--project=' + os.path.join(__proj_dir, 'helloworld2.vcxproj') + ] + ret, stdout, stderr = cppcheck(args) + filename = os.path.join(__proj_dir, 'main.c') + assert ret == 0, stdout + assert __getVsConfigs(stdout, filename) == 'Debug|Win32 Debug|x64 Release|Win32 Release|x64' + assert stderr == '[%s:5]: (error) Division by zero.\n' % filename + +def __test_cppcheck_project_local_path(extra_args=None, exp_vs_cfg='Debug|x64'): + args = [ + '--template=cppcheck1', + '--platform=win64', + '--project=helloworld2.cppcheck' + ] + if extra_args: + args += extra_args + ret, stdout, stderr = cppcheck(args, cwd=__proj_dir) + assert ret == 0, stdout + assert __getVsConfigs(stdout, 'main.c') == exp_vs_cfg + assert stderr == '[main.c:5]: (error) Division by zero.\n' + +def test_cppcheck_project_local_path(): + __test_cppcheck_project_local_path() + +@pytest.mark.xfail # TODO: no source files found +def test_cppcheck_project_local_path_select_one(): + __test_cppcheck_project_local_path(['--project-configuration=Release|Win32'], 'Release|Win32') + +@pytest.mark.xfail # TODO: no source files found +def test_cppcheck_project_local_path_select_one_multiple(): + __test_cppcheck_project_local_path(['--project-configuration=Debug|Win32', '--project-configuration=Release|Win32'], 'Release|Win32') + +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(): + args = [ + '--template=cppcheck1', + '--platform=win64', + '--project=' + os.path.join('helloworld2', 'helloworld2.cppcheck') + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + filename = os.path.join('helloworld2', 'main.c') + assert ret == 0, stdout + assert __getVsConfigs(stdout, filename) == 'Debug|x64' + assert stderr == '[%s:5]: (error) Division by zero.\n' % filename + +def test_cppcheck_project_absolute_path(): + args = [ + '--template=cppcheck1', + '--platform=win64', + '--project=' + os.path.join(__proj_dir, 'helloworld2.cppcheck') + ] + ret, stdout, stderr = cppcheck(args) + filename = os.path.join(__proj_dir, 'main.c') + assert ret == 0, stdout + assert __getVsConfigs(stdout, filename) == 'Debug|x64' + assert stderr == '[%s:5]: (error) Division by zero.\n' % filename + +def test_suppress_command_line_related(): + args = [ + '--suppress=zerodiv:' + os.path.join('helloworld2', 'main.c'), + 'helloworld2' + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + assert stderr == '' + +def test_suppress_command_line_absolute(): + args = [ + '--suppress=zerodiv:' + os.path.join(__proj_dir, 'main.c'), + __proj_dir + ] + ret, stdout, stderr = cppcheck(args) + assert ret == 0, stdout + assert stderr == '' + +def test_suppress_project_relative(tmp_path): + shutil.copytree(os.path.join(__script_dir, 'helloworld2'), tmp_path / 'helloworld2') + project_file = os.path.join('helloworld2', 'test.cppcheck') + create_gui_project_file(tmp_path / project_file, + paths=['.'], + suppressions=[{'fileName':'main.c', 'id':'zerodiv'}]) + + args = [ + '--project={}'.format(project_file) + ] + + ret, stdout, stderr = cppcheck(args, cwd=tmp_path) + assert ret == 0, stdout + assert stderr == '' + + +def test_suppress_project_absolute(tmp_path): + shutil.copytree(os.path.join(__script_dir, 'helloworld2'), tmp_path / 'helloworld2') + project_file = tmp_path / 'helloworld2' / 'test.cppcheck' + create_gui_project_file(project_file, + paths=['.'], + suppressions=[{'fileName':'main.c', 'id':'zerodiv'}]) + + args = [ + '--project={}'.format(project_file) + ] + + ret, stdout, stderr = cppcheck(args) + assert ret == 0, stdout + assert stderr == '' + +def test_exclude(): + args = [ + '-i' + 'helloworld2', + '--platform=win64', + '--project=' + os.path.join('helloworld2', 'helloworld2.cppcheck') + ] + ret, stdout, _ = cppcheck(args, cwd=__script_dir) + assert ret == 1 + lines = stdout.splitlines() + assert lines == [ + 'cppcheck: error: no C or C++ source files found.', + 'cppcheck: all paths were ignored' + ] + + +def test_build_dir_dump_output(tmpdir): + args = [ + f'--cppcheck-build-dir={tmpdir}', + '--addon=misra', + 'helloworld2' + ] + + cppcheck(args) + cppcheck(args) + + filename = f'{tmpdir}/main.a1.*.dump' + filelist = glob.glob(filename) + assert(len(filelist) == 0) + + +def test_checkers_report(tmpdir): + filename = os.path.join(tmpdir, '1.txt') + args = [ + f'--checkers-report={filename}', + '--no-cppcheck-build-dir', # TODO: remove this + 'helloworld2' + ] + + cppcheck(args, cwd=__script_dir) + + with open(filename, 'rt') as f: + data = f.read().splitlines() + assert 'No CheckAutoVariables::assignFunctionArg require:style,warning' in data, json.dumps(data, indent=4) + assert 'Yes CheckAutoVariables::autoVariables' in data, json.dumps(data, indent=4) + + args += [ + '--enable=style' + ] + cppcheck(args, cwd=__script_dir) + with open(filename, 'rt') as f: + data = f.read().splitlines() + # checker has been activated by --enable=style + assert 'Yes CheckAutoVariables::assignFunctionArg' in data, json.dumps(data, indent=4) + + +def test_missing_include_system(): # #11283 + args = [ + '--enable=missingInclude', + '--suppress=zerodiv', + '--template=simple', + 'helloworld2' + ] + + _, _, stderr = cppcheck(args, cwd=__script_dir) + assert stderr.replace('\\', '/') == 'helloworld2/main.c:1:2: information: Include file: not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]\n' + + +def test_sarif(): + args = [ + '--output-format=sarif', + 'helloworld2' + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + res = json.loads(stderr) + assert res['version'] == '2.1.0' + assert res['runs'][0]['results'][0]['locations'][0]['physicalLocation']['artifactLocation']['uri'] == 'helloworld2/main.c' + assert res['runs'][0]['results'][0]['ruleId'] == 'zerodiv' + assert res['runs'][0]['tool']['driver']['rules'][0]['id'] == 'zerodiv' + assert res['runs'][0]['tool']['driver']['rules'][0]['properties']['precision'] == 'high' + assert res['runs'][0]['tool']['driver']['rules'][0]['properties']['security-severity'] == '9.9' + assert 'security' in res['runs'][0]['tool']['driver']['rules'][0]['properties']['tags'] + assert re.match(r'[0-9]+(.[0-9]+)+', res['runs'][0]['tool']['driver']['semanticVersion']) + assert 'level' in res['runs'][0]['tool']['driver']['rules'][0]['defaultConfiguration'] # #13885 + assert res['runs'][0]['tool']['driver']['rules'][0]['shortDescription']['text'] == '' + assert res['runs'][0]['results'][0]['message']['text'] == 'Division by zero.' + assert res['runs'][0]['tool']['driver']['rules'][0]['properties']['problem.severity'] == 'error' + + +def test_xml_checkers_report(): + test_file = os.path.join(__proj_dir, 'main.c') + args = ['--xml-version=3', '--enable=all', test_file] + + exitcode, _, stderr = cppcheck(args) + assert exitcode == 0 + assert ET.fromstring(stderr) is not None From ed8294eb5f7bfc3939d21764c76c4788a39e1346 Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Sat, 10 Jan 2026 19:55:44 -0500 Subject: [PATCH 4/5] Update manual for Visual Studio 2026 .slnx projects --- man/manual.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/man/manual.md b/man/manual.md index d8da119f388..7aa36132655 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 From 4873886e0bbc7fbc90cffd2a82e4ef38993c10e2 Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Sun, 11 Jan 2026 17:09:08 -0500 Subject: [PATCH 5/5] Fix opening from Analyze -> Files... --- gui/mainwindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/mainwindow.cpp b/gui/mainwindow.cpp index 8561fb79086..eb06d07a905 100644 --- a/gui/mainwindow.cpp +++ b/gui/mainwindow.cpp @@ -817,7 +817,7 @@ void MainWindow::analyzeFiles() 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));