Skip to content

Commit 5746270

Browse files
authored
fixed #13721 - Add support for Visual Studio 2026 .slnx project files (danmar#8355)
1 parent 57149b4 commit 5746270

File tree

11 files changed

+144
-19
lines changed

11 files changed

+144
-19
lines changed

.github/workflows/selfcheck.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ jobs:
121121
122122
- name: Self check (unusedFunction / no test / no gui)
123123
run: |
124-
supprs="--suppress=unusedFunction:lib/errorlogger.h:197 --suppress=unusedFunction:lib/importproject.cpp:1531 --suppress=unusedFunction:lib/importproject.cpp:1555"
124+
supprs="--suppress=unusedFunction:lib/errorlogger.h:197 --suppress=unusedFunction:lib/importproject.cpp:1584 --suppress=unusedFunction:lib/importproject.cpp:1608"
125125
./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
126126
env:
127127
DISABLE_VALUEFLOW: 1

cli/cmdlineparser.cpp

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,7 +1188,9 @@ CmdLineParser::Result CmdLineParser::parseFromArgs(int argc, const char* const a
11881188
}
11891189
if (projectType == ImportProject::Type::COMPILE_DB)
11901190
mSettings.maxConfigsProject = 1;
1191-
if (projectType == ImportProject::Type::VS_SLN || projectType == ImportProject::Type::VS_VCXPROJ) {
1191+
if (projectType == ImportProject::Type::VS_SLN ||
1192+
projectType == ImportProject::Type::VS_SLNX ||
1193+
projectType == ImportProject::Type::VS_VCXPROJ) {
11921194
mSettings.libraries.emplace_back("windows");
11931195
}
11941196
for (const auto &error : project.errors)
@@ -1214,7 +1216,9 @@ CmdLineParser::Result CmdLineParser::parseFromArgs(int argc, const char* const a
12141216
mLogger.printError("--project-configuration parameter is empty.");
12151217
return Result::Fail;
12161218
}
1217-
if (projectType != ImportProject::Type::VS_SLN && projectType != ImportProject::Type::VS_VCXPROJ) {
1219+
if (projectType != ImportProject::Type::VS_SLN &&
1220+
projectType != ImportProject::Type::VS_SLNX &&
1221+
projectType != ImportProject::Type::VS_VCXPROJ) {
12181222
mLogger.printError("--project-configuration has no effect - no Visual Studio project provided.");
12191223
return Result::Fail;
12201224
}
@@ -1659,7 +1663,9 @@ CmdLineParser::Result CmdLineParser::parseFromArgs(int argc, const char* const a
16591663
mSettings.platform.defaultSign = defaultSign;
16601664

16611665
if (!mSettings.analyzeAllVsConfigs) {
1662-
if (projectType != ImportProject::Type::VS_SLN && projectType != ImportProject::Type::VS_VCXPROJ) {
1666+
if (projectType != ImportProject::Type::VS_SLN &&
1667+
projectType != ImportProject::Type::VS_SLNX &&
1668+
projectType != ImportProject::Type::VS_VCXPROJ) {
16631669
if (mAnalyzeAllVsConfigsSetOnCmdLine) {
16641670
mLogger.printError("--no-analyze-all-vs-configs has no effect - no Visual Studio project provided.");
16651671
return Result::Fail;
@@ -1945,13 +1951,13 @@ void CmdLineParser::printHelp() const
19451951

19461952
oss <<
19471953
" --project=<file> Run Cppcheck on project. The <file> can be a Visual\n"
1948-
" Studio Solution (*.sln), Visual Studio Project\n"
1954+
" Studio Solution (*.sln) or (*.slnx), Visual Studio Project\n"
19491955
" (*.vcxproj), compile database (compile_commands.json),\n"
19501956
" or Borland C++ Builder 6 (*.bpr). The files to analyse,\n"
19511957
" include paths, defines, platform and undefines in\n"
19521958
" the specified file will be used.\n"
19531959
" --project-configuration=<config>\n"
1954-
" If used together with a Visual Studio Solution (*.sln)\n"
1960+
" If used together with a Visual Studio Solution (*.sln) or (*.slnx)\n"
19551961
" or Visual Studio Project (*.vcxproj) you can limit\n"
19561962
" the configuration cppcheck should check.\n"
19571963
" For example: '--project-configuration=Release|Win32'\n"

gui/mainwindow.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -768,7 +768,7 @@ QStringList MainWindow::selectFilesToAnalyze(QFileDialog::FileMode mode)
768768
QMap<QString,QString> filters;
769769
filters[tr("C/C++ Source")] = FileList::getDefaultFilters().join(" ");
770770
filters[tr("Compile database")] = compile_commands_json;
771-
filters[tr("Visual Studio")] = "*.sln *.vcxproj";
771+
filters[tr("Visual Studio")] = "*.sln *.slnx *.vcxproj";
772772
filters[tr("Borland C++ Builder 6")] = "*.bpr";
773773
QString lastFilter = mSettings->value(SETTINGS_LAST_ANALYZE_FILES_FILTER).toString();
774774
selected = QFileDialog::getOpenFileNames(this,
@@ -811,13 +811,14 @@ void MainWindow::analyzeFiles()
811811

812812
const QString file0 = (!selected.empty() ? selected[0].toLower() : QString());
813813
if (file0.endsWith(".sln")
814+
|| file0.endsWith(".slnx")
814815
|| file0.endsWith(".vcxproj")
815816
|| file0.endsWith(compile_commands_json)
816817
|| file0.endsWith(".bpr")) {
817818
ImportProject p;
818819
p.import(selected[0].toStdString());
819820

820-
if (file0.endsWith(".sln")) {
821+
if (file0.endsWith(".sln") || file0.endsWith(".slnx")) {
821822
QStringList configs;
822823
for (auto it = p.fileSettings.cbegin(); it != p.fileSettings.cend(); ++it) {
823824
const QString cfg(QString::fromStdString(it->cfg));
@@ -1968,6 +1969,7 @@ void MainWindow::analyzeProject(const ProjectFile *projectFile, const QStringLis
19681969
switch (result) {
19691970
case ImportProject::Type::COMPILE_DB:
19701971
case ImportProject::Type::VS_SLN:
1972+
case ImportProject::Type::VS_SLNX:
19711973
case ImportProject::Type::VS_VCXPROJ:
19721974
case ImportProject::Type::BORLAND:
19731975
case ImportProject::Type::CPPCHECK_GUI:

gui/projectfiledialog.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ static std::string suppressionAsText(const SuppressionList::Suppression& s)
111111

112112
QStringList ProjectFileDialog::getProjectConfigs(const QString &fileName)
113113
{
114-
if (!fileName.endsWith(".sln") && !fileName.endsWith(".vcxproj"))
114+
if (!fileName.endsWith(".sln") && !fileName.endsWith(".slnx") && !fileName.endsWith(".vcxproj"))
115115
return QStringList();
116116
QStringList ret;
117117
ImportProject importer;
@@ -596,7 +596,7 @@ void ProjectFileDialog::updatePathsAndDefines()
596596
{
597597
const QString &fileName = mUI->mEditImportProject->text();
598598
const bool importProject = !fileName.isEmpty();
599-
const bool hasConfigs = fileName.endsWith(".sln") || fileName.endsWith(".vcxproj");
599+
const bool hasConfigs = fileName.endsWith(".sln") || fileName.endsWith(".slnx") || fileName.endsWith(".vcxproj");
600600
mUI->mBtnClearImportProject->setEnabled(importProject);
601601
mUI->mListCheckPaths->setEnabled(!importProject);
602602
mUI->mListIncludeDirs->setEnabled(!importProject);
@@ -626,7 +626,7 @@ void ProjectFileDialog::browseImportProject()
626626
const QFileInfo inf(mProjectFile->getFilename());
627627
const QDir &dir = inf.absoluteDir();
628628
QMap<QString,QString> filters;
629-
filters[tr("Visual Studio")] = "*.sln *.vcxproj";
629+
filters[tr("Visual Studio")] = "*.sln *.slnx *.vcxproj";
630630
filters[tr("Compile database")] = "compile_commands.json";
631631
filters[tr("Borland C++ Builder 6")] = "*.bpr";
632632
QString fileName = QFileDialog::getOpenFileName(this, tr("Import Project"),

lib/importproject.cpp

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,11 @@ ImportProject::Type ImportProject::import(const std::string &filename, Settings
330330
setRelativePaths(filename);
331331
return ImportProject::Type::VS_SLN;
332332
}
333+
} else if (endsWith(filename, ".slnx")) {
334+
if (importSlnx(filename, fileFilters)) {
335+
setRelativePaths(filename);
336+
return ImportProject::Type::VS_SLNX;
337+
}
333338
} else if (endsWith(filename, ".vcxproj")) {
334339
std::map<std::string, std::string, cppcheck::stricmp> variables;
335340
std::vector<SharedItemsProject> sharedItemsProjects;
@@ -503,6 +508,54 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const
503508
return true;
504509
}
505510

511+
bool ImportProject::importSlnx(const std::string& filename, const std::vector<std::string>& fileFilters)
512+
{
513+
tinyxml2::XMLDocument doc;
514+
const tinyxml2::XMLError error = doc.LoadFile(filename.c_str());
515+
if (error != tinyxml2::XML_SUCCESS) {
516+
errors.emplace_back(std::string("Visual Studio solution file is not a valid XML - ") + tinyxml2::XMLDocument::ErrorIDToName(error));
517+
return false;
518+
}
519+
520+
const tinyxml2::XMLElement* const rootnode = doc.FirstChildElement();
521+
if (rootnode == nullptr) {
522+
errors.emplace_back("Visual Studio solution file has no XML root node");
523+
return false;
524+
}
525+
526+
std::map<std::string, std::string, cppcheck::stricmp> variables;
527+
variables["SolutionDir"] = Path::simplifyPath(Path::getPathFromFilename(filename));
528+
529+
bool found = false;
530+
std::vector<SharedItemsProject> sharedItemsProjects;
531+
532+
for (const tinyxml2::XMLElement* node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) {
533+
const char* name = node->Name();
534+
if (std::strcmp(name, "Project") == 0) {
535+
const char* labelAttribute = node->Attribute("Path");
536+
if (labelAttribute) {
537+
std::string vcxproj(labelAttribute);
538+
vcxproj = Path::toNativeSeparators(std::move(vcxproj));
539+
if (!Path::isAbsolute(vcxproj))
540+
vcxproj = variables["SolutionDir"] + vcxproj;
541+
vcxproj = Path::fromNativeSeparators(std::move(vcxproj));
542+
if (!importVcxproj(vcxproj, variables, "", fileFilters, sharedItemsProjects)) {
543+
errors.emplace_back("failed to load '" + vcxproj + "' from Visual Studio solution");
544+
return false;
545+
}
546+
found = true;
547+
}
548+
}
549+
}
550+
551+
if (!found) {
552+
errors.emplace_back("no projects found in Visual Studio solution file");
553+
return false;
554+
}
555+
556+
return true;
557+
}
558+
506559
namespace {
507560
struct ProjectConfiguration {
508561
explicit ProjectConfiguration(const tinyxml2::XMLElement *cfg) {

lib/importproject.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class CPPCHECKLIB WARN_UNUSED ImportProject {
6363
FAILURE,
6464
COMPILE_DB,
6565
VS_SLN,
66+
VS_SLNX,
6667
VS_VCXPROJ,
6768
BORLAND,
6869
CPPCHECK_GUI
@@ -120,6 +121,7 @@ class CPPCHECKLIB WARN_UNUSED ImportProject {
120121
void setRelativePaths(const std::string &filename);
121122

122123
bool importSln(std::istream &istr, const std::string &path, const std::vector<std::string> &fileFilters);
124+
bool importSlnx(const std::string& filename, const std::vector<std::string>& fileFilters);
123125
SharedItemsProject importVcxitems(const std::string &filename, const std::vector<std::string> &fileFilters, std::vector<SharedItemsProject> &cache);
124126
bool importBcb6Prj(const std::string &projectFilename);
125127

man/manual.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,12 +309,16 @@ To ignore certain folders you can use `-i`. This will skip analysis of source fi
309309

310310
## Visual Studio
311311

312-
You can run Cppcheck on individual project files (\*.vcxproj) or on a whole solution (\*.sln)
312+
You can run Cppcheck on individual project files (\*.vcxproj) or on a whole solution (\*.sln) or (\*.slnx)
313313

314314
Running Cppcheck on an entire Visual Studio solution:
315315

316316
cppcheck --project=foobar.sln
317317

318+
Running Cppcheck on an entire Visual Studio 2026 solution:
319+
320+
cppcheck --project=foobar.slnx
321+
318322
Running Cppcheck on a Visual Studio project:
319323

320324
cppcheck --project=foobar.vcxproj
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Solution>
3+
<Configurations>
4+
<Platform Name="x64" />
5+
<Platform Name="x86" />
6+
</Configurations>
7+
<Project Path="helloworld.vcxproj" />
8+
</Solution>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project version="1">
3+
<root name="."/>
4+
<importproject>helloworld.slnx</importproject>
5+
<analyze-all-vs-configs>false</analyze-all-vs-configs>
6+
</project>

test/cli/helloworld_test.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,23 +223,25 @@ def test_cppcheck_project_local_path_select_one_multiple():
223223
def test_cppcheck_project_local_path_analyze_all():
224224
__test_cppcheck_project_local_path(['--analyze-all-vs-configs'], 'Debug|Win32 Debug|x64 Release|Win32 Release|x64')
225225

226-
def test_cppcheck_project_relative_path():
226+
@pytest.mark.parametrize("project_file", ["helloworld.cppcheck", "helloworld_slnx.cppcheck"])
227+
def test_cppcheck_project_relative_path(project_file):
227228
args = [
228229
'--template=cppcheck1',
229230
'--platform=win64',
230-
'--project=' + os.path.join('helloworld', 'helloworld.cppcheck')
231+
'--project=' + os.path.join('helloworld', project_file)
231232
]
232233
ret, stdout, stderr = cppcheck(args, cwd=__script_dir)
233234
filename = os.path.join('helloworld', 'main.c')
234235
assert ret == 0, stdout
235236
assert __getVsConfigs(stdout, filename) == 'Debug|x64'
236237
assert stderr == '[%s:5]: (error) Division by zero.\n' % filename
237238

238-
def test_cppcheck_project_absolute_path():
239+
@pytest.mark.parametrize("project_file", ["helloworld.cppcheck", "helloworld_slnx.cppcheck"])
240+
def test_cppcheck_project_absolute_path(project_file):
239241
args = [
240242
'--template=cppcheck1',
241243
'--platform=win64',
242-
'--project=' + os.path.join(__proj_dir, 'helloworld.cppcheck')
244+
'--project=' + os.path.join(__proj_dir, project_file)
243245
]
244246
ret, stdout, stderr = cppcheck(args)
245247
filename = os.path.join(__proj_dir, 'main.c')
@@ -296,11 +298,12 @@ def test_suppress_project_absolute(tmp_path):
296298
assert ret == 0, stdout
297299
assert stderr == ''
298300

299-
def test_exclude():
301+
@pytest.mark.parametrize("project_file", ["helloworld.cppcheck", "helloworld_slnx.cppcheck"])
302+
def test_exclude(project_file):
300303
args = [
301304
'-i' + 'helloworld',
302305
'--platform=win64',
303-
'--project=' + os.path.join('helloworld', 'helloworld.cppcheck')
306+
'--project=' + os.path.join('helloworld', project_file)
304307
]
305308
ret, stdout, _ = cppcheck(args, cwd=__script_dir)
306309
assert ret == 1

0 commit comments

Comments
 (0)