Skip to content

Commit 26b1cc3

Browse files
Leander SchultenLeander Schulten
authored andcommitted
Fix #14334: Support more msbuild conditional constructs
1 parent ec77e31 commit 26b1cc3

4 files changed

Lines changed: 227 additions & 55 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:1584 --suppress=unusedFunction:lib/importproject.cpp:1608"
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: 191 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include <iostream>
3636
#include <iterator>
3737
#include <stack>
38+
#include <string>
3839
#include <unordered_set>
3940
#include <utility>
4041
#include <vector>
@@ -558,6 +559,7 @@ bool ImportProject::importSlnx(const std::string& filename, const std::vector<st
558559

559560
namespace {
560561
struct ProjectConfiguration {
562+
ProjectConfiguration() = default;
561563
explicit ProjectConfiguration(const tinyxml2::XMLElement *cfg) {
562564
const char *a = cfg->Attribute("Include");
563565
if (a)
@@ -601,45 +603,189 @@ namespace {
601603
}
602604
}
603605

606+
/**
607+
* @brief evalExpression tries to evaluate the expression or throws an exception if it can not
608+
* @param expr The msbuild condition
609+
* @return the result of the expression
610+
* @throws std::runtime_error if the expression can not be evaluated
611+
*/
612+
static bool evalExpression(const std::string& expr /* should be a std::string_view */, const ProjectConfiguration& p)
613+
{
614+
const auto startsHere = [&](size_t& i, const std::string& string)
615+
{
616+
if (expr.compare(i, string.length(), string) == 0)
617+
{
618+
i += string.length();
619+
return true;
620+
}
621+
return false;
622+
};
623+
const auto skipSpaces = [&](size_t& i)
624+
{
625+
i = expr.find_first_not_of(' ', i);
626+
};
627+
const auto expectAfterSpaces = [&](size_t &i, const std::string& expected, const char* error)
628+
{
629+
skipSpaces(i);
630+
if (!startsHere(i, expected))
631+
{
632+
throw std::runtime_error(error);
633+
}
634+
};
635+
const auto parseString = [&](size_t& i)
636+
{
637+
expectAfterSpaces(i, "\'", "Expecting a start of a string!");
638+
const auto start = i;
639+
const auto end = expr.find('\'', i);
640+
if (end == std::string::npos)
641+
{
642+
throw std::runtime_error("Expecting an end of a string!");
643+
}
644+
std::string string = expr.substr(start, end - start);
645+
i = end + 1;
646+
return string;
647+
};
648+
bool currentVal = false; // should be std::optional<bool>
649+
bool initialized = false;
650+
for (std::size_t i = 0; i < expr.length();)
651+
{
652+
if (expr[i] == ' ')
653+
{
654+
++i;
655+
continue;
656+
}
657+
if (expr[i] == '!')
658+
{
659+
return !evalExpression(expr.substr(i + 1), p);
660+
}
661+
if (startsHere(i, "And"))
662+
{
663+
if (!initialized)
664+
{
665+
throw std::runtime_error("'And' without previous expression!");
666+
}
667+
return currentVal && evalExpression(expr.substr(i), p);
668+
}
669+
if (startsHere(i, "Or"))
670+
{
671+
if (!initialized)
672+
{
673+
throw std::runtime_error("'Or' without previous expression!");
674+
}
675+
return currentVal || evalExpression(expr.substr(i), p);
676+
}
677+
if (expr[i] == '(')
678+
{
679+
// find closing )
680+
int count = 1;
681+
bool inString = false;
682+
auto end = std::string::npos;
683+
for (int j = i + 1; j < expr.length(); ++j)
684+
{
685+
if (inString)
686+
{
687+
if (expr[j] == '\'')
688+
inString = false;
689+
}
690+
else if (expr[j] == '\'')
691+
{
692+
inString = true;
693+
}
694+
else if (expr[j] == '(')
695+
{
696+
++count;
697+
}
698+
else if (expr[j] == ')')
699+
{
700+
--count;
701+
if (count == 0)
702+
{
703+
end = j;
704+
break;
705+
}
706+
}
707+
}
708+
if (end == std::string::npos)
709+
{
710+
throw std::runtime_error("'(' without closing ')'!");
711+
}
712+
initialized = true;
713+
currentVal = evalExpression(expr.substr(i + 1, end - i - 1), p);
714+
i = end + 1;
715+
continue;
716+
}
717+
if (expr[i] == '\'')
718+
{
719+
auto left = parseString(i);
720+
skipSpaces(i);
721+
if (i + 4 >= expr.length())
722+
{
723+
throw std::runtime_error("Within a string comparison. We expect at least a =='' or !='' !");
724+
}
725+
bool equal = expr[i] == '=';
726+
i += 2;
727+
// expect a string now
728+
auto right = parseString(i);
729+
replaceAll(left, "$(Configuration)", p.configuration);
730+
replaceAll(left, "$(Platform)", p.platformStr);
731+
replaceAll(right, "$(Configuration)", p.configuration);
732+
replaceAll(right, "$(Platform)", p.platformStr);
733+
initialized = true;
734+
currentVal = equal ? left == right : left != right;
735+
continue;
736+
}
737+
if (startsHere(i, "$(Configuration."))
738+
{
739+
initialized = true;
740+
if (startsHere(i, "Contains"))
741+
{
742+
expectAfterSpaces(i, "(", "Expected start of call");
743+
auto containsParam = parseString(i);
744+
currentVal = p.configuration.find(containsParam) != std::string::npos;
745+
}
746+
else if (startsHere(i, "StartsWith"))
747+
{
748+
expectAfterSpaces(i, "(", "Expected start of call");
749+
auto startsWithParam = parseString(i);
750+
currentVal = p.configuration.find(startsWithParam) == 0;
751+
}
752+
else if (startsHere(i, "EndsWith"))
753+
{
754+
expectAfterSpaces(i, "(", "Expected start of call");
755+
auto endsWithParam = parseString(i);
756+
currentVal = p.configuration.rfind(endsWithParam) == p.configuration.length() - endsWithParam.length();
757+
}
758+
else
759+
{
760+
throw std::runtime_error("Unexpected function call!");
761+
}
762+
expectAfterSpaces(i, ")", "Expecting end of function call!");
763+
expectAfterSpaces(i, ")", "Expecting end of expression!");
764+
continue;
765+
}
766+
throw std::runtime_error("Unhandled expression!");
767+
}
768+
if (!initialized)
769+
{
770+
throw std::runtime_error("Expected expression here!");
771+
}
772+
return currentVal;
773+
}
774+
604775
// see https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-conditions
605776
// properties are .NET String objects and you can call any of its members on them
606-
bool conditionIsTrue(const ProjectConfiguration &p) const {
777+
bool conditionIsTrue(const ProjectConfiguration &p, std::vector<std::string> &errors, const std::string &filename) const {
607778
if (mCondition.empty())
608779
return true;
609-
std::string c = '(' + mCondition + ");";
610-
replaceAll(c, "$(Configuration)", p.configuration);
611-
replaceAll(c, "$(Platform)", p.platformStr);
612-
613-
// TODO: improve evaluation
614-
const Settings s;
615-
TokenList tokenlist(s, Standards::Language::C);
616-
tokenlist.createTokensFromBuffer(c.data(), c.size()); // TODO: check result
617-
// TODO: put in a helper
618-
// generate links
780+
try
619781
{
620-
std::stack<Token*> lpar;
621-
for (Token* tok2 = tokenlist.front(); tok2; tok2 = tok2->next()) {
622-
if (tok2->str() == "(")
623-
lpar.push(tok2);
624-
else if (tok2->str() == ")") {
625-
if (lpar.empty())
626-
break;
627-
Token::createMutualLinks(lpar.top(), tok2);
628-
lpar.pop();
629-
}
630-
}
782+
return evalExpression(mCondition, p);
631783
}
632-
tokenlist.createAst();
633-
for (const Token *tok = tokenlist.front(); tok; tok = tok->next()) {
634-
if (tok->str() == "(" && tok->astOperand1() && tok->astOperand2()) {
635-
// TODO: this is wrong - it is Contains() not Equals()
636-
if (tok->astOperand1()->expressionString() == "Configuration.Contains")
637-
return ('\'' + p.configuration + '\'') == tok->astOperand2()->str();
638-
}
639-
if (tok->str() == "==" && tok->astOperand1() && tok->astOperand2() && tok->astOperand1()->str() == tok->astOperand2()->str())
640-
return true;
784+
catch (const std::runtime_error& r)
785+
{
786+
errors.emplace_back(filename + ": Can not evaluate condition '" + mCondition + "': " + r.what());
787+
return false;
641788
}
642-
return false;
643789
}
644790
private:
645791
std::string mCondition;
@@ -712,6 +858,17 @@ namespace {
712858
};
713859
}
714860

861+
// only used by tests (testimportproject.cpp::testVcxprojConditions):
862+
// cppcheck-suppress unusedFunction
863+
bool cppcheck::testing::evaluateVcxprojCondition(const std::string& condition, const std::string& configuration,
864+
const std::string& platform)
865+
{
866+
ProjectConfiguration p;
867+
p.configuration = configuration;
868+
p.platformStr = platform;
869+
return ConditionalGroup::evalExpression(condition, p);
870+
}
871+
715872
static std::list<std::string> toStringList(const std::string &s)
716873
{
717874
std::list<std::string> ret;
@@ -947,7 +1104,7 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X
9471104
}
9481105
std::string additionalIncludePaths;
9491106
for (const ItemDefinitionGroup &i : itemDefinitionGroupList) {
950-
if (!i.conditionIsTrue(p))
1107+
if (!i.conditionIsTrue(p, errors, cfilename))
9511108
continue;
9521109
fs.standard = Standards::getCPP(i.cppstd);
9531110
fs.defines += ';' + i.preprocessorDefinitions;
@@ -965,7 +1122,7 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X
9651122
}
9661123
bool useUnicode = false;
9671124
for (const ConfigurationPropertyGroup &c : configurationPropertyGroups) {
968-
if (!c.conditionIsTrue(p))
1125+
if (!c.conditionIsTrue(p, errors, cfilename))
9691126
continue;
9701127
// in msbuild the last definition wins
9711128
useUnicode = c.useUnicode;

lib/importproject.h

Lines changed: 5 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
/**

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)