-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Fix #14334: Support more msbuild conditional constructs #8039
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
|
@@ -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) | ||
| { | ||
| 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) | ||
autoantwort marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| 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!"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should also add tests which triggers (all) the exception if possible.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do I assert that i except an exception. There is
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There's
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok I added
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Why? You can use the one I mentioned above.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok changed it to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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")) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this will be true if expr contains
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you do that? :)
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
|
@@ -644,6 +793,17 @@ namespace { | |
| }; | ||
| } | ||
|
|
||
| // only used by tests (testimportproject.cpp::testVcxprojConditions): | ||
| // cppcheck-suppress unusedFunction | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
|
@@ -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; | ||
|
|
@@ -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; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a doxygen
@throwscomment to the function.