Skip to content

Commit e24c10c

Browse files
Leander SchultenLeander Schulten
authored andcommitted
Fix #14334: Support more msbuild conditional constructs
1 parent 3a99f41 commit e24c10c

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:196 --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: 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>
@@ -505,6 +506,7 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const
505506

506507
namespace {
507508
struct ProjectConfiguration {
509+
ProjectConfiguration() = default;
508510
explicit ProjectConfiguration(const tinyxml2::XMLElement *cfg) {
509511
const char *a = cfg->Attribute("Include");
510512
if (a)
@@ -548,45 +550,189 @@ namespace {
548550
}
549551
}
550552

553+
/**
554+
* @brief evalExpression tries to evaluate the expression or throws an exception if it can not
555+
* @param expr The msbuild condition
556+
* @return the result of the expression
557+
* @throws std::runtime_error if the expression can not be evaluated
558+
*/
559+
static bool evalExpression(const std::string& expr /* should be a std::string_view */, const ProjectConfiguration& p)
560+
{
561+
const auto startsHere = [&](size_t& i, const std::string& string)
562+
{
563+
if (expr.compare(i, string.length(), string) == 0)
564+
{
565+
i += string.length();
566+
return true;
567+
}
568+
return false;
569+
};
570+
const auto skipSpaces = [&](size_t& i)
571+
{
572+
i = expr.find_first_not_of(' ', i);
573+
};
574+
const auto expectAfterSpaces = [&](size_t &i, const std::string& expected, const char* error)
575+
{
576+
skipSpaces(i);
577+
if (!startsHere(i, expected))
578+
{
579+
throw std::runtime_error(error);
580+
}
581+
};
582+
const auto parseString = [&](size_t& i)
583+
{
584+
expectAfterSpaces(i, "\'", "Expecting a start of a string!");
585+
const auto start = i;
586+
const auto end = expr.find('\'', i);
587+
if (end == std::string::npos)
588+
{
589+
throw std::runtime_error("Expecting an end of a string!");
590+
}
591+
std::string string = expr.substr(start, end - start);
592+
i = end + 1;
593+
return string;
594+
};
595+
bool currentVal = false; // should be std::optional<bool>
596+
bool initialized = false;
597+
for (std::size_t i = 0; i < expr.length();)
598+
{
599+
if (expr[i] == ' ')
600+
{
601+
++i;
602+
continue;
603+
}
604+
if (expr[i] == '!')
605+
{
606+
return !evalExpression(expr.substr(i + 1), p);
607+
}
608+
if (startsHere(i, "And"))
609+
{
610+
if (!initialized)
611+
{
612+
throw std::runtime_error("'And' without previous expression!");
613+
}
614+
return currentVal && evalExpression(expr.substr(i), p);
615+
}
616+
if (startsHere(i, "Or"))
617+
{
618+
if (!initialized)
619+
{
620+
throw std::runtime_error("'Or' without previous expression!");
621+
}
622+
return currentVal || evalExpression(expr.substr(i), p);
623+
}
624+
if (expr[i] == '(')
625+
{
626+
// find closing )
627+
int count = 1;
628+
bool inString = false;
629+
auto end = std::string::npos;
630+
for (int j = i + 1; j < expr.length(); ++j)
631+
{
632+
if (inString)
633+
{
634+
if (expr[j] == '\'')
635+
inString = false;
636+
}
637+
else if (expr[j] == '\'')
638+
{
639+
inString = true;
640+
}
641+
else if (expr[j] == '(')
642+
{
643+
++count;
644+
}
645+
else if (expr[j] == ')')
646+
{
647+
--count;
648+
if (count == 0)
649+
{
650+
end = j;
651+
break;
652+
}
653+
}
654+
}
655+
if (end == std::string::npos)
656+
{
657+
throw std::runtime_error("'(' without closing ')'!");
658+
}
659+
initialized = true;
660+
currentVal = evalExpression(expr.substr(i + 1, end - i - 1), p);
661+
i = end + 1;
662+
continue;
663+
}
664+
if (expr[i] == '\'')
665+
{
666+
auto left = parseString(i);
667+
skipSpaces(i);
668+
if (i + 4 >= expr.length())
669+
{
670+
throw std::runtime_error("Within a string comparison. We expect at least a =='' or !='' !");
671+
}
672+
bool equal = expr[i] == '=';
673+
i += 2;
674+
// expect a string now
675+
auto right = parseString(i);
676+
replaceAll(left, "$(Configuration)", p.configuration);
677+
replaceAll(left, "$(Platform)", p.platformStr);
678+
replaceAll(right, "$(Configuration)", p.configuration);
679+
replaceAll(right, "$(Platform)", p.platformStr);
680+
initialized = true;
681+
currentVal = equal ? left == right : left != right;
682+
continue;
683+
}
684+
if (startsHere(i, "$(Configuration."))
685+
{
686+
initialized = true;
687+
if (startsHere(i, "Contains"))
688+
{
689+
expectAfterSpaces(i, "(", "Expected start of call");
690+
auto containsParam = parseString(i);
691+
currentVal = p.configuration.find(containsParam) != std::string::npos;
692+
}
693+
else if (startsHere(i, "StartsWith"))
694+
{
695+
expectAfterSpaces(i, "(", "Expected start of call");
696+
auto startsWithParam = parseString(i);
697+
currentVal = p.configuration.find(startsWithParam) == 0;
698+
}
699+
else if (startsHere(i, "EndsWith"))
700+
{
701+
expectAfterSpaces(i, "(", "Expected start of call");
702+
auto endsWithParam = parseString(i);
703+
currentVal = p.configuration.rfind(endsWithParam) == p.configuration.length() - endsWithParam.length();
704+
}
705+
else
706+
{
707+
throw std::runtime_error("Unexpected function call!");
708+
}
709+
expectAfterSpaces(i, ")", "Expecting end of function call!");
710+
expectAfterSpaces(i, ")", "Expecting end of expression!");
711+
continue;
712+
}
713+
throw std::runtime_error("Unhandled expression!");
714+
}
715+
if (!initialized)
716+
{
717+
throw std::runtime_error("Expected expression here!");
718+
}
719+
return currentVal;
720+
}
721+
551722
// see https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-conditions
552723
// properties are .NET String objects and you can call any of its members on them
553-
bool conditionIsTrue(const ProjectConfiguration &p) const {
724+
bool conditionIsTrue(const ProjectConfiguration &p, std::vector<std::string> &errors, const std::string &filename) const {
554725
if (mCondition.empty())
555726
return true;
556-
std::string c = '(' + mCondition + ");";
557-
replaceAll(c, "$(Configuration)", p.configuration);
558-
replaceAll(c, "$(Platform)", p.platformStr);
559-
560-
// TODO: improve evaluation
561-
const Settings s;
562-
TokenList tokenlist(s, Standards::Language::C);
563-
tokenlist.createTokensFromBuffer(c.data(), c.size()); // TODO: check result
564-
// TODO: put in a helper
565-
// generate links
727+
try
566728
{
567-
std::stack<Token*> lpar;
568-
for (Token* tok2 = tokenlist.front(); tok2; tok2 = tok2->next()) {
569-
if (tok2->str() == "(")
570-
lpar.push(tok2);
571-
else if (tok2->str() == ")") {
572-
if (lpar.empty())
573-
break;
574-
Token::createMutualLinks(lpar.top(), tok2);
575-
lpar.pop();
576-
}
577-
}
729+
return evalExpression(mCondition, p);
578730
}
579-
tokenlist.createAst();
580-
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();
585-
}
586-
if (tok->str() == "==" && tok->astOperand1() && tok->astOperand2() && tok->astOperand1()->str() == tok->astOperand2()->str())
587-
return true;
731+
catch (const std::runtime_error& r)
732+
{
733+
errors.emplace_back(filename + ": Can not evaluate condition '" + mCondition + "': " + r.what());
734+
return false;
588735
}
589-
return false;
590736
}
591737
private:
592738
std::string mCondition;
@@ -659,6 +805,17 @@ namespace {
659805
};
660806
}
661807

808+
// only used by tests (testimportproject.cpp::testVcxprojConditions):
809+
// cppcheck-suppress unusedFunction
810+
bool cppcheck::testing::evaluateVcxprojCondition(const std::string& condition, const std::string& configuration,
811+
const std::string& platform)
812+
{
813+
ProjectConfiguration p;
814+
p.configuration = configuration;
815+
p.platformStr = platform;
816+
return ConditionalGroup::evalExpression(condition, p);
817+
}
818+
662819
static std::list<std::string> toStringList(const std::string &s)
663820
{
664821
std::list<std::string> ret;
@@ -894,7 +1051,7 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X
8941051
}
8951052
std::string additionalIncludePaths;
8961053
for (const ItemDefinitionGroup &i : itemDefinitionGroupList) {
897-
if (!i.conditionIsTrue(p))
1054+
if (!i.conditionIsTrue(p, errors, cfilename))
8981055
continue;
8991056
fs.standard = Standards::getCPP(i.cppstd);
9001057
fs.defines += ';' + i.preprocessorDefinitions;
@@ -912,7 +1069,7 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X
9121069
}
9131070
bool useUnicode = false;
9141071
for (const ConfigurationPropertyGroup &c : configurationPropertyGroups) {
915-
if (!c.conditionIsTrue(p))
1072+
if (!c.conditionIsTrue(p, errors, cfilename))
9161073
continue;
9171074
// in msbuild the last definition wins
9181075
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_2(cppcheck::testing::evaluateVcxprojCondition("And", "", ""), std::runtime_error, "'And' without previous expression!");
705+
ASSERT_THROW_EQUALS_2(cppcheck::testing::evaluateVcxprojCondition("Or", "", ""), std::runtime_error, "'Or' without previous expression!");
706+
ASSERT_THROW_EQUALS_2(cppcheck::testing::evaluateVcxprojCondition("!", "", ""), std::runtime_error, "Expected expression here!");
707+
ASSERT_THROW_EQUALS_2(cppcheck::testing::evaluateVcxprojCondition("'' == '' And ", "", ""), std::runtime_error, "Expected expression here!");
708+
ASSERT_THROW_EQUALS_2(cppcheck::testing::evaluateVcxprojCondition("('' == ''", "", ""), std::runtime_error, "'(' without closing ')'!");
709+
ASSERT_THROW_EQUALS_2(cppcheck::testing::evaluateVcxprojCondition("'' == '')", "", ""), std::runtime_error, "Unhandled expression!");
710+
ASSERT_THROW_EQUALS_2(cppcheck::testing::evaluateVcxprojCondition("''", "", ""), std::runtime_error, "Within a string comparison. We expect at least a =='' or !='' !");
711+
ASSERT_THROW_EQUALS_2(cppcheck::testing::evaluateVcxprojCondition("'' == '", "", ""), std::runtime_error, "Within a string comparison. We expect at least a =='' or !='' !");
712+
ASSERT_THROW_EQUALS_2(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)