Skip to content

Commit f2d2293

Browse files
danmarLeander Schulten
authored andcommitted
Fix #14334 (Support more msbuild conditional constructs)
1 parent 15424d0 commit f2d2293

4 files changed

Lines changed: 132 additions & 33 deletions

File tree

.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:196 --suppress=unusedFunction:lib/importproject.cpp:1673 --suppress=unusedFunction:lib/importproject.cpp:1697"
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

lib/importproject.cpp

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const
505505

506506
namespace {
507507
struct ProjectConfiguration {
508+
ProjectConfiguration() = default;
508509
explicit ProjectConfiguration(const tinyxml2::XMLElement *cfg) {
509510
const char *a = cfg->Attribute("Include");
510511
if (a)
@@ -550,10 +551,14 @@ namespace {
550551

551552
// see https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-conditions
552553
// properties are .NET String objects and you can call any of its members on them
553-
bool conditionIsTrue(const ProjectConfiguration &p) const {
554-
if (mCondition.empty())
554+
bool conditionIsTrue(const ProjectConfiguration &p, const std::string &filename, std::vector<std::string> &errors) const {
555+
return conditionIsTrue(mCondition, p, filename, errors);
556+
}
557+
558+
static bool conditionIsTrue(const std::string& condition, const ProjectConfiguration &p, const std::string &filename, std::vector<std::string> &errors) {
559+
if (condition.empty())
555560
return true;
556-
std::string c = '(' + mCondition + ");";
561+
std::string c = '(' + condition + ");";
557562
replaceAll(c, "$(Configuration)", p.configuration);
558563
replaceAll(c, "$(Platform)", p.platformStr);
559564

@@ -576,19 +581,83 @@ namespace {
576581
}
577582
}
578583
}
584+
585+
// Replace "And" and "Or" with "&&" and "||"
586+
for (Token *tok = tokenlist.front(); tok; tok = tok->next()) {
587+
if (tok->str() == "And")
588+
tok->str("&&");
589+
else if (tok->str() == "Or")
590+
tok->str("||");
591+
}
592+
579593
tokenlist.createAst();
594+
595+
// Locate ast top and execute the condition
580596
for (const Token *tok = tokenlist.front(); tok; tok = tok->next()) {
581-
if (tok->str() == "(" && tok->astOperand1() && tok->astOperand2()) {
582-
// TODO: this is wrong - it is Contains() not Equals()
583-
if (tok->astOperand1()->expressionString() == "Configuration.Contains")
584-
return ('\'' + p.configuration + '\'') == tok->astOperand2()->str();
597+
if (tok->astParent()) {
598+
return execute(tok->astTop(), p) == "True";
585599
}
586-
if (tok->str() == "==" && tok->astOperand1() && tok->astOperand2() && tok->astOperand1()->str() == tok->astOperand2()->str())
587-
return true;
588600
}
589-
return false;
601+
602+
throw std::runtime_error("Invalid condition: '" + condition + "'");
590603
}
591604
private:
605+
606+
static std::string executeOp1(const Token* tok, const ProjectConfiguration &p, bool b=false) {
607+
const std::string result = execute(tok->astOperand1(), p);
608+
if (b)
609+
return (result != "False" && !result.empty()) ? "True" : "False";
610+
return result;
611+
}
612+
613+
static std::string executeOp2(const Token* tok, const ProjectConfiguration &p, bool b=false) {
614+
const std::string result = execute(tok->astOperand2(), p);
615+
if (b)
616+
return (result != "False" && !result.empty()) ? "True" : "False";
617+
return result;
618+
}
619+
620+
static std::string execute(const Token* tok, const ProjectConfiguration &p) {
621+
if (!tok)
622+
throw std::runtime_error("Missing operator");
623+
auto boolResult = [](bool b) -> std::string { return b ? "True" : "False"; };
624+
if (tok->isUnaryOp("!"))
625+
return boolResult(executeOp1(tok, p, true) == "False");
626+
if (tok->str() == "==")
627+
return boolResult(executeOp1(tok, p) == executeOp2(tok, p));
628+
if (tok->str() == "!=")
629+
return boolResult(executeOp1(tok, p) != executeOp2(tok, p));
630+
if (tok->str() == "&&")
631+
return boolResult(executeOp1(tok, p, true) == "True" && executeOp2(tok, p, true) == "True");
632+
if (tok->str() == "||")
633+
return boolResult(executeOp1(tok, p, true) == "True" || executeOp2(tok, p, true) == "True");
634+
if (tok->str() == "(" && Token::Match(tok->previous(), "$ ( %name% . %name% (")) {
635+
const std::string propertyName = tok->next()->str();
636+
std::string propertyValue;
637+
if (propertyName == "Configuration")
638+
propertyValue = p.configuration;
639+
else if (propertyName == "Platform")
640+
propertyValue = p.platform;
641+
else
642+
throw std::runtime_error("Unhandled property '" + propertyName + "'");
643+
const std::string method = tok->strAt(3);
644+
std::string arg = executeOp2(tok->tokAt(4), p);
645+
if (arg.size() >= 2 && arg[0] == '\'')
646+
arg = arg.substr(1, arg.size() - 2);
647+
if (method == "Contains")
648+
return boolResult(propertyValue.find(arg) != std::string::npos);
649+
if (method == "EndsWith")
650+
return boolResult(endsWith(propertyValue,arg.c_str(),arg.size()));
651+
if (method == "StartsWith")
652+
return boolResult(startsWith(propertyValue,arg));
653+
throw std::runtime_error("Unhandled method '" + method + "'");
654+
}
655+
if (tok->str().size() >= 2 && tok->str()[0] == '\'')
656+
return tok->str();
657+
658+
throw std::runtime_error("Unknown/unhandled operator/operand '" + tok->str() + "'");
659+
}
660+
592661
std::string mCondition;
593662
};
594663

@@ -894,7 +963,7 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X
894963
}
895964
std::string additionalIncludePaths;
896965
for (const ItemDefinitionGroup &i : itemDefinitionGroupList) {
897-
if (!i.conditionIsTrue(p))
966+
if (!i.conditionIsTrue(p, cfilename, errors))
898967
continue;
899968
fs.standard = Standards::getCPP(i.cppstd);
900969
fs.defines += ';' + i.preprocessorDefinitions;
@@ -912,7 +981,7 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X
912981
}
913982
bool useUnicode = false;
914983
for (const ConfigurationPropertyGroup &c : configurationPropertyGroups) {
915-
if (!c.conditionIsTrue(p))
984+
if (!c.conditionIsTrue(p, cfilename, errors))
916985
continue;
917986
// in msbuild the last definition wins
918987
useUnicode = c.useUnicode;
@@ -1569,3 +1638,14 @@ void ImportProject::setRelativePaths(const std::string &filename)
15691638
}
15701639
}
15711640

1641+
// only used by tests (testimportproject.cpp::testVcxprojConditions):
1642+
// cppcheck-suppress unusedFunction
1643+
bool cppcheck::testing::evaluateVcxprojCondition(const std::string& condition, const std::string& configuration,
1644+
const std::string& platform)
1645+
{
1646+
ProjectConfiguration p;
1647+
p.configuration = configuration;
1648+
p.platformStr = platform;
1649+
std::vector<std::string> errors;
1650+
return ConditionalGroup::conditionIsTrue(condition, p, "file.vcxproj", errors) && errors.empty();
1651+
}

lib/importproject.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ namespace cppcheck {
4949
return caseInsensitiveStringCompare(lhs,rhs) < 0;
5050
}
5151
};
52+
53+
namespace testing
54+
{
55+
CPPCHECKLIB bool evaluateVcxprojCondition(const std::string& condition, const std::string& configuration, const std::string& platform);
56+
}
5257
}
5358

