Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/selfcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ jobs:
- name: Self check (unusedFunction / no test / no gui)
run: |
supprs="--suppress=unusedFunction:lib/errorlogger.h:196 --suppress=unusedFunction:lib/importproject.cpp:1516 --suppress=unusedFunction:lib/importproject.cpp:1540"
supprs="--suppress=unusedFunction:lib/errorlogger.h:196 --suppress=unusedFunction:lib/importproject.cpp:1669 --suppress=unusedFunction:lib/importproject.cpp:1693"
./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
env:
DISABLE_VALUEFLOW: 1
Expand Down
228 changes: 194 additions & 34 deletions lib/importproject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const

namespace {
struct ProjectConfiguration {
ProjectConfiguration() = default;
explicit ProjectConfiguration(const tinyxml2::XMLElement *cfg) {
const char *a = cfg->Attribute("Include");
if (a)
Expand Down Expand Up @@ -533,45 +534,193 @@ namespace {
}
}

/**
* @brief evalExpression tries to evaluate the expression or throws an exception if it can not
* @param expr The msbuild condition
* @return the result of the expression
* @throws std::runtime_error if the expression can not be evaluated
*/
static bool evalExpression(const std::string& expr /* should be a std::string_view */, const ProjectConfiguration& p)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a doxygen @throws comment to the function.

{
const auto startsHere = [&](size_t& i, const std::string& string)
{
if (expr.compare(i, string.length(), string) == 0)
{
i += string.length();
return true;
}
return false;
};
const auto skipSpaces = [&](size_t& i)
{
i = expr.find_first_not_of(' ', i);
};
const auto parseString = [&](size_t& i)
{
skipSpaces(i);
if (i >= expr.length() || expr[i] != '\'')
{
throw std::runtime_error("Expecting a start of a string!");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also add tests which triggers (all) the exception if possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that we know that invalid msbuild projects are also invalid in cppcheck?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that we know that invalid msbuild projects are also invalid in cppcheck?

Yes. Add to make sure test we do not accidentally break these in future changes. And because of code coverage in general.

We should also add a Python test with a valid and invalid configuration. The latter because this adds exceptions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do I assert that i except an exception. There is TODO_ASSERT_THROW but why is it TODO? There is no TODO variant

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do I assert that i except an exception. There is TODO_ASSERT_THROW but why is it TODO? There is no TODO variant

There's ASSERT_THROW_EQUALS_2.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I added #define ASSERT_THROW( CMD, EXCEPTION ) do { try { (void)(CMD); ASSERT_MSG(false, "Exception was not thrown"); } catch (const AssertFailedError&) { throw; } catch (const EXCEPTION&) {} catch (...) { assertNoThrowFail(__FILE__, __LINE__, true); } } while (false)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I added #define ASSERT_THROW( CMD, EXCEPTION ) do { try { (void)(CMD); ASSERT_MSG(false, "Exception was not thrown"); } catch (const AssertFailedError&) { throw; } catch (const EXCEPTION&) {} catch (...) { assertNoThrowFail(__FILE__, __LINE__, true); } } while (false)

Why? You can use the one I mentioned above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok changed it to ASSERT_THROW_EQUALS_2. I saw your comment after implementing the tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that github requires a page reload so that I see new comments

}
auto start = ++i;
while (i < expr.length() && expr[i] != '\'') ++i;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can there be some escape sequences we should handle i.e. if expr is 'a\'b' ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it seems that is not possible

std::string string = expr.substr(start, i - start);
if (i >= expr.length() || expr[i] != '\'')
{
throw std::runtime_error("Expecting an end of a string!");
}
++i;
return string;
};
const auto expectAfterSpaces = [&](size_t &i, const std::string& expected, const char* error)
{
skipSpaces(i);
if (!startsHere(i, expected))
{
throw std::runtime_error(error);
}
};
bool currentVal = false; // should be std::optional<bool>
bool initialized = false;
for (std::size_t i = 0; i < expr.length();)
{
if (expr[i] == ' ')
{
++i;
continue;
}
if (expr[i] == '!')
{
return !evalExpression(expr.substr(i + 1), p);
}
if (startsHere(i, "And"))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be true if expr contains AndXY.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm that is true. Otherwise this is then also an invalid msbuild project that does not build...

{
if (!initialized)
{
throw std::runtime_error("'And' without previous expression!");
}
return currentVal && evalExpression(expr.substr(i), p);
}
if (startsHere(i, "Or"))
{
if (!initialized)
{
throw std::runtime_error("'Or' without previous expression!");
}
return currentVal || evalExpression(expr.substr(i), p);
}
if (expr[i] == '(')
{
// find closing )
int count = 1;
bool inString = false;
auto end = std::string::npos;
for (int j = i + 1; j < expr.length(); ++j)
{
if (inString)
{
if (expr[j] == '\'')
inString = false;
}
else if (expr[j] == '\'')
{
inString = true;
}
else if (expr[j] == '(')
{
++count;
}
else if (expr[j] == ')')
{
--count;
if (count == 0)
{
end = j;
break;
}
}
}
if (end == std::string::npos)
{
throw std::runtime_error("'(' without closing ')'!");
}
initialized = true;
currentVal = evalExpression(expr.substr(i + 1, end - i - 1), p);
i = end + 1;
continue;
}
if (expr[i] == '\'')
{
auto left = parseString(i);
skipSpaces(i);
if (i + 4 >= expr.length())
{
throw std::runtime_error("Within a string comparison. We expect at least a =='' or !='' !");
}
bool equal = expr[i] == '=';
i += 2;
// expect a string now
auto right = parseString(i);
replaceAll(left, "$(Configuration)", p.configuration);
replaceAll(left, "$(Platform)", p.platformStr);
replaceAll(right, "$(Configuration)", p.configuration);
replaceAll(right, "$(Platform)", p.platformStr);
initialized = true;
currentVal = equal ? left == right : left != right;
continue;
}
if (startsHere(i, "$(Configuration."))
{
initialized = true;
if (startsHere(i, "Contains"))
{
expectAfterSpaces(i, "(", "Expected start of call");
auto containsParam = parseString(i);
currentVal = p.configuration.find(containsParam) != std::string::npos;
}
else if (startsHere(i, "StartsWith"))
{
expectAfterSpaces(i, "(", "Expected start of call");
auto startsWithParam = parseString(i);
currentVal = p.configuration.find(startsWithParam) == 0;
}
else if (startsHere(i, "EndsWith"))
{
expectAfterSpaces(i, "(", "Expected start of call");
auto endsWithParam = parseString(i);
currentVal = p.configuration.rfind(endsWithParam) == p.configuration.length() - endsWithParam.length();
}
else
{
throw std::runtime_error("Unexpected function call!");
}
expectAfterSpaces(i, ")", "Expecting end of function call!");
expectAfterSpaces(i, ")", "Expecting end of expression!");
continue;
}
throw std::runtime_error("Unhandled expression!");
}
if (!initialized)
{
throw std::runtime_error("Expected expression here!");
}
return currentVal;
}

// see https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-conditions
// properties are .NET String objects and you can call any of its members on them
bool conditionIsTrue(const ProjectConfiguration &p) const {
bool conditionIsTrue(const ProjectConfiguration &p, std::vector<std::string> &errors, const std::string &filename) const {
if (mCondition.empty())
return true;
std::string c = '(' + mCondition + ");";
replaceAll(c, "$(Configuration)", p.configuration);
replaceAll(c, "$(Platform)", p.platformStr);

// TODO: improve evaluation
const Settings s;
TokenList tokenlist(s, Standards::Language::C);
tokenlist.createTokensFromBuffer(c.data(), c.size()); // TODO: check result
// TODO: put in a helper
// generate links
try
{
std::stack<Token*> lpar;
for (Token* tok2 = tokenlist.front(); tok2; tok2 = tok2->next()) {
if (tok2->str() == "(")
lpar.push(tok2);
else if (tok2->str() == ")") {
if (lpar.empty())
break;
Token::createMutualLinks(lpar.top(), tok2);
lpar.pop();
}
}
return evalExpression(mCondition, p);
}
tokenlist.createAst();
for (const Token *tok = tokenlist.front(); tok; tok = tok->next()) {
if (tok->str() == "(" && tok->astOperand1() && tok->astOperand2()) {
// TODO: this is wrong - it is Contains() not Equals()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have filed a ticket for this since fixing it is a behavior change. Are you able to do that or should I file one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you do that? :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do later. I want to have a reproducer first.

if (tok->astOperand1()->expressionString() == "Configuration.Contains")
return ('\'' + p.configuration + '\'') == tok->astOperand2()->str();
}
if (tok->str() == "==" && tok->astOperand1() && tok->astOperand2() && tok->astOperand1()->str() == tok->astOperand2()->str())
return true;
catch (const std::runtime_error& r)
{
errors.emplace_back(filename + ": Can not evaluate condition '" + mCondition + "': " + r.what());
return false;
}
return false;
}
private:
std::string mCondition;
Expand Down Expand Up @@ -644,6 +793,17 @@ namespace {
};
}

// only used by tests (testimportproject.cpp::testVcxprojConditions):
// cppcheck-suppress unusedFunction
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add an additional comment that the function is only used by test code.

bool cppcheck::testing::evaluateVcxprojCondition(const std::string& condition, const std::string& configuration,
const std::string& platform)
{
ProjectConfiguration p;
p.configuration = configuration;
p.platformStr = platform;
return ConditionalGroup::evalExpression(condition, p);
}

static std::list<std::string> toStringList(const std::string &s)
{
std::list<std::string> ret;
Expand Down Expand Up @@ -879,7 +1039,7 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X
}
std::string additionalIncludePaths;
for (const ItemDefinitionGroup &i : itemDefinitionGroupList) {
if (!i.conditionIsTrue(p))
if (!i.conditionIsTrue(p, errors, cfilename))
continue;
fs.standard = Standards::getCPP(i.cppstd);
fs.defines += ';' + i.preprocessorDefinitions;
Expand All @@ -897,7 +1057,7 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X
}
bool useUnicode = false;
for (const ConfigurationPropertyGroup &c : configurationPropertyGroups) {
if (!c.conditionIsTrue(p))
if (!c.conditionIsTrue(p, errors, cfilename))
continue;
// in msbuild the last definition wins
useUnicode = c.useUnicode;
Expand Down
5 changes: 5 additions & 0 deletions lib/importproject.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ namespace cppcheck {
return caseInsensitiveStringCompare(lhs,rhs) < 0;
}
};

namespace testing
{
CPPCHECKLIB bool evaluateVcxprojCondition(const std::string& condition, const std::string& configuration, const std::string& platform);
}
}

/**
Expand Down
51 changes: 31 additions & 20 deletions test/testimportproject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include <list>
#include <map>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
Expand Down Expand Up @@ -75,6 +76,7 @@ class TestImportProject : public TestFixture {
TEST_CASE(importCppcheckGuiProjectPremiumMisra);
TEST_CASE(ignorePaths);
TEST_CASE(testVcxprojUnicode);
TEST_CASE(testVcxprojConditions);
}

void setDefines() const {
Expand Down Expand Up @@ -579,28 +581,37 @@ class TestImportProject : public TestFixture {
ASSERT_EQUALS(project.fileSettings.back().useMfc, true);
}


void testVcxprojConditions() const
{
ASSERT(cppcheck::testing::evaluateVcxprojCondition("'$(Configuration)'=='Debug'", "Debug", "Win32"));
ASSERT(cppcheck::testing::evaluateVcxprojCondition("'$(Platform)'=='Win32'", "Debug", "Win32"));
ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'$(Configuration)'=='Release'", "Debug", "Win32"));
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" '$(Configuration)' == 'Debug' ", "Debug", "Win32"));
ASSERT(!cppcheck::testing::evaluateVcxprojCondition(" '$(Configuration)' != 'Debug' ", "Debug", "Win32"));
ASSERT(cppcheck::testing::evaluateVcxprojCondition("'$(Configuration)|$(Platform)' == 'Debug|Win32' ", "Debug", "Win32"));
ASSERT(!cppcheck::testing::evaluateVcxprojCondition("!('$(Configuration)|$(Platform)' == 'Debug|Win32' )", "Debug", "Win32"));
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" '$(Configuration)' == 'Debug' And '$(Platform)' == 'Win32'", "Debug", "Win32"));
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" '$(Configuration)' == 'Debug' Or '$(Platform)' == 'Win32'", "Release", "Win32"));
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" $(Configuration.StartsWith('Debug'))", "Debug-AddressSanitizer", "Win32"));
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" $(Configuration.EndsWith('AddressSanitizer'))", "Debug-AddressSanitizer", "Win32"));
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" $(Configuration.Contains('Address'))", "Debug-AddressSanitizer", "Win32"));
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" $(Configuration.Contains ( 'Address' ) )", "Debug-AddressSanitizer", "Win32"));
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" $(Configuration.Contains('Address')) And '$(Platform)' == 'Win32'", "Debug-AddressSanitizer", "Win32"));
ASSERT(cppcheck::testing::evaluateVcxprojCondition(" ($(Configuration.Contains('Address')) ) And ( '$(Platform)' == 'Win32')", "Debug-AddressSanitizer", "Win32"));
ASSERT_THROW_EQUALS_2(cppcheck::testing::evaluateVcxprojCondition("And", "", ""), std::runtime_error, "'And' without previous expression!");
ASSERT_THROW_EQUALS_2(cppcheck::testing::evaluateVcxprojCondition("Or", "", ""), std::runtime_error, "'Or' without previous expression!");
ASSERT_THROW_EQUALS_2(cppcheck::testing::evaluateVcxprojCondition("!", "", ""), std::runtime_error, "Expected expression here!");
ASSERT_THROW_EQUALS_2(cppcheck::testing::evaluateVcxprojCondition("'' == '' And ", "", ""), std::runtime_error, "Expected expression here!");
ASSERT_THROW_EQUALS_2(cppcheck::testing::evaluateVcxprojCondition("('' == ''", "", ""), std::runtime_error, "'(' without closing ')'!");
ASSERT_THROW_EQUALS_2(cppcheck::testing::evaluateVcxprojCondition("'' == '')", "", ""), std::runtime_error, "Unhandled expression!");
ASSERT_THROW_EQUALS_2(cppcheck::testing::evaluateVcxprojCondition("''", "", ""), std::runtime_error, "Within a string comparison. We expect at least a =='' or !='' !");
ASSERT_THROW_EQUALS_2(cppcheck::testing::evaluateVcxprojCondition("'' == '", "", ""), std::runtime_error, "Within a string comparison. We expect at least a =='' or !='' !");
ASSERT_THROW_EQUALS_2(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Lower())", "", ""), std::runtime_error, "Unexpected function call!");
}

// TODO: test fsParseCommand()

// TODO: test vcxproj conditions
/*
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<PreprocessorDefinitions>CPPCHECKLIB_IMPORT</PreprocessorDefinitions>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="main.c" />
</ItemGroup>
</Project>
*/
};

REGISTER_TEST(TestImportProject)
Loading