5459
/**
@@ -194,6 +199,10 @@ namespace CppcheckXml {
194199
static constexpr char ProjectNameElementName[] = "project-name";
195200
}
196201

202+
namespace testing
203+
{
204+
CPPCHECKLIB bool evaluateVcxprojCondition(const std::string& condition, const std::string& configuration, const std::string& platform);
205+
}
197206
/// @}
198207
//---------------------------------------------------------------------------
199208
#endif // importprojectH

test/testimportproject.cpp

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include <list>
2929
#include <map>
3030
#include <sstream>
31+
#include <stdexcept>
3132
#include <string>
3233
#include <utility>
3334
#include <vector>
@@ -85,6 +86,7 @@ class TestImportProject : public TestFixture {
8586
TEST_CASE(testCollectArgs5);
8687
TEST_CASE(testCollectArgs6);
8788
TEST_CASE(testCollectArgs7);
89+
TEST_CASE(testVcxprojConditions);
8890
}
8991

9092
void setDefines() const {
@@ -682,28 +684,36 @@ class TestImportProject : public TestFixture {
682684
ASSERT_EQUALS("Missing closing quote in command string", error);
683685
}
684686

687+
void testVcxprojConditions() const
688+
{
689+
ASSERT(cppcheck::testing::evaluateVcxprojCondition("'$(Configuration)'=='Debug'", "Debug", "Win32"));
690+
ASSERT(cppcheck::testing::evaluateVcxprojCondition("'$(Platform)'=='Win32'", "Debug", "Win32"));
691+
ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'$(Configuration)'=='Release'", "Debug", "Win32"));
692+
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" '$(Configuration)' == 'Debug' ", "Debug", "Win32"));
693+
ASSERT(!cppcheck::testing::evaluateVcxprojCondition(" '$(Configuration)' != 'Debug' ", "Debug", "Win32"));
694+
ASSERT(cppcheck::testing::evaluateVcxprojCondition("'$(Configuration)|$(Platform)' == 'Debug|Win32' ", "Debug", "Win32"));
695+
ASSERT(!cppcheck::testing::evaluateVcxprojCondition("!('$(Configuration)|$(Platform)' == 'Debug|Win32' )", "Debug", "Win32"));
696+
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" '$(Configuration)' == 'Debug' And '$(Platform)' == 'Win32'", "Debug", "Win32"));
697+
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" '$(Configuration)' == 'Debug' Or '$(Platform)' == 'Win32'", "Release", "Win32"));
698+
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" $(Configuration.StartsWith('Debug'))", "Debug-AddressSanitizer", "Win32"));
699+
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" $(Configuration.EndsWith('AddressSanitizer'))", "Debug-AddressSanitizer", "Win32"));
700+
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" $(Configuration.Contains('Address'))", "Debug-AddressSanitizer", "Win32"));
701+
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" $(Configuration.Contains ( 'Address' ) )", "Debug-AddressSanitizer", "Win32"));
702+
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" $(Configuration.Contains('Address')) And '$(Platform)' == 'Win32'", "Debug-AddressSanitizer", "Win32"));
703+
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" ($(Configuration.Contains('Address')) ) And ( '$(Platform)' == 'Win32')", "Debug-AddressSanitizer", "Win32"));
704+
ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("And", "", ""), std::runtime_error, "'And' without previous expression!");
705+
ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("Or", "", ""), std::runtime_error, "'Or' without previous expression!");
706+
ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("!", "", ""), std::runtime_error, "Expected expression here!");
707+
ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("'' == '' And ", "", ""), std::runtime_error, "Expected expression here!");
708+
ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("('' == ''", "", ""), std::runtime_error, "'(' without closing ')'!");
709+
ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("'' == '')", "", ""), std::runtime_error, "Unhandled expression!");
710+
ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("''", "", ""), std::runtime_error, "Within a string comparison. We expect at least a =='' or !='' !");
711+
ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("'' == '", "", ""), std::runtime_error, "Within a string comparison. We expect at least a =='' or !='' !");
712+
ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Lower())", "", ""), std::runtime_error, "Unexpected function call!");
713+
}
714+
685715
// TODO: test fsParseCommand()
686716

687-
// TODO: test vcxproj conditions
688-
/*
689-
<?xml version="1.0" encoding="utf-8"?>
690-
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
691-
<ItemGroup Label="ProjectConfigurations">
692-
<ProjectConfiguration Include="Debug|x64">
693-
<Configuration>Debug</Configuration>
694-
<Platform>x64</Platform>
695-
</ProjectConfiguration>
696-
</ItemGroup>
697-
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
698-
<ClCompile>
699-
<PreprocessorDefinitions>CPPCHECKLIB_IMPORT</PreprocessorDefinitions>
700-
</ClCompile>
701-
</ItemDefinitionGroup>
702-
<ItemGroup>
703-
<ClCompile Include="main.c" />
704-
</ItemGroup>
705-
</Project>
706-
*/
707717
};
708718

709719
REGISTER_TEST(TestImportProject)

0 commit comments

Comments
 (0)