diff --git a/Makefile b/Makefile index a7f3feefba6..9b245226036 100644 --- a/Makefile +++ b/Makefile @@ -615,7 +615,7 @@ $(libcppdir)/forwardanalyzer.o: lib/forwardanalyzer.cpp lib/analyzer.h lib/astut $(libcppdir)/fwdanalysis.o: lib/fwdanalysis.cpp lib/astutils.h lib/checkers.h lib/config.h lib/errortypes.h lib/fwdanalysis.h lib/library.h lib/mathlib.h lib/platform.h lib/settings.h lib/smallvector.h lib/sourcelocation.h lib/standards.h lib/symboldatabase.h lib/templatesimplifier.h lib/token.h lib/utils.h lib/vfvalue.h $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/fwdanalysis.cpp -$(libcppdir)/importproject.o: lib/importproject.cpp externals/picojson/picojson.h externals/tinyxml2/tinyxml2.h lib/checkers.h lib/config.h lib/errortypes.h lib/filesettings.h lib/importproject.h lib/json.h lib/library.h lib/mathlib.h lib/path.h lib/pathmatch.h lib/platform.h lib/settings.h lib/smallvector.h lib/standards.h lib/suppressions.h lib/templatesimplifier.h lib/token.h lib/tokenlist.h lib/utils.h lib/vfvalue.h lib/xml.h +$(libcppdir)/importproject.o: lib/importproject.cpp externals/picojson/picojson.h externals/tinyxml2/tinyxml2.h lib/checkers.h lib/config.h lib/filesettings.h lib/importproject.h lib/json.h lib/library.h lib/mathlib.h lib/path.h lib/pathmatch.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/utils.h lib/xml.h $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/importproject.cpp $(libcppdir)/infer.o: lib/infer.cpp lib/calculate.h lib/config.h lib/errortypes.h lib/infer.h lib/mathlib.h lib/smallvector.h lib/templatesimplifier.h lib/token.h lib/utils.h lib/valueptr.h lib/vfvalue.h @@ -819,7 +819,7 @@ test/testfunctions.o: test/testfunctions.cpp lib/check.h lib/checkers.h lib/chec test/testgarbage.o: test/testgarbage.cpp lib/check.h lib/checkers.h lib/checks.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/settings.h lib/smallvector.h lib/standards.h lib/templatesimplifier.h lib/token.h lib/tokenize.h lib/tokenlist.h lib/utils.h lib/vfvalue.h test/fixture.h test/helpers.h $(CXX) ${INCLUDE_FOR_TEST} ${CFLAGS_FOR_TEST} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ test/testgarbage.cpp -test/testimportproject.o: test/testimportproject.cpp externals/tinyxml2/tinyxml2.h lib/check.h lib/checkers.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/filesettings.h lib/importproject.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/utils.h lib/xml.h test/fixture.h test/redirect.h +test/testimportproject.o: test/testimportproject.cpp lib/check.h lib/checkers.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/filesettings.h lib/importproject.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/utils.h test/fixture.h test/redirect.h $(CXX) ${INCLUDE_FOR_TEST} ${CFLAGS_FOR_TEST} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ test/testimportproject.cpp test/testincompletestatement.o: test/testincompletestatement.cpp lib/check.h lib/checkers.h lib/checkimpl.h lib/checkother.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/settings.h lib/standards.h lib/tokenize.h lib/tokenlist.h lib/utils.h test/fixture.h test/helpers.h diff --git a/lib/importproject.cpp b/lib/importproject.cpp index 61ef7a2d38a..7e88fd59819 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -23,18 +23,19 @@ #include "settings.h" #include "standards.h" #include "suppressions.h" -#include "token.h" -#include "tokenlist.h" #include "utils.h" #include +#include #include #include #include #include #include #include -#include +#include +#include +#include #include #include #include @@ -246,70 +247,36 @@ void ImportProject::fsSetDefines(FileSettings& fs, std::string defs) fs.defines.swap(defs); } -static bool simplifyPathWithVariables(std::string &s, std::map &variables) +static void expandMSBuildVariables(std::string &s, VariablesMap &variables) { - std::set expanded; - std::string::size_type start = 0; - while ((start = s.find("$(")) != std::string::npos) { - const std::string::size_type end = s.find(')',start); - if (end == std::string::npos) - break; - const std::string var = s.substr(start+2,end-start-2); - if (expanded.find(var) != expanded.end()) - break; - expanded.insert(var); - auto it1 = utils::as_const(variables).find(var); - // variable was not found within defined variables - if (it1 == variables.end()) { - const char *envValue = std::getenv(var.c_str()); - if (!envValue) { - //! \todo generate a debug/info message about undefined variable + // Use multiple passes with a "no change" termination guard. + // A cap of 50 prevents infinite loops from genuinely cyclic variables (A=$(B), B=$(A)). + const int maxPasses = 50; + for (int pass = 0; pass < maxPasses; ++pass) { + bool changed = false; + std::string::size_type pos = 0; + while ((pos = s.find("$(", pos)) != std::string::npos) { + const std::string::size_type end = s.find(')', pos); + if (end == std::string::npos) break; + const std::string var = s.substr(pos + 2, end - pos - 2); + auto it = variables.find(var); + if (it == variables.end()) { + // fall back to environment variable and cache for future passes + const char *envValue = std::getenv(var.c_str()); + if (!envValue) { + pos = end + 1; // unknown — skip and keep going + continue; + } + variables[var] = envValue; + it = variables.find(var); } - variables[var] = std::string(envValue); - it1 = variables.find(var); + s.replace(pos, end - pos + 1, it->second); + pos += it->second.size(); // advance past the replacement + changed = true; } - s.replace(start, end - start + 1, it1->second); - } - if (s.find("$(") != std::string::npos) - return false; - s = Path::simplifyPath(std::move(s)); - return true; -} - -void ImportProject::fsSetIncludePaths(FileSettings& fs, const std::string &basepath, const std::list &in, std::map &variables) -{ - std::set found; - // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) - const std::list copyIn(in); - fs.includePaths.clear(); - for (const std::string &ipath : copyIn) { - if (ipath.empty()) - continue; - if (startsWith(ipath,"%(")) - continue; - std::string s(Path::fromNativeSeparators(ipath)); - if (!found.insert(s).second) - continue; - if (s[0] == '/' || (s.size() > 1U && s.compare(1,2,":/") == 0)) { - if (!endsWith(s,'/')) - s += '/'; - fs.includePaths.push_back(std::move(s)); - continue; - } - - if (endsWith(s,'/')) // this is a temporary hack, simplifyPath can crash if path ends with '/' - s.pop_back(); - - if (s.find("$(") == std::string::npos) { - s = Path::simplifyPath(basepath + s); - } else { - if (!simplifyPathWithVariables(s, variables)) - continue; - } - if (s.empty()) - continue; - fs.includePaths.push_back(s.back() == '/' ? s : (s + '/')); + if (!changed) + break; // nothing left to expand — done } } @@ -332,7 +299,7 @@ ImportProject::Type ImportProject::import(const std::string &filename, Settings return ImportProject::Type::COMPILE_DB; } } else if (endsWith(filename, ".sln")) { - if (importSln(fin, mPath, fileFilters)) { + if (importSln(fin, filename, fileFilters)) { setRelativePaths(filename); return ImportProject::Type::VS_SLN; } @@ -342,9 +309,7 @@ ImportProject::Type ImportProject::import(const std::string &filename, Settings return ImportProject::Type::VS_SLNX; } } else if (endsWith(filename, ".vcxproj")) { - std::map variables; - std::vector sharedItemsProjects; - if (importVcxproj(filename, variables, "", fileFilters, sharedItemsProjects)) { + if (importVcxproj(toAbsolute(filename), mVariables, fileFilters)) { setRelativePaths(filename); return ImportProject::Type::VS_VCXPROJ; } @@ -452,7 +417,7 @@ bool ImportProject::importCompileCommands(std::istream &istr) path = Path::simplifyPath(directory + file); FileSettings fs{path, Standards::Language::None, 0}; // file will be identified later on parseArgs(fs, arguments); - std::map variables; + VariablesMap variables; fsSetIncludePaths(fs, directory, fs.includePaths, variables); // Assign a unique index to each file path. If the file path already exists in the map, // increment the index to handle duplicate file entries. @@ -463,10 +428,24 @@ bool ImportProject::importCompileCommands(std::istream &istr) return true; } -bool ImportProject::importSln(std::istream &istr, const std::string &path, const std::vector &fileFilters) +void ImportProject::setSolution(const std::string &filename, VariablesMap &variables) { + const std::string absolutePath = toAbsolute(filename); + variables["SolutionDir"] = Path::getPathFromFilename(absolutePath); + variables["SolutionExt"] = Path::getFilenameExtensionInLowerCase(absolutePath); + variables["SolutionPath"] = absolutePath; + // Path::stripDirectoryPart doesn't work on windows with unix paths + variables["SolutionFileName"] = absolutePath.substr(absolutePath.rfind('/') + 1, absolutePath.size()); + std::string temp = variables["SolutionFileName"]; + findAndReplace(temp, Path::getFilenameExtension(temp), ""); + variables["SolutionName"] = temp; +} + +bool ImportProject::importSln(std::istream &istr, const std::string &filename, const std::vector &fileFilters) { std::string line; + debugs.clear(); + if (!std::getline(istr,line)) { errors.emplace_back("Visual Studio solution file is empty"); return false; @@ -480,12 +459,22 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const } } - std::map variables; - variables["SolutionDir"] = path; + VariablesMap solutionVariables; + setSolution(filename, solutionVariables); + + solutionVariables["VisualStudioVersion"] = "17.0"; + + const std::string solutionDir = solutionVariables["SolutionDir"]; bool found = false; - std::vector sharedItemsProjects; while (std::getline(istr,line)) { + if (startsWith(line, "VisualStudioVersion = ")) { + const std::string ver = line.substr(std::strlen("VisualStudioVersion = ")); + const std::string::size_type dot = ver.find('.'); + const std::string::size_type dot2 = (dot != std::string::npos) ? ver.find('.', dot + 1) : std::string::npos; + solutionVariables["VisualStudioVersion"] = (dot2 != std::string::npos) ? ver.substr(0, dot2) : ver; + continue; + } if (!startsWith(line,"Project(")) continue; const std::string::size_type pos = line.find(".vcxproj"); @@ -496,10 +485,11 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const continue; std::string vcxproj(line.substr(pos1+1, pos-pos1+7)); vcxproj = Path::toNativeSeparators(std::move(vcxproj)); - if (!Path::isAbsolute(vcxproj)) - vcxproj = path + vcxproj; + vcxproj = toAbsolute(vcxproj, solutionDir, solutionVariables); vcxproj = Path::fromNativeSeparators(std::move(vcxproj)); - if (!importVcxproj(vcxproj, variables, "", fileFilters, sharedItemsProjects)) { + + mVariables = solutionVariables; + if (!importVcxproj(vcxproj, mVariables, fileFilters)) { errors.emplace_back("failed to load '" + vcxproj + "' from Visual Studio solution"); return false; } @@ -516,6 +506,8 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const bool ImportProject::importSlnx(const std::string& filename, const std::vector& fileFilters) { + debugs.clear(); + tinyxml2::XMLDocument doc; const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); if (error != tinyxml2::XML_SUCCESS) { @@ -534,11 +526,12 @@ bool ImportProject::importSlnx(const std::string& filename, const std::vector variables; - variables["SolutionDir"] = Path::simplifyPath(Path::getPathFromFilename(filename)); + VariablesMap solutionVariables; + setSolution(filename, solutionVariables); + + solutionVariables["VisualStudioVersion"] = "19.0"; bool found = false; - std::vector sharedItemsProjects; auto processProject = [&](const tinyxml2::XMLElement* projectNode) { const char* pathAttribute = projectNode->Attribute("Path"); @@ -551,11 +544,12 @@ bool ImportProject::importSlnx(const std::string& filename, const std::vectorFirstChildElement(); childNode; childNode = childNode->NextSiblingElement()) { - if (std::strcmp(childNode->Name(), "Project") == 0) { - if (!processProject(childNode)) - return false; + // Walk nested Folder/Project nodes recursively + std::function processFolder; + processFolder = [&](const tinyxml2::XMLElement *folder) -> bool { + for (const tinyxml2::XMLElement *child = folder->FirstChildElement(); child; child = child->NextSiblingElement()) { + const char *childName = child->Name(); + if (std::strcmp(childName, "Project") == 0) { + if (!processProject(child)) + return false; + } else if (std::strcmp(childName, "Folder") == 0) { + if (!processFolder(child)) + return false; + } } - } + return true; + }; + if (!processFolder(node)) + return false; } } @@ -586,360 +591,1076 @@ bool ImportProject::importSlnx(const std::string& filename, const std::vectorAttribute("Include"); + if (a) + name = a; + for (const tinyxml2::XMLElement *e = cfg->FirstChildElement(); e; e = e->NextSiblingElement()) { + const char * const text = e->GetText(); + if (!text) + continue; + const char * ename = e->Name(); + if (std::strcmp(ename,"Configuration")==0) + configuration = text; + else if (std::strcmp(ename,"Platform")==0) { + platformStr = text; + if (platformStr == "Win32") + platform = Win32; + else if (platformStr == "x64") + platform = x64; + else + platform = Unknown; + } + } +} + +void ImportProject::checkUnexpandedExpressions(const std::string &text, const char *context) +{ + std::string::size_type pos = 0; + while ((pos = text.find("$(", pos)) != std::string::npos) { + const std::string::size_type end = text.find(')', pos + 2); + if (end == std::string::npos) + break; + std::stringstream message; + message << "unexpanded property $(" + << text.substr(pos + 2, end - pos - 2) + << ")" + << (context ? " in " : "") + << (context ? context : "") + << ": " << text << '\n'; + debugs.emplace_back(message.str()); + pos = end + 1; + } + pos = 0; + while ((pos = text.find("%(", pos)) != std::string::npos) { + const std::string::size_type end = text.find(')', pos + 2); + if (end == std::string::npos) + break; + std::stringstream message; + message << "unexpanded metadata %(" + << text.substr(pos + 2, end - pos - 2) + << ")" + << (context ? " in " : "") + << (context ? context : "") + << ": " << text << '\n'; + debugs.emplace_back(message.str()); + pos = end + 1; + } +} + namespace { - struct ProjectConfiguration { - ProjectConfiguration() = default; - explicit ProjectConfiguration(const tinyxml2::XMLElement *cfg) { - const char *a = cfg->Attribute("Include"); - if (a) - name = a; - for (const tinyxml2::XMLElement *e = cfg->FirstChildElement(); e; e = e->NextSiblingElement()) { - const char * const text = e->GetText(); - if (!text) - continue; - const char * ename = e->Name(); - if (std::strcmp(ename,"Configuration")==0) - configuration = text; - else if (std::strcmp(ename,"Platform")==0) { - platformStr = text; - if (platformStr == "Win32") - platform = Win32; - else if (platformStr == "x64") - platform = x64; - else - platform = Unknown; - } + // see https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-conditions + class ConditionParser { + public: + ConditionParser(const std::string &condition, const VariablesMap &variables) + : mCondition(condition), mVariables(variables) {} + + bool parse() { + const std::string value = parseOr(); + + skipWhitespace(); + + if (mPos != mCondition.size()) { + if (mCondition[mPos] == ')') + throw std::runtime_error("unmatched ')' in condition " + mCondition); + + throw std::runtime_error("Invalid condition: '" + mCondition + "'"); } + + if (value != "True" && value != "False") + throw std::runtime_error("Invalid condition: '" + mCondition + "'"); + + return value == "True"; } - std::string name; - std::string configuration; - enum : std::uint8_t { Win32, x64, Unknown } platform = Unknown; - std::string platformStr; - }; - struct Conditional { - explicit Conditional(const tinyxml2::XMLElement *idg){ - const char *condAttr = idg->Attribute("Condition"); - if (condAttr) - mCondition = condAttr; + private: + const std::string &mCondition; + const VariablesMap &mVariables; + std::size_t mPos = 0; + + void skipWhitespace() { + while (mPos < mCondition.size() && std::isspace(static_cast(mCondition[mPos]))) + ++mPos; + } + + bool match(const std::string &text) { + skipWhitespace(); + if (mCondition.compare(mPos, text.size(), text) != 0) + return false; + mPos += text.size(); + return true; + } + + bool matchWord(const std::string &word) { + skipWhitespace(); + if (mCondition.size() - mPos < word.size()) + return false; + if (caseInsensitiveStringCompare(mCondition.substr(mPos, word.size()), word) != 0) + return false; + + const std::size_t end = mPos + word.size(); + if (end < mCondition.size() && + (std::isalnum(static_cast(mCondition[end])) || mCondition[end] == '_')) + return false; + + mPos = end; + return true; } - explicit Conditional(std::string condition) : mCondition(std::move(condition)) {} - static void replaceAll(std::string &c, const std::string &from, const std::string &to) { - std::string::size_type pos; - while ((pos = c.find(from)) != std::string::npos) { - c.erase(pos,from.size()); - c.insert(pos,to); + void expect(const std::string &text) { + if (match(text)) + return; + + if (text == ")") + throw std::runtime_error("'(' without closing ')'!"); + + throw std::runtime_error("Expected '" + text + "' in condition '" + mCondition + "'"); + } + + std::string parseOr() { + std::string lhs = parseAnd(); + while (matchWord("or") || match("||")) { + const std::string rhs = parseAnd(); + lhs = rhs == "True" ? "True" : (lhs == "True" ? "True" : "False"); } + return lhs; } - // 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 std::string &filename, std::vector &errors) const { - if (mCondition.empty()) - return true; - try { - return evalCondition(mCondition, p); + std::string parseAnd() { + std::string lhs = parseUnary(); + while (matchWord("and") || match("&&")) { + const std::string rhs = parseUnary(); + lhs = (lhs == "True" && rhs == "True") ? "True" : "False"; } - catch (const std::runtime_error& r) - { - errors.emplace_back(filename + ": Can not evaluate condition '" + mCondition + "': " + r.what()); - return false; + return lhs; + } + + std::string parseUnary() { + if (match("!")) { + skipWhitespace(); + if (mPos == mCondition.size()) + throw std::runtime_error("Invalid condition: '" + mCondition + "'"); + + return parseUnary() == "False" ? "True" : "False"; } + + return parsePrimary(); } - static bool evalCondition(const std::string& condition, const ProjectConfiguration &p) { - std::string c = '(' + condition + ")\n"; - replaceAll(c, "$(Configuration)", p.configuration); - replaceAll(c, "$(Platform)", p.platformStr); + std::string parsePrimary() { + skipWhitespace(); - const Settings s; - TokenList tokenlist(s, Standards::Language::C); - if (!tokenlist.createTokensFromBuffer(c.data(), c.size())) { - throw std::runtime_error("Can not tokenize condition"); + if (match("(")) { + std::string value = parseOr(); + expect(")"); + return value; } - // generate links - { - std::stack lpar; - for (Token* tok2 = tokenlist.front(); tok2; tok2 = tok2->next()) { - if (tok2->str() == "(") - lpar.push(tok2); - else if (tok2->str() == ")") { - if (lpar.empty()) - throw std::runtime_error("unmatched ')' in condition " + condition); - Token::createMutualLinks(lpar.top(), tok2); - lpar.pop(); - } - } - if (!lpar.empty()) - throw std::runtime_error("'(' without closing ')'!"); + if (matchWord("Exists")) + return parseExists(); + + if (matchWord("And") || matchWord("Or") || match("!")) + throw std::runtime_error("Invalid condition: '" + mCondition + "'"); + + if (matchWord("HasTrailingSlash")) + return parseHasTrailingSlash(); + + return parseComparison(); + } + + std::string parseComparison() { + const std::string lhs = parseValue(); + skipWhitespace(); + + static constexpr const char *ops[] = { "==", "!=", "<=", ">=", "<", ">" }; + for (const char *op : ops) { + // cppcheck-suppress useStlAlgorithm + if (match(op)) + return compare(lhs, op, parseValue()) ? "True" : "False"; } - // Replace "And" and "Or" with "&&" and "||" - for (Token *tok = tokenlist.front(); tok; tok = tok->next()) { - if (tok->str() == "And") - tok->str("&&"); - else if (tok->str() == "Or") - tok->str("||"); + return lhs; + } + + std::string parseValue() { + skipWhitespace(); + + if (mPos >= mCondition.size()) + throw std::runtime_error("Missing operator"); + + if (matchWord("true")) + return "True"; + + if (matchWord("false")) + return "False"; + + if (mCondition[mPos] == '\'') + return parseString(); + + if (mCondition.compare(mPos, 2, "$(") == 0) + return parsePropertyExpression(); + + if (std::isdigit(static_cast(mCondition[mPos])) || + (mCondition[mPos] == '-' && + mPos + 1 < mCondition.size() && std::isdigit(static_cast(mCondition[mPos + 1])))) { + const std::size_t begin = mPos++; + + while (mPos < mCondition.size() && std::isdigit(static_cast(mCondition[mPos]))) + ++mPos; + + return mCondition.substr(begin, mPos - begin); } - tokenlist.createAst(); + const std::size_t begin = mPos; + while (mPos < mCondition.size()) { + const auto c = static_cast(mCondition[mPos]); + if (!std::isalnum(c) && c != '_' && c != '-' && c != '.') + break; + ++mPos; + } - // Locate ast top and execute the condition - for (const Token *tok = tokenlist.front(); tok; tok = tok->next()) { - if (tok->astParent()) { - return execute(tok->astTop(), p) == "True"; - } + if (mPos != begin) + return mCondition.substr(begin, mPos - begin); + throw std::runtime_error("Unknown/unhandled operator/operand '" + mCondition.substr(mPos) + "'"); + } + + std::string parseString() { + ++mPos; + std::string value; + + while (mPos < mCondition.size()) { + const char c = mCondition[mPos++]; + + if (c == '\'') + return expandProperties(value); + + value += c; } - throw std::runtime_error("Invalid condition: '" + condition + "'"); + + throw std::runtime_error("Can not tokenize condition"); } + static bool parseInteger(const std::string &s, long &value) + { + if (s.empty()) + return false; - private: + const char *begin = s.c_str(); + char *end = nullptr; + int base = 10; - static std::string executeOp1(const Token* tok, const ProjectConfiguration &p) { - return execute(tok->astOperand1(), p); + if (s.size() > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { + begin += 2; + if (*begin == '\0') + return false; + base = 16; + } + + value = std::strtol(begin, &end, base); + return end != begin && *end == '\0'; } - static std::string executeOp2(const Token* tok, const ProjectConfiguration &p) { - return execute(tok->astOperand2(), p); + std::string parseIdentifier() { + skipWhitespace(); + const std::size_t begin = mPos; + while (mPos < mCondition.size()) { + const auto c = static_cast(mCondition[mPos]); + if (!std::isalnum(c) && c != '_' && c != '-') + break; + ++mPos; + } + if (begin == mPos) + throw std::runtime_error("Expected identifier in condition '" + mCondition + "'"); + return mCondition.substr(begin, mPos - begin); + } + + std::string parsePropertyExpression() { + expect("$("); + std::string value = getPropertyValue(parseIdentifier()); + + while (true) { + skipWhitespace(); + if (!match(".")) + break; + + const std::string method = parseIdentifier(); + expect("("); + std::vector args; + skipWhitespace(); + if (!match(")")) { + do { + args.push_back(parseValue()); + } while (match(",")); + expect(")"); + } + value = applyMethod(value, method, args); + } + + expect(")"); + return value; } - static std::string execute(const Token* tok, const ProjectConfiguration &p) { - if (!tok) - throw std::runtime_error("Missing operator"); - auto boolResult = [](bool b) -> std::string { - return b ? "True" : "False"; - }; - if (tok->isUnaryOp("!")) - return boolResult(executeOp1(tok, p) == "False"); - if (tok->str() == "==") - return boolResult(executeOp1(tok, p) == executeOp2(tok, p)); - if (tok->str() == "!=") - return boolResult(executeOp1(tok, p) != executeOp2(tok, p)); - if (tok->str() == "&&") - return boolResult(executeOp1(tok, p) == "True" && executeOp2(tok, p) == "True"); - if (tok->str() == "||") - return boolResult(executeOp1(tok, p) == "True" || executeOp2(tok, p) == "True"); - if (tok->str() == "(" && Token::Match(tok->previous(), "$ ( %name% . %name% (")) { - const std::string& propertyName = tok->strAt(1); - std::string propertyValue; - if (propertyName == "Configuration") - propertyValue = p.configuration; - else if (propertyName == "Platform") - propertyValue = p.platformStr; - else - throw std::runtime_error("Unhandled property '" + propertyName + "'"); - const std::string& method = tok->strAt(3); - std::string arg = executeOp2(tok->tokAt(4), p); - if (arg.size() >= 2 && arg[0] == '\'') - arg = arg.substr(1, arg.size() - 2); - if (method == "Contains") - return boolResult(propertyValue.find(arg) != std::string::npos); - if (method == "EndsWith") - return boolResult(endsWith(propertyValue,arg.c_str(),arg.size())); - if (method == "StartsWith") - return boolResult(startsWith(propertyValue,arg)); - throw std::runtime_error("Unhandled method '" + method + "'"); + std::string parseExists() { + expect("("); + const std::string filename = parseValue(); + expect(")"); + + std::string path = filename; + if (!Path::isAbsolute(path)) { + auto it = mVariables.find("MSBuildThisFileDirectory"); + if (it == mVariables.end()) + it = mVariables.find("ProjectDir"); + if (it != mVariables.end()) + path = it->second + path; } - if (tok->str().size() >= 2 && tok->str()[0] == '\'') // String Literal - return tok->str(); - throw std::runtime_error("Unknown/unhandled operator/operand '" + tok->str() + "'"); + return (Path::isFile(path) || Path::isDirectory(path)) ? "True" : "False"; } - std::string mCondition; - }; + std::string parseHasTrailingSlash() { + expect("("); + const std::string value = parseValue(); + expect(")"); - struct ItemDefinitionGroup : Conditional { - explicit ItemDefinitionGroup(const tinyxml2::XMLElement *idg, std::string includePaths) : Conditional(idg), additionalIncludePaths(std::move(includePaths)) { - for (const tinyxml2::XMLElement *e1 = idg->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { - const char* name = e1->Name(); - if (std::strcmp(name, "ClCompile") == 0) { - enhancedInstructionSet = "StreamingSIMDExtensions2"; - for (const tinyxml2::XMLElement *e = e1->FirstChildElement(); e; e = e->NextSiblingElement()) { - const char * const text = e->GetText(); - if (!text) - continue; - const char * const ename = e->Name(); - if (std::strcmp(ename, "PreprocessorDefinitions") == 0) - preprocessorDefinitions = text; - else if (std::strcmp(ename, "AdditionalIncludeDirectories") == 0) { - if (!additionalIncludePaths.empty()) - additionalIncludePaths += ';'; - additionalIncludePaths += text; - } else if (std::strcmp(ename, "LanguageStandard") == 0) { - if (std::strcmp(text, "stdcpp14") == 0) - cppstd = Standards::CPP14; - else if (std::strcmp(text, "stdcpp17") == 0) - cppstd = Standards::CPP17; - else if (std::strcmp(text, "stdcpp20") == 0) - cppstd = Standards::CPP20; - else if (std::strcmp(text, "stdcpplatest") == 0) - cppstd = Standards::CPPLatest; - } else if (std::strcmp(ename, "EnableEnhancedInstructionSet") == 0) { - enhancedInstructionSet = text; - } - } - } - else if (std::strcmp(name, "Link") == 0) { - for (const tinyxml2::XMLElement *e = e1->FirstChildElement(); e; e = e->NextSiblingElement()) { - const char * const text = e->GetText(); - if (!text) - continue; - if (std::strcmp(e->Name(), "EntryPointSymbol") == 0) { - entryPointSymbol = text; - } - } + return (!value.empty() && (value.back() == '/' || value.back() == '\\')) + ? "True" + : "False"; + } + + std::string getPropertyValue(const std::string &name) const { + const auto it = mVariables.find(name); + if (it != mVariables.end()) + return it->second; + + const char *envValue = std::getenv(name.c_str()); + return envValue ? envValue : ""; + } + + std::string expandProperties(const std::string &input) const { + std::string result = input; + std::size_t pos = 0; + while ((pos = result.find("$(", pos)) != std::string::npos) { + const std::size_t begin = pos + 2; + const std::size_t end = result.find(')', begin); + if (end == std::string::npos) + break; + const std::string name = result.substr(begin, end - begin); + // Only expand simple property references here. Property methods + // are parsed by parsePropertyExpression(). + if (name.find_first_of("()") != std::string::npos) { + pos = end + 1; + continue; } + const std::string value = getPropertyValue(name); + result.replace(pos, end - pos + 1, value); + pos += value.size(); } + return result; } - std::string enhancedInstructionSet; - std::string preprocessorDefinitions; - std::string additionalIncludePaths; - std::string entryPointSymbol; // TODO: use this - Standards::cppstd_t cppstd = Standards::CPPLatest; - }; + static std::string applyMethod(std::string value, + const std::string &method, + const std::vector &args) { + if (caseInsensitiveStringCompare(method, "ToUpper") == 0) { + if (!args.empty()) + throw std::runtime_error("ToUpper takes no arguments"); + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { + return std::toupper(c); + }); + return value; + } - struct ConfigurationPropertyGroup : Conditional { - explicit ConfigurationPropertyGroup(const tinyxml2::XMLElement *idg) : Conditional(idg) { - for (const tinyxml2::XMLElement *e = idg->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (std::strcmp(e->Name(), "UseOfMfc") == 0) { - useOfMfc = true; - } else if (std::strcmp(e->Name(), "CharacterSet") == 0) { - useUnicode = std::strcmp(e->GetText(), "Unicode") == 0; + if (caseInsensitiveStringCompare(method, "ToLower") == 0) { + if (!args.empty()) + throw std::runtime_error("ToLower takes no arguments"); + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { + return std::tolower(c); + }); + return value; + } + + if (caseInsensitiveStringCompare(method, "Contains") == 0) { + if (args.size() != 1) + throw std::runtime_error("Contains requires one argument"); + return value.find(args[0]) != std::string::npos ? "True" : "False"; + } + + if (caseInsensitiveStringCompare(method, "StartsWith") == 0) { + if (args.size() != 1) + throw std::runtime_error("StartsWith requires one argument"); + return startsWith(value, args[0]) ? "True" : "False"; + } + + if (caseInsensitiveStringCompare(method, "EndsWith") == 0) { + if (args.size() != 1) + throw std::runtime_error("EndsWith requires one argument"); + return endsWith(value, args[0].c_str(), args[0].size()) ? "True" : "False"; + } + + if (caseInsensitiveStringCompare(method, "Trim") == 0) { + if (args.empty()) { + const std::size_t first = value.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) + return ""; + + const std::size_t last = value.find_last_not_of(" \t\r\n"); + return value.substr(first, last - first + 1); } + + std::string chars; + for (const std::string &arg : args) + chars += arg; + + const std::size_t first = value.find_first_not_of(chars); + if (first == std::string::npos) + return ""; + + const std::size_t last = value.find_last_not_of(chars); + return value.substr(first, last - first + 1); } - } - bool useOfMfc = false; - bool useUnicode = false; - }; + if (caseInsensitiveStringCompare(method, "TrimStart") == 0) { + if (args.empty()) { + const std::size_t first = value.find_first_not_of(" \t\r\n"); + return first == std::string::npos ? "" : value.substr(first); + } - struct ItemGroupClCompile { - explicit ItemGroupClCompile(std::string filename) : mFilename(std::move(filename)) {} - ItemGroupClCompile(const tinyxml2::XMLElement *element, std::string file) : mFilename(std::move(file)) { - for (const tinyxml2::XMLElement* childElement = element->FirstChildElement(); childElement; childElement = childElement->NextSiblingElement()) { - const char *name = childElement->Name(); - if (!name) - continue; - if (std::strcmp(name, "ExcludedFromBuild") == 0) { - const char *condition = childElement->Attribute("Condition"); - const char *text = childElement->GetText(); - if (!condition || !text || std::strcmp(text, "true") != 0) - continue; - mConditions.emplace_back(condition); + std::string chars; + for (const std::string &arg : args) + chars += arg; + + const std::size_t first = value.find_first_not_of(chars); + return first == std::string::npos ? "" : value.substr(first); + } + + if (caseInsensitiveStringCompare(method, "TrimEnd") == 0) { + if (args.empty()) { + const std::size_t last = value.find_last_not_of(" \t\r\n"); + return last == std::string::npos ? "" : value.substr(0, last + 1); } - // TODO: ForcedIncludeFiles and PrecompiledHeaderFile + + std::string chars; + for (const std::string &arg : args) + chars += arg; + + const std::size_t last = value.find_last_not_of(chars); + return last == std::string::npos ? "" : value.substr(0, last + 1); + } + + if (caseInsensitiveStringCompare(method, "Substring") == 0) { + if (args.size() != 1 && args.size() != 2) + throw std::runtime_error("Substring requires one or two arguments"); + + char *end = nullptr; + const long start = std::strtol(args[0].c_str(), &end, 10); + if (end == args[0].c_str() || *end != '\0') + throw std::runtime_error("Invalid Substring start index"); + + if (start < 0 || + static_cast(start) > value.size()) + throw std::runtime_error("Substring start index out of range"); + + const auto index = static_cast(start); + + if (args.size() == 1) + return value.substr(index); + + end = nullptr; + const long length = std::strtol(args[1].c_str(), &end, 10); + if (end == args[1].c_str() || *end != '\0') + throw std::runtime_error("Invalid Substring length"); + + if (length < 0 || + static_cast(length) > value.size() - index) + throw std::runtime_error("Substring length out of range"); + + return value.substr(index, static_cast(length)); } + + if (caseInsensitiveStringCompare(method, "Replace") == 0) { + if (args.size() != 2) + throw std::runtime_error("Replace requires two arguments"); + + if (args[0].empty()) + throw std::runtime_error("Replace search string cannot be empty"); + + std::size_t pos = 0; + while ((pos = value.find(args[0], pos)) != std::string::npos) { + value.replace(pos, args[0].size(), args[1]); + pos += args[1].size(); + } + return value; + } + + throw std::runtime_error("Unhandled method '" + method + "'"); } - bool exclude(const ProjectConfiguration& p, std::vector& errors) const { - if (mConditions.empty()) - return false; - for (const std::string& condition : mConditions) { - Conditional conditional(condition); - if (conditional.conditionIsTrue(p, mFilename, errors)) - return true; + + static int compareVersions(const std::vector &lhs, + const std::vector &rhs) { + const std::size_t count = std::max(lhs.size(), rhs.size()); + + for (std::size_t i = 0; i < count; ++i) { + if (i >= lhs.size()) + return -1; + + if (i >= rhs.size()) + return 1; + + if (lhs[i] < rhs[i]) + return -1; + + if (lhs[i] > rhs[i]) + return 1; } + + return 0; + } + + static bool compareVersionResult(int result, const std::string &op) { + if (op == "<") + return result < 0; + if (op == ">") + return result > 0; + if (op == "<=") + return result <= 0; + if (op == ">=") + return result >= 0; return false; } - std::string mFilename; - std::list mConditions; + + static bool compare(const std::string &lhs, const std::string &op, const std::string &rhs) { + const auto parseVersion = [](const std::string &s) -> std::vector { + if (s.empty()) + return {}; + + std::size_t pos = (s[0] == 'v' || s[0] == 'V') ? 1 : 0; + if (pos == s.size()) + return {}; + + std::vector parts; + while (pos < s.size()) { + const std::size_t dot = s.find('.', pos); + const std::size_t end = + dot == std::string::npos ? s.size() : dot; + + if (end == pos) + return {}; + + const std::string part = s.substr(pos, end - pos); + char *endPtr = nullptr; + const long value = std::strtol(part.c_str(), &endPtr, 10); + + if (endPtr != part.c_str() && *endPtr == '\0') + parts.push_back(static_cast(value)); + else + return {}; + + if (dot == std::string::npos) + break; + + pos = dot + 1; + } + + if (parts.empty() || parts.size() > 4) + return {}; + + return parts; + }; + + if (op == "==") + return caseInsensitiveStringCompare(lhs, rhs) == 0; + if (op == "!=") + return caseInsensitiveStringCompare(lhs, rhs) != 0; + + if (caseInsensitiveStringCompare(lhs, "Current") == 0) { + const auto rhsVersion = parseVersion(rhs); + if (!rhsVersion.empty()) { + const std::vector currentVersion{ 18 }; + return compareVersionResult(compareVersions(currentVersion, rhsVersion), op); + } + } + + long lhsInt = 0; + long rhsInt = 0; + if (parseInteger(lhs, lhsInt) && parseInteger(rhs, rhsInt)) { + if (op == "<") return lhsInt < rhsInt; + if (op == ">") return lhsInt > rhsInt; + if (op == "<=") return lhsInt <= rhsInt; + if (op == ">=") return lhsInt >= rhsInt; + } + + const std::vector lhsVersion = parseVersion(lhs); + const std::vector rhsVersion = parseVersion(rhs); + + if (!lhsVersion.empty() && !rhsVersion.empty()) + return compareVersionResult(compareVersions(lhsVersion, rhsVersion), op); + + throw std::runtime_error("Cannot compare '" + lhs + "' and '" + rhs + "'"); + } + }; + + bool evalCondition(const std::string &condition, const VariablesMap &variables) { + return ConditionParser(condition, variables).parse(); + } + + bool conditionIsTrue(const tinyxml2::XMLElement *node, const VariablesMap &variables) { + const char *condAttr = node->Attribute("Condition"); + if (!condAttr) + return true; + return evalCondition(condAttr, variables); + } + + bool hasName(const tinyxml2::XMLElement *node, const char *nodeName, const VariablesMap &variables) { + const char *name = node->Name(); + if (!name || std::strcmp(nodeName, name) != 0) + return false; + return conditionIsTrue(node, variables); + } + + bool hasNameAndAttribute(const tinyxml2::XMLElement *node, const char *nodeName, const char *attrName, const VariablesMap &variables) { + const char *name = node->Name(); + const char *attr = node->Attribute(attrName); + if (!name || !attr || std::strcmp(nodeName, name) != 0) + return false; + return conditionIsTrue(node, variables); + } + + bool hasNameAndLabel(const tinyxml2::XMLElement *node, const char *nodeName, const char *nodeAttr, const VariablesMap &variables) { + const char *name = node->Name(); + const char *label = node->Attribute("Label"); + if (!name || !label || std::strcmp(nodeName, name) != 0 || std::strcmp(label, nodeAttr) != 0) + return false; + return conditionIsTrue(node, variables); + } + + bool hasNameAndNotLabel(const tinyxml2::XMLElement *node, const char *nodeName, const char *nodeAttr, const VariablesMap &variables) { + const char *name = node->Name(); + if (!name || std::strcmp(nodeName, name) != 0) + return false; + const char *label = node->Attribute("Label"); + if (label && std::strcmp(label, nodeAttr) == 0) + return false; + return conditionIsTrue(node, variables); + } + + std::list toStringList(const std::string &s) + { + std::list ret; + std::string::size_type pos1 = 0; + std::string::size_type pos2; + while ((pos2 = s.find(';',pos1)) != std::string::npos) { + ret.push_back(s.substr(pos1, pos2-pos1)); + pos1 = pos2 + 1; + if (pos1 >= s.size()) + break; + } + if (pos1 < s.size()) + ret.push_back(s.substr(pos1)); + return ret; + } + + std::string findFile(const std::string &startDirectory, const std::string &file) + { + std::string currentDir = startDirectory; + if (currentDir.back() == '/' && currentDir.size() > 1 && currentDir[currentDir.size() - 2] != ':') + currentDir.pop_back(); + + while (!currentDir.empty()) { + std::string targetFile = Path::join(currentDir, file); + if (Path::isFile(targetFile)) + return targetFile; + if (currentDir.back() == '/' || (currentDir.back() == ':' && currentDir.size() == 2)) + break; + size_t lastSlash = currentDir.find_last_of('/'); + if (lastSlash == std::string::npos) + break; + currentDir.resize(lastSlash); + } + + return ""; + } + + struct MSBuildThis { + VariablesMap &variablesMap; + std::string thisFile; + std::string thisFileName; + std::string thisFileExtension; + std::string thisFileDirectory; + std::string thisFileDirectoryNoRoot; + std::string thisFileFullPath; + + MSBuildThis(const std::string &filename, VariablesMap &variables) + : variablesMap(variables) + , thisFile(variables.at("MSBuildThisFile")) + , thisFileName(variables.at("MSBuildThisFileName")) + , thisFileExtension(variables.at("MSBuildThisFileExtension")) + , thisFileDirectory(variables.at("MSBuildThisFileDirectory")) + , thisFileDirectoryNoRoot(variables.at("MSBuildThisFileDirectoryNoRoot")) + , thisFileFullPath(variables.at("MSBuildThisFileFullPath")) { + setMSBuildThis(filename, variables); + } + + static void setMSBuildThis(const std::string &filename, VariablesMap &variables) { + variables["MSBuildThisFileFullPath"] = filename; + std::string temp = filename.substr(filename.rfind('/') + 1, filename.size()); + variables["MSBuildThisFile"] = temp; + findAndReplace(temp, Path::getFilenameExtension(temp), ""); + variables["MSBuildThisFileName"] = temp; + variables["MSBuildThisFileDirectory"] = Path::simplifyPath(Path::getPathFromFilename(filename)); + temp = Path::simplifyPath(Path::getPathFromFilename(filename)); + std::string::size_type pos = filename.find('/', 0); + temp.erase(0, pos + 1); + variables["MSBuildThisFileDirectoryNoRoot"] = temp; + variables["MSBuildThisFileExtension"] = Path::getFilenameExtensionInLowerCase(filename); + } + + ~MSBuildThis() { + variablesMap["MSBuildThisFile"] = thisFile; + variablesMap["MSBuildThisFileName"] = thisFileName; + variablesMap["MSBuildThisFileExtension"] = thisFileExtension; + variablesMap["MSBuildThisFileDirectory"] = thisFileDirectory; + variablesMap["MSBuildThisFileDirectoryNoRoot"] = thisFileDirectoryNoRoot; + variablesMap["MSBuildThisFileFullPath"] = thisFileFullPath; + } + }; + + struct ImportStackGuard { + std::unordered_set &mStack; + std::string mKey; + + ImportStackGuard(std::unordered_set &stack, std::string key) + : mStack(stack), mKey(std::move(key)) {} + + ~ImportStackGuard() { + mStack.erase(mKey); + } }; } -static std::list toStringList(const std::string &s) +std::string ImportProject::toAbsolute(const std::string &path) { - std::list ret; - std::string::size_type pos1 = 0; - std::string::size_type pos2; - while ((pos2 = s.find(';',pos1)) != std::string::npos) { - ret.push_back(s.substr(pos1, pos2-pos1)); - pos1 = pos2 + 1; - if (pos1 >= s.size()) - break; - } - if (pos1 < s.size()) - ret.push_back(s.substr(pos1)); - return ret; + if (Path::isAbsolute(path)) + return Path::simplifyPath(path); + return Path::simplifyPath(Path::getCurrentPath() + "/" + path); } -static void importPropertyGroup(const tinyxml2::XMLElement *node, std::map &variables, std::string &includePath) +std::string ImportProject::toAbsolute(const std::string &filename, const std::string &baseDir, VariablesMap &variables) { - const char* labelAttribute = node->Attribute("Label"); - if (labelAttribute && std::strcmp(labelAttribute, "UserMacros") == 0) { - for (const tinyxml2::XMLElement *propertyGroup = node->FirstChildElement(); propertyGroup; propertyGroup = propertyGroup->NextSiblingElement()) { - const char* name = propertyGroup->Name(); - const char *text = empty_if_null(propertyGroup->GetText()); - variables[name] = text; + std::string resolved(filename); + if (!simplifyPathWithVariables(resolved, variables)) + return resolved; + + if (Path::isAbsolute(resolved)) + return Path::simplifyPath(resolved); + return Path::simplifyPath(baseDir + resolved); +} + +bool ImportProject::simplifyPathWithVariables(std::string &s, VariablesMap &variables) +{ + expandMSBuildVariables(s, variables); + checkUnexpandedExpressions(s, "path"); + if (s.find("$(") != std::string::npos) + return false; + s = Path::simplifyPath(std::move(s)); + return true; +} + +void ImportProject::fsSetIncludePaths(FileSettings &fs, const std::string &basepath, const std::list &in, VariablesMap &variables) +{ + std::set found; + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + const std::list copyIn(in); + fs.includePaths.clear(); + for (const std::string &ipath : copyIn) { + if (ipath.empty()) + continue; + if (startsWith(ipath, "%(")) + continue; + std::string s(Path::fromNativeSeparators(ipath)); + if (!found.insert(s).second) + continue; + if (s[0] == '/' || (s.size() > 1U && s.compare(1, 2, ":/") == 0)) { + if (!endsWith(s, '/')) + s += '/'; + fs.includePaths.push_back(std::move(s)); + continue; } - } else if (!labelAttribute) { - for (const tinyxml2::XMLElement *propertyGroup = node->FirstChildElement(); propertyGroup; propertyGroup = propertyGroup->NextSiblingElement()) { - if (std::strcmp(propertyGroup->Name(), "IncludePath") != 0) - continue; - const char *text = propertyGroup->GetText(); - if (!text) + if (endsWith(s, '/')) // this is a temporary hack, simplifyPath can crash if path ends with '/' + s.pop_back(); + + if (s.find("$(") == std::string::npos) { + s = Path::simplifyPath(basepath + s); + } else { + if (!simplifyPathWithVariables(s, variables)) continue; - std::string path(text); - const std::string::size_type pos = path.find("$(IncludePath)"); - if (pos != std::string::npos) - path.replace(pos, 14U, includePath); - includePath = std::move(path); } + if (s.empty()) + continue; + fs.includePaths.push_back(s.back() == '/' ? s : (s + '/')); + } +} + +void ImportProject::addProperty(const tinyxml2::XMLElement *node, VariablesMap &variables) { + const char *eName = node->Name(); + if (!eName || !conditionIsTrue(node, variables)) + return; + const char *eText = node->GetText(); + std::string text(eText ? eText : ""); + const std::string original = variables[eName]; + findAndReplace(text, "%(" + std::string(eName) + ")", original); + expandMSBuildVariables(text, variables); + variables[eName] = text; + checkUnexpandedExpressions(text, eName); +} + +std::string ImportProject::getProperty(const tinyxml2::XMLElement *node, VariablesMap &variables, const std::string &original) { + const char *eName = node->Name(); + const char *eText = node->GetText(); + if (!eName || !eText || !conditionIsTrue(node, variables)) + return original; + std::string text(eText); + findAndReplace(text, "%(" + std::string(eName) + ")", original); + expandMSBuildVariables(text, variables); + checkUnexpandedExpressions(text, eName); + return text; +} + +const std::string &ImportProject::importResultStr(ImportProject::ImportResult result) { + static std::string ok("ok"); + static std::string notResolvable("Not Resolvable"); + static std::string notFound("Not Found"); + static std::string notValid("Not Valid"); + static std::string cycle("Cycle"); + static std::string unknown("Unknown"); + + switch (result) { + case ImportProject::ImportResult::Ok: + return ok; + case ImportProject::ImportResult::NotResolvable: + return notResolvable; + case ImportProject::ImportResult::NotFound: + return notFound; + case ImportProject::ImportResult::NotValid: + return notValid; + case ImportProject::ImportResult::Cycle: + return cycle; + } + return unknown; +} + +ImportProject::ImportResult ImportProject::importCompile(const tinyxml2::XMLElement *node, + const std::string &projectDir, + VariablesMap &variables, + std::list &compileList) { + const char *include = node->Attribute("Include"); + if (!include) + return ImportResult::NotFound; + + std::string toInclude = toAbsolute(include, projectDir, variables); + if (!Path::acceptFile(toInclude)) + return ImportResult::NotFound; + + ItemGroupClCompile compile(toInclude); + // a file with no override of its own inherits the ItemDefinitionGroup value outright + compile.additionalIncludeDirectories = variables["AdditionalIncludeDirectories"]; + compile.forcedIncludeFiles = variables["ForcedIncludeFiles"]; + compile.preprocessorDefinitions = variables["PreprocessorDefinitions"]; + compile.languageStandard = variables["LanguageStandard"]; + bool excludedFromBuild = false; + + for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { + const char *text = e1->GetText(); + if (!text) + continue; + + if (hasName(e1, "ExcludedFromBuild", variables)) { + if (caseInsensitiveStringCompare(text, "true") == 0) { + excludedFromBuild = true; + break; + } + } else if (hasName(e1, "AdditionalIncludeDirectories", variables)) { + compile.additionalIncludeDirectories = getProperty(e1, variables, compile.additionalIncludeDirectories); + } else if (hasName(e1, "ForcedIncludeFiles", variables)) { + compile.forcedIncludeFiles = getProperty(e1, variables, compile.forcedIncludeFiles); + } else if (hasName(e1, "PreprocessorDefinitions", variables)) { + compile.preprocessorDefinitions = getProperty(e1, variables, compile.preprocessorDefinitions); + } else if (hasName(e1, "LanguageStandard", variables)) { + compile.languageStandard = getProperty(e1, variables, compile.languageStandard); + } + } + + if (!excludedFromBuild) + compileList.emplace_back(compile); + + return ImportResult::Ok; +} + +ImportProject::ImportResult ImportProject::importProject(const tinyxml2::XMLElement *node, + const std::string &projectDir, + VariablesMap &variables, + std::list &projectConfigurationList, + std::unordered_set &importStack) { + const char *projectAttribute = node->Attribute("Project"); + if (!projectAttribute) + return ImportResult::Ok; + std::string file = toAbsolute(projectAttribute, projectDir, variables); + std::string extension = Path::getFilenameExtensionInLowerCase(file); + if (extension == ".props" || extension == ".targets") { + const char *sdk = node->Attribute("Sdk"); + if (sdk) + return ImportResult::NotResolvable; + ImportResult result = importPropsOrTargets(file, variables, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); + return result; + } + if (result == ImportResult::NotResolvable) { + debugs.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); + } + } else { + debugs.emplace_back("Could not import \"" + file + "\" - "); } + return ImportResult::Ok; } -static void loadVisualStudioProperties(const std::string &props, std::map &variables, std::string &includePath, const std::string &additionalIncludeDirectories, std::list &itemDefinitionGroupList) +ImportProject::ImportResult ImportProject::importPropsOrTargets(const std::string &file, + VariablesMap &variables, + std::list &projectConfigurationList, + std::unordered_set &importStack) { - std::string filename(props); + std::string filename(file); // variables can't be resolved if (!simplifyPathWithVariables(filename, variables)) - return; + return ImportResult::NotResolvable; // prepend project dir (if it exists) to transform relative paths into absolute ones if (!Path::isAbsolute(filename) && variables.count("ProjectDir") > 0) - filename = Path::getAbsoluteFilePath(variables.at("ProjectDir") + filename); + filename = toAbsolute(filename, variables.at("ProjectDir"), variables); + + // detect circular property sheet imports (A imports B, B imports A, a file importing + // itself, ...) instead of recursing until the stack overflows - mirrors MSBuild's own + // import-cycle detection, which errors out rather than looping forever + const std::string simplifiedFilename = Path::simplifyPath(filename); + if (!importStack.insert(simplifiedFilename).second) + return ImportResult::Cycle; + + ImportStackGuard guard(importStack, simplifiedFilename); // erases on any exit from here tinyxml2::XMLDocument doc; if (doc.LoadFile(filename.c_str()) != tinyxml2::XML_SUCCESS) - return; + return ImportResult::NotFound; + const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement(); if (rootnode == nullptr) - return; + return ImportResult::NotValid; + + MSBuildThis msBuildThis(filename, variables); + std::string propsDir = Path::getPathFromFilename(filename); + + ImportResult ret = ImportResult::Ok; for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { - const char* name = node->Name(); - if (std::strcmp(name, "ImportGroup") == 0) { - const char *labelAttribute = node->Attribute("Label"); - if (labelAttribute == nullptr || std::strcmp(labelAttribute, "PropertySheets") != 0) - continue; - for (const tinyxml2::XMLElement *importGroup = node->FirstChildElement(); importGroup; importGroup = importGroup->NextSiblingElement()) { - if (std::strcmp(importGroup->Name(), "Import") == 0) { - const char *projectAttribute = importGroup->Attribute("Project"); - if (projectAttribute == nullptr) - continue; - std::string loadprj(projectAttribute); - if (loadprj.find('$') == std::string::npos) { - loadprj = Path::getPathFromFilename(filename) + loadprj; + if (hasName(node, "ImportGroup", variables)) { + // Accept any (PropertySheets, Shared, unlabeled) — .targets files + // commonly use unlabeled or differently-labeled groups for transitive imports. + const char* label = node->Attribute("Label"); + const bool isPropertySheets = (label == nullptr) || + (std::strcmp(label, "PropertySheets") == 0) || + (std::strcmp(label, "Shared") == 0); + if (isPropertySheets) { + for (const tinyxml2::XMLElement *importGroup = node->FirstChildElement(); importGroup; importGroup = importGroup->NextSiblingElement()) { + if (hasNameAndAttribute(importGroup, "Import", "Project", variables)) { + ImportResult result = importProject(importGroup, propsDir, variables, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) + return result; } - loadVisualStudioProperties(loadprj, variables, includePath, additionalIncludeDirectories, itemDefinitionGroupList); } } - } else if (std::strcmp(name,"PropertyGroup")==0) { - importPropertyGroup(node, variables, includePath); - } else if (std::strcmp(name,"ItemDefinitionGroup")==0) { - itemDefinitionGroupList.emplace_back(node, additionalIncludeDirectories); + } else if (hasName(node, "PropertyGroup", variables)) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) + addProperty(e, variables); + } else if (hasName(node, "ItemDefinitionGroup", variables)) { + for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { + if (hasName(e1, "ClCompile", variables)) { + for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) { + addProperty(e2, variables); + } + } + } + } else if (hasNameAndLabel(node, "ItemGroup", "ProjectConfigurations", variables)) { + for (const tinyxml2::XMLElement *pcNode = node->FirstChildElement("ProjectConfiguration"); pcNode; pcNode = pcNode->NextSiblingElement("ProjectConfiguration")) { + const ProjectConfiguration pc(pcNode); + if (pc.platform != ProjectConfiguration::Unknown) { + projectConfigurationList.emplace_back(pc); + mAllVSConfigs.insert(pc.configuration); + } + } + } else if (hasNameAndAttribute(node, "Import", "Project", variables)) { + ImportResult result = importProject(node, propsDir, variables, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) + return result; + } + } + + return ret; +} + +ImportProject::ImportResult ImportProject::importVcxitems(const std::string &items, + VariablesMap &variables, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack) +{ + std::string filename(items); + // variables can't be resolved + if (!simplifyPathWithVariables(filename, variables)) + return ImportResult::NotResolvable; + + const std::string simplifiedFilename = Path::simplifyPath(filename); + if (!importStack.insert(simplifiedFilename).second) + return ImportResult::Cycle; + + ImportStackGuard guard(importStack, simplifiedFilename); // erases on any exit from here + + tinyxml2::XMLDocument doc; + const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); + if (error != tinyxml2::XML_SUCCESS) + return ImportResult::NotFound; + + const tinyxml2::XMLElement *const rootnode = doc.FirstChildElement(); + if (rootnode == nullptr) + return ImportResult::NotValid; + + const std::string itemsDir = Path::simplifyPath(Path::getPathFromFilename(filename)); + MSBuildThis msBuildThis(filename, variables); + + ImportResult ret = ImportResult::Ok; + for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { + if (hasName(node, "ItemGroup", variables)) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { + if (hasName(e, "ClCompile", variables)) { + importCompile(e, itemsDir, variables, compileList); + } + } + } else if (hasName(node, "ItemDefinitionGroup", variables)) { + for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { + if (hasName(e1, "ClCompile", variables)) { + for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) + addProperty(e2, variables); + } + } + } else if (hasNameAndAttribute(node, "Import", "Project", variables)) { + if (importProject(node, itemsDir, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) + break; } } + + return ret; } bool ImportProject::importVcxproj(const std::string &filename, - std::map &variables, - const std::string &additionalIncludeDirectories, - const std::vector &fileFilters, - std::vector &cache) + VariablesMap &variables, + const std::vector &fileFilters) { tinyxml2::XMLDocument doc; const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); @@ -947,250 +1668,263 @@ bool ImportProject::importVcxproj(const std::string &filename, errors.emplace_back(std::string("Visual Studio project file is not a valid XML - ") + tinyxml2::XMLDocument::ErrorIDToName(error)); return false; } - return importVcxproj(filename, doc, variables, additionalIncludeDirectories, fileFilters, cache); -} -bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::XMLDocument &doc, std::map &variables, const std::string &additionalIncludeDirectories, const std::vector &fileFilters, std::vector &cache) -{ + variables.emplace("VisualStudioVersion", "17.0"); + + variables["ProjectPath"] = filename; + std::string temp = filename.substr(filename.rfind('/') + 1, filename.size()); + variables["ProjectFileName"] = temp; + findAndReplace(temp, Path::getFilenameExtension(temp), ""); + variables["ProjectName"] = temp; + temp.resize(std::min(temp.size(), size_t(16))); + variables["ShortProjectName"] = temp; + variables["ProjectExt"] = Path::getFilenameExtensionInLowerCase(filename); variables["ProjectDir"] = Path::simplifyPath(Path::getPathFromFilename(filename)); + // importVcxproj called directly + if (variables.find("SolutionDir") == variables.end()) { + debugs.clear(); + variables["SolutionDir"] = variables["ProjectDir"]; + } + + variables["MSBuildProjectName"] = variables["ProjectName"]; + variables["MSBuildProjectExtension"] = variables["ProjectExt"]; + variables["MSBuildProjectDirectory"] = variables["ProjectDir"]; + variables["MSBuildProjectFile"] = variables["ProjectFileName"]; + variables["MSBuildProjectFullPath"] = variables["ProjectPath"]; + + // common defaults + variables["IntDir"] = "$(Platform)/$(Configuration)/"; + variables["OutDir"] = "$(SolutionDir)$(Platform)/$(Configuration)/"; + variables["GeneratedFilesDir"] = "$(IntDir)Generated Files/"; + + MSBuildThis::setMSBuildThis(filename, variables); + + std::string projectDir = variables["ProjectDir"]; std::list projectConfigurationList; std::list compileList; - std::list itemDefinitionGroupList; - std::vector configurationPropertyGroups; - std::string includePath; - std::vector sharedItemsProjects; + std::unordered_set importStack; const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement(); if (rootnode == nullptr) { errors.emplace_back("Visual Studio project file has no XML root node"); return false; } + + // Read MSBuildToolsVersion directly from . + // "Current" is the standard value for VS2019+ and is the correct fallback. + const char *toolsVersion = rootnode->Attribute("ToolsVersion"); + variables.emplace("MSBuildToolsVersion", toolsVersion ? toolsVersion : "Current"); + + // find all Visual Studio project configurations for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { - const char* name = node->Name(); - if (std::strcmp(name, "ItemGroup") == 0) { - const char *labelAttribute = node->Attribute("Label"); - if (labelAttribute && std::strcmp(labelAttribute, "ProjectConfigurations") == 0) { - for (const tinyxml2::XMLElement *cfg = node->FirstChildElement(); cfg; cfg = cfg->NextSiblingElement()) { - if (std::strcmp(cfg->Name(), "ProjectConfiguration") == 0) { - const ProjectConfiguration p(cfg); - if (p.platform != ProjectConfiguration::Unknown) { - projectConfigurationList.emplace_back(cfg); - mAllVSConfigs.insert(p.configuration); - } - } - } - } else { - for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (std::strcmp(e->Name(), "ClCompile") == 0) { - const char *include = e->Attribute("Include"); - if (include && Path::acceptFile(include)) { - std::string toInclude = Path::simplifyPath(Path::isAbsolute(include) ? include : Path::getPathFromFilename(filename) + include); - findAndReplace(toInclude, "$(MSBuildThisFileDirectory)", "./"); - compileList.emplace_back(e, toInclude); - } - } + if (hasNameAndLabel(node, "ItemGroup", "ProjectConfigurations", variables)) { + for (const tinyxml2::XMLElement *pcNode = node->FirstChildElement("ProjectConfiguration"); pcNode; pcNode = pcNode->NextSiblingElement("ProjectConfiguration")) { + const ProjectConfiguration pc(pcNode); + if (!pc.configuration.empty()) { // only require a configuration name + projectConfigurationList.emplace_back(pc); + mAllVSConfigs.insert(pc.configuration); } } - } else if (std::strcmp(name, "ItemDefinitionGroup") == 0) { - itemDefinitionGroupList.emplace_back(node, additionalIncludeDirectories); - } else if (std::strcmp(name, "PropertyGroup") == 0) { - const char* labelAttribute = node->Attribute("Label"); - if (labelAttribute && std::strcmp(labelAttribute, "Configuration") == 0) { - configurationPropertyGroups.emplace_back(node); - } else { - importPropertyGroup(node, variables, includePath); + } + } + + // fixme how to do this right + std::string directoryBuildProps = findFile(projectDir, "Directory.Build.props"); + if (!directoryBuildProps.empty()) { + if (importPropsOrTargets(directoryBuildProps, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) { + errors.emplace_back("Could not load property sheet \"" + directoryBuildProps + "\" - it may be missing, invalid, or part of a circular import"); + } + // fixme is this right + std::string forceImportBeforeCppProps = variables["ForceImportBeforeCppProps"]; + if (!forceImportBeforeCppProps.empty()) { + if (importPropsOrTargets(forceImportBeforeCppProps, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) { + errors.emplace_back("Could not load property sheet \"" + forceImportBeforeCppProps + "\" - it may be missing, invalid, or part of a circular import"); } - } else if (std::strcmp(name, "ImportGroup") == 0) { - const char *labelAttribute = node->Attribute("Label"); - if (labelAttribute && std::strcmp(labelAttribute, "PropertySheets") == 0) { + } + +/* fixme are these really all of them ? + std::string forceImportAfterCppDefaultProp = variables["ForceImportAfterCppDefaultProp"]; + if (!forceImportAfterCppDefaultProp.empty()) + importPropsOrTargets(forceImportAfterCppDefaultProp, variables, projectConfigurationList, importStack); + + std::string forceImportAfterCppProps = variables["ForceImportAfterCppProps"]; + if (!forceImportAfterCppProps.empty()) + importPropsOrTargets(forceImportAfterCppProps, variables, projectConfigurationList, importStack); + */ + } + + std::string directoryBuildTargets = findFile(projectDir, "Directory.Build.targets"); + if (!directoryBuildTargets.empty()) { + if (importPropsOrTargets(directoryBuildTargets, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) { + errors.emplace_back("Could not load targets \"" + directoryBuildTargets + "\" - it may be missing, invalid, or part of a circular import"); + } +/* fixme + ForceImportBeforeCppProps + ForceImportAfterCppDefaultProps + ForceImportAfterCppProps + ForceImportBeforeCppTargets + ForceImportAfterCppTargets + */ + } + + VariablesMap originalVariables = variables; + + bool first = true; + + for (const ProjectConfiguration &pc : projectConfigurationList) { + if (!first) { + compileList.clear(); + variables = originalVariables; + } else + first = false; + + variables["Configuration"] = pc.configuration; + variables["Platform"] = pc.platformStr; + + for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { + if (hasNameAndNotLabel(node, "ItemGroup", "ProjectConfigurations", variables)) { for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (std::strcmp(e->Name(), "Import") == 0) { - const char *projectAttribute = e->Attribute("Project"); - if (projectAttribute) - loadVisualStudioProperties(projectAttribute, variables, includePath, additionalIncludeDirectories, itemDefinitionGroupList); + if (hasNameAndAttribute(e, "ClCompile", "Include", variables)) + importCompile(e, projectDir, variables, compileList); + } + } else if (hasName(node, "ItemDefinitionGroup", variables)) { + for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { + if (hasName(e1, "ClCompile", variables)) { + for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) + addProperty(e2, variables); } } - } else if (labelAttribute && std::strcmp(labelAttribute, "Shared") == 0) { - for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (std::strcmp(e->Name(), "Import") == 0) { - const char *projectAttribute = e->Attribute("Project"); - if (projectAttribute) { - // Path to shared items project is relative to current project directory, - // unless the string starts with $(SolutionDir) - std::string pathToSharedItemsFile; - if (std::string(projectAttribute).rfind("$(SolutionDir)", 0) == 0) { - pathToSharedItemsFile = projectAttribute; - } else { - pathToSharedItemsFile = variables["ProjectDir"] + projectAttribute; - } - if (!simplifyPathWithVariables(pathToSharedItemsFile, variables)) { - errors.emplace_back("Could not simplify path to referenced shared items project"); - return false; - } - - SharedItemsProject toAdd = importVcxitems(pathToSharedItemsFile, fileFilters, cache); - if (!toAdd.successful) { - errors.emplace_back("Could not load shared items project \"" + pathToSharedItemsFile + "\" from original path \"" + std::string(projectAttribute) + "\"."); + } else if (hasName(node, "PropertyGroup", variables)) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) + addProperty(e, variables); + } else if (hasName(node, "ImportGroup", variables)) { + const char *labelAttribute = node->Attribute("Label"); + if (labelAttribute && std::strcmp(labelAttribute, "PropertySheets") == 0) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { + if (hasName(e, "Import", variables)) { + const char *projectAttribute = e->Attribute("Project"); + if (!projectAttribute) + continue; + if (importProject(e, projectDir, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) return false; + } + } + } else if (labelAttribute && std::strcmp(labelAttribute, "Shared") == 0) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { + if (hasName(e, "Import", variables)) { + const char *projectAttribute = e->Attribute("Project"); + if (!projectAttribute) + continue; + std::string file = toAbsolute(projectAttribute, projectDir, variables); + std::string extension = Path::getFilenameExtensionInLowerCase(file); + if (extension == ".vcxitems") { + ImportResult result = importVcxitems(file, variables, compileList, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import items \"" + file + "\" - " + importResultStr(result)); + return false; + } + if (result == ImportResult::NotResolvable) { + debugs.emplace_back("Could not import items \"" + file + "\" - " + importResultStr(result)); + } + } else { + debugs.emplace_back("Could not import unknown file type \"" + file + "\""); } - sharedItemsProjects.emplace_back(toAdd); } } } + } else if (hasNameAndAttribute(node, "Import", "Project", variables)) { + if (importProject(node, projectDir, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) + return false; } } - } - // # TODO: support signedness of char via /J (and potential XML option for it)? - // we can only set it globally but in this context it needs to be treated per file - // Include shared items project files - std::vector sharedItemsIncludePaths; - for (const auto& sharedProject : sharedItemsProjects) { - for (const auto &file : sharedProject.sourceFiles) { - std::string pathToFile = Path::simplifyPath(Path::getPathFromFilename(sharedProject.pathToProjectFile) + file); - compileList.emplace_back(pathToFile); - } - for (const auto &p : sharedProject.includePaths) { - std::string path = Path::simplifyPath(Path::getPathFromFilename(sharedProject.pathToProjectFile) + p); - sharedItemsIncludePaths.emplace_back(std::move(path)); - } - } + // # TODO: support signedness of char via /J (and potential XML option for it)? + // we can only set it globally but in this context it needs to be treated per file - // Project files - PathMatch filtermatcher(fileFilters, Path::getCurrentPath()); - for (const ItemGroupClCompile& compile : compileList) { - if (!fileFilters.empty() && !filtermatcher.match(compile.mFilename)) - continue; - - for (const ProjectConfiguration &p : projectConfigurationList) { + // Project files + PathMatch filtermatcher(fileFilters, Path::getCurrentPath()); + for (const ItemGroupClCompile &compile : compileList) { + if (!fileFilters.empty() && !filtermatcher.match(compile.filename)) + continue; if (!guiProject.checkVsConfigs.empty()) { - const bool doChecking = std::any_of(guiProject.checkVsConfigs.cbegin(), guiProject.checkVsConfigs.cend(), [&](const std::string& c) { - return c == p.configuration; + const bool doChecking = std::any_of(guiProject.checkVsConfigs.cbegin(), guiProject.checkVsConfigs.cend(), [&](const std::string &c) { + return c == pc.configuration; }); if (!doChecking) continue; } - // check if the file should be excluded for this configuration - if (compile.exclude(p, errors)) - continue; - - FileSettings fs{ compile.mFilename, Standards::Language::None, 0}; // file will be identified later on - fs.cfg = p.name; + FileSettings fs{ compile.filename, Standards::Language::None, 0 }; // file will be identified later on + fs.cfg = pc.name; // TODO: detect actual MSC version fs.msc = true; fs.defines = "_WIN32=1"; - if (p.platform == ProjectConfiguration::Win32) + if (pc.platform == ProjectConfiguration::Win32) fs.platformType = Platform::Type::Win32W; - else if (p.platform == ProjectConfiguration::x64) { + else if (pc.platform == ProjectConfiguration::x64) { fs.platformType = Platform::Type::Win64; fs.defines += ";_WIN64=1"; } - std::string additionalIncludePaths; - for (const ItemDefinitionGroup &i : itemDefinitionGroupList) { - if (!i.conditionIsTrue(p, compile.mFilename, errors)) - continue; - fs.standard = Standards::getCPP(i.cppstd); - fs.defines += ';' + i.preprocessorDefinitions; - if (i.enhancedInstructionSet == "StreamingSIMDExtensions") - fs.defines += ";__SSE__"; - else if (i.enhancedInstructionSet == "StreamingSIMDExtensions2") - fs.defines += ";__SSE2__"; - else if (i.enhancedInstructionSet == "AdvancedVectorExtensions") - fs.defines += ";__AVX__"; - else if (i.enhancedInstructionSet == "AdvancedVectorExtensions2") - fs.defines += ";__AVX2__"; - else if (i.enhancedInstructionSet == "AdvancedVectorExtensions512") - fs.defines += ";__AVX512__"; - additionalIncludePaths += ';' + i.additionalIncludePaths; - } - bool useUnicode = false; - for (const ConfigurationPropertyGroup &c : configurationPropertyGroups) { - if (!c.conditionIsTrue(p, compile.mFilename, errors)) - continue; - // in msbuild the last definition wins - useUnicode = c.useUnicode; - fs.useMfc = c.useOfMfc; - } - if (useUnicode) { + + Standards::cppstd_t cppstd = Standards::CPPLatest; + const std::string &languageStandard = compile.languageStandard; + if (languageStandard == "stdcpp11") + cppstd = Standards::CPP11; + else if (languageStandard == "stdcpp14") + cppstd = Standards::CPP14; + else if (languageStandard == "stdcpp17") + cppstd = Standards::CPP17; + else if (languageStandard == "stdcpp20") + cppstd = Standards::CPP20; + else if (languageStandard == "stdcpp23") + cppstd = Standards::CPP23; + else if (languageStandard == "stdcpplatest") + cppstd = Standards::CPPLatest; + fs.standard = Standards::getCPP(cppstd); + + std::string enableEnhancedInstructionSet = variables["EnableEnhancedInstructionSet"]; + if (enableEnhancedInstructionSet == "StreamingSIMDExtensions") + fs.defines += ";__SSE__"; + else if (enableEnhancedInstructionSet == "StreamingSIMDExtensions2") + fs.defines += ";__SSE2__"; + else if (enableEnhancedInstructionSet == "AdvancedVectorExtensions") + fs.defines += ";__AVX__"; + else if (enableEnhancedInstructionSet == "AdvancedVectorExtensions2") + fs.defines += ";__AVX2__"; + else if (enableEnhancedInstructionSet == "AdvancedVectorExtensions512") + fs.defines += ";__AVX512F__"; + + const auto charSetIt = variables.find("CharacterSet"); + const std::string charSet = (charSetIt != variables.end()) ? charSetIt->second : std::string(); + + const auto useOfMfcIt = variables.find("UseOfMfc"); + fs.useMfc = useOfMfcIt != variables.end() && !useOfMfcIt->second.empty() && + caseInsensitiveStringCompare(useOfMfcIt->second, "false") != 0; + + if (charSet == "Unicode") { fs.defines += ";UNICODE=1;_UNICODE=1"; + } else if (charSet == "MultiByte") { + fs.defines += ";_MBCS=1"; } - fsSetDefines(fs, fs.defines); - fsSetIncludePaths(fs, Path::getPathFromFilename(compile.mFilename), toStringList(includePath + ';' + additionalIncludePaths), variables); - for (const auto &path : sharedItemsIncludePaths) { - fs.includePaths.emplace_back(path); - } - fileSettings.push_back(std::move(fs)); - } - } - - return true; -} - -ImportProject::SharedItemsProject ImportProject::importVcxitems(const std::string& filename, const std::vector& fileFilters, std::vector &cache) -{ - auto isInCacheCheck = [filename](const ImportProject::SharedItemsProject& e) -> bool { - return filename == e.pathToProjectFile; - }; - const auto iterator = std::find_if(cache.begin(), cache.end(), isInCacheCheck); - if (iterator != std::end(cache)) { - return *iterator; - } - SharedItemsProject result; - result.pathToProjectFile = filename; + std::string defines = fs.defines; + if (!compile.preprocessorDefinitions.empty()) + defines += (";" + compile.preprocessorDefinitions); + fsSetDefines(fs, defines); + fsSetIncludePaths(fs, projectDir, toStringList(variables["IncludePath"]), variables); + fs.systemIncludePaths = std::move(fs.includePaths); + fsSetIncludePaths(fs, projectDir, toStringList(compile.additionalIncludeDirectories), variables); + fs.forcedIncludes = toStringList(compile.forcedIncludeFiles); + for (auto &forcedInclude : fs.forcedIncludes) + forcedInclude = toAbsolute(forcedInclude, projectDir, variables); - PathMatch filtermatcher(fileFilters, Path::getCurrentPath()); - - tinyxml2::XMLDocument doc; - const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); - if (error != tinyxml2::XML_SUCCESS) { - errors.emplace_back(std::string("Visual Studio project file is not a valid XML - ") + tinyxml2::XMLDocument::ErrorIDToName(error)); - return result; - } - const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement(); - if (rootnode == nullptr) { - errors.emplace_back("Visual Studio project file has no XML root node"); - return result; - } - for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { - if (std::strcmp(node->Name(), "ItemGroup") == 0) { - for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (std::strcmp(e->Name(), "ClCompile") == 0) { - const char* include = e->Attribute("Include"); - if (include && Path::acceptFile(include)) { - std::string file(include); - findAndReplace(file, "$(MSBuildThisFileDirectory)", "./"); - - // Skip file if it doesn't match the filter - if (!fileFilters.empty() && !filtermatcher.match(file)) - continue; - - result.sourceFiles.emplace_back(file); - } else { - errors.emplace_back("Could not find shared items source file"); - return result; - } - } - } - } else if (std::strcmp(node->Name(), "ItemDefinitionGroup") == 0) { - ItemDefinitionGroup temp(node, ""); - for (const auto& includePath : toStringList(temp.additionalIncludePaths)) { - if (includePath == "%(AdditionalIncludeDirectories)") - continue; - - std::string toAdd(includePath); - findAndReplace(toAdd, "$(MSBuildThisFileDirectory)", "./"); - result.includePaths.emplace_back(toAdd); - } + fileSettings.push_back(std::move(fs)); } } - result.successful = true; - cache.emplace_back(result); - return result; + return true; } bool ImportProject::importBcb6Prj(const std::string &projectFilename) @@ -1435,7 +2169,7 @@ bool ImportProject::importBcb6Prj(const std::string &projectFilename) // Reading the BCB6 install location from registry in windows environments would also be possible, // but I didn't see any such functionality around the source. Not in favor of adding it only // for the BCB6 project loading. - std::map variables; + VariablesMap variables; const std::string defines = predefines + ";" + sysdefines + ";" + userdefines; const std::string cppDefines = cppPredefines + ";" + defines; const bool forceCppMode = (cflags.find("-P") != cflags.end()); @@ -1780,16 +2514,23 @@ void ImportProject::setRelativePaths(const std::string &filename) const std::string rel = Path::getRelativePath(includePath, basePaths); includePath = rel.empty() ? "." : rel; } + for (auto &includePath: fs.systemIncludePaths) { + const std::string rel = Path::getRelativePath(includePath, basePaths); + includePath = rel.empty() ? "." : rel; + } + for (auto &forcedInclude: fs.forcedIncludes) + forcedInclude = Path::getRelativePath(forcedInclude, basePaths); } } // only used by tests (testimportproject.cpp::testVcxprojConditions): // cppcheck-suppress unusedFunction -bool cppcheck::testing::evaluateVcxprojCondition(const std::string& condition, const std::string& configuration, +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 Conditional::evalCondition(condition, p); + VariablesMap variables; + variables["Platform"] = platform; + variables["Configuration"] = configuration; + return evalCondition(condition, variables); } diff --git a/lib/importproject.h b/lib/importproject.h index b8bbbed3fa3..f2b0a89a6c5 100644 --- a/lib/importproject.h +++ b/lib/importproject.h @@ -32,12 +32,14 @@ #include #include #include +#include #include class Settings; struct Suppressions; + namespace tinyxml2 { - class XMLDocument; + class XMLElement; } /// @addtogroup Core @@ -56,11 +58,14 @@ namespace cppcheck { } } +using VariablesMap = std::map; + /** * @brief Importing project settings. */ class CPPCHECKLIB WARN_UNUSED ImportProject { public: + enum class Type : std::uint8_t { NONE, UNKNOWN, @@ -73,14 +78,22 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { BORLAND, CPPCHECK_GUI }; + enum class ImportResult : std::uint8_t { + Ok, + NotResolvable, + NotFound, + NotValid, + Cycle + }; protected: static void fsSetDefines(FileSettings& fs, std::string defs); - static void fsSetIncludePaths(FileSettings& fs, const std::string &basepath, const std::list &in, std::map &variables); + void fsSetIncludePaths(FileSettings& fs, const std::string &basepath, const std::list &in, VariablesMap &variables); public: std::list fileSettings; std::vector errors; + std::vector debugs; ImportProject() = default; virtual ~ImportProject() = default; @@ -106,30 +119,72 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { void ignoreOtherConfigs(const std::string &cfg); Type import(const std::string &filename, Settings *settings=nullptr, Suppressions *supprs=nullptr); + + static const std::string &importResultStr(ImportResult result); + protected: bool importCompileCommands(std::istream &istr); bool importCppcheckGuiProject(std::istream &istr, Settings &settings, Suppressions &supprs); static std::string collectArgs(const std::string &cmd, std::vector &args); void setRelativePaths(const std::string &filename); - struct SharedItemsProject { - bool successful = false; - std::string pathToProjectFile; - std::vector includePaths; - std::vector sourceFiles; - }; + VariablesMap mVariables; - bool importVcxproj(const std::string &filename, std::map &variables, const std::string &additionalIncludeDirectories, const std::vector &fileFilters, std::vector &cache); - bool importVcxproj(const std::string &filename, const tinyxml2::XMLDocument &doc, std::map &variables, const std::string &additionalIncludeDirectories, const std::vector &fileFilters, std::vector &cache); private: static void parseArgs(FileSettings &fs, const std::vector &args); - bool importSln(std::istream &istr, const std::string &path, const std::vector &fileFilters); - bool importSlnx(const std::string& filename, const std::vector& fileFilters); - SharedItemsProject importVcxitems(const std::string &filename, const std::vector &fileFilters, std::vector &cache); bool importBcb6Prj(const std::string &projectFilename); + struct ProjectConfiguration { + explicit ProjectConfiguration(const tinyxml2::XMLElement *cfg); + + std::string name; + std::string configuration; + enum : std::uint8_t { Win32, x64, Unknown } platform = Unknown; + std::string platformStr; + }; + + struct ItemGroupClCompile { + explicit ItemGroupClCompile(std::string filename) : filename(std::move(filename)) {} + std::string filename; + std::string additionalIncludeDirectories; + std::string forcedIncludeFiles; + std::string preprocessorDefinitions; + std::string languageStandard; + }; + + bool importSln(std::istream &istr, const std::string &filename, const std::vector &fileFilters); + bool importSlnx(const std::string& filename, const std::vector& fileFilters); + bool importVcxproj(const std::string &filename, VariablesMap &variables, const std::vector &fileFilters); + + ImportResult importPropsOrTargets(const std::string &file, + VariablesMap &variables, + std::list &projectConfigurationList, + std::unordered_set &importStack); + ImportResult importVcxitems(const std::string &items, + VariablesMap &variables, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack); + ImportResult importProject(const tinyxml2::XMLElement *node, + const std::string &projectDir, + VariablesMap &variables, + std::list &projectConfigurationList, + std::unordered_set &importStack); + ImportResult importCompile(const tinyxml2::XMLElement *node, + const std::string &projectDir, + VariablesMap &variables, + std::list &compileList); + void checkUnexpandedExpressions(const std::string &text, const char *context); + bool simplifyPathWithVariables(std::string &s, VariablesMap &variables); + void addProperty(const tinyxml2::XMLElement *node, VariablesMap &variables); + std::string getProperty(const tinyxml2::XMLElement *node, VariablesMap &variables, const std::string &original); + std::string toAbsolute(const std::string &filename, const std::string &baseDir, VariablesMap &variables); + static std::string toAbsolute(const std::string &path); + static void setSolution(const std::string &filename, VariablesMap &variables); + + std::string mPath; std::set mAllVSConfigs; }; @@ -201,10 +256,6 @@ namespace CppcheckXml { static constexpr char ProjectNameElementName[] = "project-name"; } -namespace testing -{ - CPPCHECKLIB bool evaluateVcxprojCondition(const std::string& condition, const std::string& configuration, const std::string& platform); -} /// @} //--------------------------------------------------------------------------- #endif // importprojectH diff --git a/oss-fuzz/Makefile b/oss-fuzz/Makefile index e6966747958..7413910834f 100644 --- a/oss-fuzz/Makefile +++ b/oss-fuzz/Makefile @@ -285,7 +285,7 @@ $(libcppdir)/forwardanalyzer.o: ../lib/forwardanalyzer.cpp ../lib/analyzer.h ../ $(libcppdir)/fwdanalysis.o: ../lib/fwdanalysis.cpp ../lib/astutils.h ../lib/checkers.h ../lib/config.h ../lib/errortypes.h ../lib/fwdanalysis.h ../lib/library.h ../lib/mathlib.h ../lib/platform.h ../lib/settings.h ../lib/smallvector.h ../lib/sourcelocation.h ../lib/standards.h ../lib/symboldatabase.h ../lib/templatesimplifier.h ../lib/token.h ../lib/utils.h ../lib/vfvalue.h $(CXX) ${LIB_FUZZING_ENGINE} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/fwdanalysis.cpp -$(libcppdir)/importproject.o: ../lib/importproject.cpp ../externals/picojson/picojson.h ../externals/tinyxml2/tinyxml2.h ../lib/checkers.h ../lib/config.h ../lib/errortypes.h ../lib/filesettings.h ../lib/importproject.h ../lib/json.h ../lib/library.h ../lib/mathlib.h ../lib/path.h ../lib/pathmatch.h ../lib/platform.h ../lib/settings.h ../lib/smallvector.h ../lib/standards.h ../lib/suppressions.h ../lib/templatesimplifier.h ../lib/token.h ../lib/tokenlist.h ../lib/utils.h ../lib/vfvalue.h ../lib/xml.h +$(libcppdir)/importproject.o: ../lib/importproject.cpp ../externals/picojson/picojson.h ../externals/tinyxml2/tinyxml2.h ../lib/checkers.h ../lib/config.h ../lib/filesettings.h ../lib/importproject.h ../lib/json.h ../lib/library.h ../lib/mathlib.h ../lib/path.h ../lib/pathmatch.h ../lib/platform.h ../lib/settings.h ../lib/standards.h ../lib/suppressions.h ../lib/utils.h ../lib/xml.h $(CXX) ${LIB_FUZZING_ENGINE} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/importproject.cpp $(libcppdir)/infer.o: ../lib/infer.cpp ../lib/calculate.h ../lib/config.h ../lib/errortypes.h ../lib/infer.h ../lib/mathlib.h ../lib/smallvector.h ../lib/templatesimplifier.h ../lib/token.h ../lib/utils.h ../lib/valueptr.h ../lib/vfvalue.h diff --git a/test/cli/proj2_test.py b/test/cli/proj2_test.py index c9516d9ddbf..aa8cf6d3355 100644 --- a/test/cli/proj2_test.py +++ b/test/cli/proj2_test.py @@ -18,6 +18,11 @@ 'x = 3 / 0;\n' + ' ^\n') % os.path.join('b', 'b.c') +def __get_lines(s): + # file order is not guaranteed when multiple jobs are used (TEST_CPPCHECK_INJECT_J) so + # compare output order-independently + return sorted(s.split('\n')) + def __create_compile_commands(proj_dir): proj_dir = str(proj_dir) j = [{'directory': os.path.join(proj_dir, 'a'), 'command': 'gcc -c a.c', 'file': 'a.c'}, @@ -152,7 +157,7 @@ def test_gui_project_loads_relative_vs_solution_2(tmp_path): create_gui_project_file(os.path.join(tmp_path, 'test.cppcheck'), root_path='proj2', import_project='proj2/proj2.sln') ret, stdout, stderr = cppcheck(['--project=test.cppcheck'], cwd=tmp_path) assert ret == 0, stdout - assert stderr == __ERR_A + __ERR_B + assert __get_lines(stderr) == __get_lines(__ERR_A + __ERR_B) def test_gui_project_loads_relative_vs_solution_with_exclude(tmp_path): proj_dir = tmp_path / 'proj2' @@ -170,4 +175,4 @@ def test_gui_project_loads_absolute_vs_solution_2(tmp_path): import_project=os.path.join(proj_dir, 'proj2.sln')) ret, stdout, stderr = cppcheck(['--project=test.cppcheck'], cwd=tmp_path) assert ret == 0, stdout - assert stderr == __ERR_A + __ERR_B + assert __get_lines(stderr) == __get_lines(__ERR_A + __ERR_B) diff --git a/test/cli/props-dirs/Cpp.Build.props b/test/cli/props-dirs/Cpp.Build.props new file mode 100644 index 00000000000..c7ec9783a4b --- /dev/null +++ b/test/cli/props-dirs/Cpp.Build.props @@ -0,0 +1,12 @@ + + + + + + + Debug + x64 + + + + diff --git a/test/cli/props-dirs/Cpp.Build.targets b/test/cli/props-dirs/Cpp.Build.targets new file mode 100644 index 00000000000..341027f3c7a --- /dev/null +++ b/test/cli/props-dirs/Cpp.Build.targets @@ -0,0 +1,3 @@ + + + diff --git a/test/cli/props-dirs/Directory.Build.props b/test/cli/props-dirs/Directory.Build.props new file mode 100644 index 00000000000..0e0ec4010eb --- /dev/null +++ b/test/cli/props-dirs/Directory.Build.props @@ -0,0 +1,6 @@ + + + $(MSBuildThisFileDirectory) + + + diff --git a/test/cli/props-dirs/Directory.Build.targets b/test/cli/props-dirs/Directory.Build.targets new file mode 100644 index 00000000000..8c119d5413b --- /dev/null +++ b/test/cli/props-dirs/Directory.Build.targets @@ -0,0 +1,2 @@ + + diff --git a/test/cli/props-dirs/ProjA/ProjA.vcxproj b/test/cli/props-dirs/ProjA/ProjA.vcxproj new file mode 100644 index 00000000000..5ce4fbd069d --- /dev/null +++ b/test/cli/props-dirs/ProjA/ProjA.vcxproj @@ -0,0 +1,32 @@ + + + + + Debug + x64 + + + + {a1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1} + ProjA + + + + Application + v143 + + + + + + + + + PROJA_DEFINE;%(PreprocessorDefinitions) + + + + + + + diff --git a/test/cli/props-dirs/ProjA/a.cpp b/test/cli/props-dirs/ProjA/a.cpp new file mode 100644 index 00000000000..4eb775af468 --- /dev/null +++ b/test/cli/props-dirs/ProjA/a.cpp @@ -0,0 +1,11 @@ +#include "common.h" + +#ifndef COMMON_H_INCLUDED_MARKER +#error "common.h was not found - AdditionalIncludeDirectories from common.props did not resolve" +#endif + +int main() +{ + int x = 1; + return x / 0; +} diff --git a/test/cli/props-dirs/ProjB/ProjB.vcxproj b/test/cli/props-dirs/ProjB/ProjB.vcxproj new file mode 100644 index 00000000000..047f5d4b8fa --- /dev/null +++ b/test/cli/props-dirs/ProjB/ProjB.vcxproj @@ -0,0 +1,29 @@ + + + + + Debug + x64 + + + + {b2b2b2b2-b2b2-b2b2-b2b2-b2b2b2b2b2b2} + ProjB + + + + Application + v143 + + + + + + + + + + + diff --git a/test/cli/props-dirs/ProjB/b.cpp b/test/cli/props-dirs/ProjB/b.cpp new file mode 100644 index 00000000000..c24977c13ae --- /dev/null +++ b/test/cli/props-dirs/ProjB/b.cpp @@ -0,0 +1,11 @@ +#include "common.h" + +#ifndef COMMON_H_INCLUDED_MARKER +#error "common.h was not found - AdditionalIncludeDirectories from common.props did not resolve" +#endif + +int main() +{ + int y = 2; + return y / 0; +} diff --git a/test/cli/props-dirs/common/common.h b/test/cli/props-dirs/common/common.h new file mode 100644 index 00000000000..72674fc6c62 --- /dev/null +++ b/test/cli/props-dirs/common/common.h @@ -0,0 +1,3 @@ +#ifndef COMMON_H_INCLUDED_MARKER +#define COMMON_H_INCLUDED_MARKER +#endif diff --git a/test/cli/props-dirs/common/common.props b/test/cli/props-dirs/common/common.props new file mode 100644 index 00000000000..f969a6e897d --- /dev/null +++ b/test/cli/props-dirs/common/common.props @@ -0,0 +1,13 @@ + + + + + + COMMON_DEFINE;%(PreprocessorDefinitions) + $(MSBuildThisFileDirectory);%(AdditionalIncludeDirectories) + stdcpp17 + + + diff --git a/test/cli/props-dirs/props-dirs.slnx b/test/cli/props-dirs/props-dirs.slnx new file mode 100644 index 00000000000..0aae89ada2a --- /dev/null +++ b/test/cli/props-dirs/props-dirs.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/cli/props-dirs/shared/shared.props b/test/cli/props-dirs/shared/shared.props new file mode 100644 index 00000000000..623e1094e9f --- /dev/null +++ b/test/cli/props-dirs/shared/shared.props @@ -0,0 +1,14 @@ + + + + + + + + + SHARED_DEFINE;%(PreprocessorDefinitions) + + + diff --git a/test/cli/props_dirs_test.py b/test/cli/props_dirs_test.py new file mode 100644 index 00000000000..123cc010b5e --- /dev/null +++ b/test/cli/props_dirs_test.py @@ -0,0 +1,78 @@ + +# python -m pytest props_dirs_test.py +# +# Regression coverage for MSBuild property-sheet (.props) loading across multiple +# directories: +# - $(MSBuildThisFileDirectory) must resolve to each .props file's own directory, +# not the importing project's directory, even through a chain of nested imports +# (ProjA/ -> shared/shared.props -> common/common.props). +# - AdditionalIncludeDirectories set via that chain must actually make a header in a +# different directory (common/common.h) resolvable from the project's source file. +# - A project that imports common/common.props directly (ProjB) must pick up exactly +# what that file sets and nothing that a *different* project in the same solution +# (ProjA) added on top - no cross-project variable leakage. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + +__ERR_A = ('%s:10:14: error: Division by zero. [zerodiv]\n' + ' return x / 0;\n' + ' ^\n') % os.path.join('props-dirs', 'ProjA', 'a.cpp') +__ERR_B = ('%s:10:14: error: Division by zero. [zerodiv]\n' + ' return y / 0;\n' + ' ^\n') % os.path.join('props-dirs', 'ProjB', 'b.cpp') + + +def __get_lines(s): + # file order is not guaranteed when multiple jobs are used (TEST_CPPCHECK_INJECT_J) so + # compare output order-independently + return sorted(s.split('\n')) + + +def test_props_dirs_solution(): + args = [ + '--project=props-dirs/props-dirs.slnx', + '--no-cppcheck-build-dir' + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + # both files were actually analyzed (division by zero fires) which also proves + # "common.h" was found via AdditionalIncludeDirectories - if it hadn't resolved, the + # #error guard in each .cpp would have fired instead and there would be no zerodiv + assert __get_lines(stderr) == __get_lines(__ERR_A + __ERR_B) + + +def test_props_dirs_defines_and_standard(): + args = [ + '--project=props-dirs/props-dirs.slnx', + '--no-cppcheck-build-dir', + '--dump' + ] + ret, stdout, _ = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + dump_a = os.path.join(__script_dir, 'props-dirs', 'ProjA', 'a.cpp.dump') + dump_b = os.path.join(__script_dir, 'props-dirs', 'ProjB', 'b.cpp.dump') + assert os.path.exists(dump_a), f"Dump file not found at {dump_a}" + assert os.path.exists(dump_b), f"Dump file not found at {dump_b}" + + with open(dump_a, 'rt') as f: + dump_a_content = f.read() + with open(dump_b, 'rt') as f: + dump_b_content = f.read() + + # ProjA imports shared/shared.props (which itself imports common/common.props), and + # also sets its own PROJA_DEFINE - all three must be present, most specific first + assert 'cfg="_WIN32=1;_WIN64=1;PROJA_DEFINE=1;SHARED_DEFINE=1;COMMON_DEFINE=1;_MSC_VER=1900"' in dump_a_content + assert '' in dump_a_content + + # ProjB imports common/common.props directly - it must see COMMON_DEFINE, but neither + # PROJA_DEFINE nor SHARED_DEFINE, which only ever applied to ProjA + assert 'cfg="_WIN32=1;_WIN64=1;COMMON_DEFINE=1;_MSC_VER=1900"' in dump_b_content + assert '' in dump_b_content + assert 'PROJA_DEFINE' not in dump_b_content + assert 'SHARED_DEFINE' not in dump_b_content diff --git a/test/cli/vcxproj-unicode/main.cpp b/test/cli/vcxproj-unicode/main.cpp new file mode 100644 index 00000000000..1a0e6f02e86 --- /dev/null +++ b/test/cli/vcxproj-unicode/main.cpp @@ -0,0 +1,7 @@ +#include + +int main() { + std::cout << "Hello world!" << std::endl; + return 0; +} + diff --git a/test/cli/vcxproj-unicode/vcxproj_unicode.vcxproj b/test/cli/vcxproj-unicode/vcxproj_unicode.vcxproj new file mode 100644 index 00000000000..e85592a647e --- /dev/null +++ b/test/cli/vcxproj-unicode/vcxproj_unicode.vcxproj @@ -0,0 +1,33 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + + + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + NotSet + Static + + + + + diff --git a/test/cli/vcxproj_forced_includes/AllX64.h b/test/cli/vcxproj_forced_includes/AllX64.h new file mode 100644 index 00000000000..0c3063b59f4 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/AllX64.h @@ -0,0 +1,6 @@ +class all +{ + all() { + int x = 3 / 0; (void)x; // ERROR + } +}; diff --git a/test/cli/vcxproj_forced_includes/DebugX64.cpp b/test/cli/vcxproj_forced_includes/DebugX64.cpp new file mode 100644 index 00000000000..cfb1fce687a --- /dev/null +++ b/test/cli/vcxproj_forced_includes/DebugX64.cpp @@ -0,0 +1,8 @@ +#include + +int foo() +{ + std::cout << "DebugX64\n"; + int x = 3 / 0; (void)x; // ERROR + return 0; +} diff --git a/test/cli/vcxproj_forced_includes/DebugX64.h b/test/cli/vcxproj_forced_includes/DebugX64.h new file mode 100644 index 00000000000..ab3bfb495da --- /dev/null +++ b/test/cli/vcxproj_forced_includes/DebugX64.h @@ -0,0 +1,6 @@ +class debug +{ + debug() { + int x = 3 / 0; (void)x; // ERROR + } +}; \ No newline at end of file diff --git a/test/cli/vcxproj_forced_includes/GlobalDebugX64.h b/test/cli/vcxproj_forced_includes/GlobalDebugX64.h new file mode 100644 index 00000000000..48038886307 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/GlobalDebugX64.h @@ -0,0 +1,6 @@ +class global +{ + global() { + int x = 3 / 0; (void)x; // ERROR + } +}; diff --git a/test/cli/vcxproj_forced_includes/GlobalReleaseX64.h b/test/cli/vcxproj_forced_includes/GlobalReleaseX64.h new file mode 100644 index 00000000000..48038886307 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/GlobalReleaseX64.h @@ -0,0 +1,6 @@ +class global +{ + global() { + int x = 3 / 0; (void)x; // ERROR + } +}; diff --git a/test/cli/vcxproj_forced_includes/PropsDebugX64.h b/test/cli/vcxproj_forced_includes/PropsDebugX64.h new file mode 100644 index 00000000000..49643c7ea86 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/PropsDebugX64.h @@ -0,0 +1,6 @@ +class props +{ + props() { + int x = 3 / 0; (void)x; // ERROR + } +}; diff --git a/test/cli/vcxproj_forced_includes/PropsReleaseX64.h b/test/cli/vcxproj_forced_includes/PropsReleaseX64.h new file mode 100644 index 00000000000..49643c7ea86 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/PropsReleaseX64.h @@ -0,0 +1,6 @@ +class props +{ + props() { + int x = 3 / 0; (void)x; // ERROR + } +}; diff --git a/test/cli/vcxproj_forced_includes/ReleaseX64.cpp b/test/cli/vcxproj_forced_includes/ReleaseX64.cpp new file mode 100644 index 00000000000..8fa6e6d0f82 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/ReleaseX64.cpp @@ -0,0 +1,8 @@ +#include + +int foo() +{ + std::cout << "ReleaseX64\n"; + int x = 3 / 0; (void)x; // ERROR + return 0; +} diff --git a/test/cli/vcxproj_forced_includes/ReleaseX64.h b/test/cli/vcxproj_forced_includes/ReleaseX64.h new file mode 100644 index 00000000000..49f9766f927 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/ReleaseX64.h @@ -0,0 +1,6 @@ +class release +{ + release() { + int x = 3 / 0; (void)x; // ERROR + } +}; \ No newline at end of file diff --git a/test/cli/vcxproj_forced_includes/foo.h b/test/cli/vcxproj_forced_includes/foo.h new file mode 100644 index 00000000000..5d5f8f0c9e7 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/foo.h @@ -0,0 +1 @@ +int foo(); diff --git a/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.props b/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.props new file mode 100644 index 00000000000..22e858590c1 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.props @@ -0,0 +1,8 @@ + + + + PropsDebugX64.h;%(ForcedIncludeFiles) + PropsReleaseX64.h;%(ForcedIncludeFiles) + + + diff --git a/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.slnx b/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.slnx new file mode 100644 index 00000000000..f586cfa3a29 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.slnx @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.vcxproj b/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.vcxproj new file mode 100644 index 00000000000..bd1676af947 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.vcxproj @@ -0,0 +1,103 @@ + + + + + Debug + x64 + + + Release + x64 + + + + 18.0 + Win32Proj + {c9d1dca1-d8ff-4c05-9159-f00816645319} + exclude + 10.0 + + + + StaticLibrary + true + v145 + Unicode + + + Application + false + v145 + true + Unicode + + + + + + + + + + + + + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + $(MSBuildThisFileDirectory)GlobalDebugX64.h;%(ForcedIncludeFiles) + + + Console + true + + + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + $(MSBuildThisFileDirectory)GlobalReleaseX64.h;%(ForcedIncludeFiles) + + + Console + true + + + true + + + + + $(MSBuildThisFileDirectory)AllX64.h;%(ForcedIncludeFiles) + $(MSBuildThisFileDirectory)DebugX64.h;%(ForcedIncludeFiles) + $(MSBuildThisFileDirectory)ReleaseX64.h;%(ForcedIncludeFiles) + true + + + $(MSBuildThisFileDirectory)AllX64.h;%(ForcedIncludeFiles) + $(MSBuildThisFileDirectory)DebugX64.h;%(ForcedIncludeFiles) + $(MSBuildThisFileDirectory)ReleaseX64.h;%(ForcedIncludeFiles) + true + + + + + + + + + \ No newline at end of file diff --git a/test/cli/vcxproj_forced_includes_test.py b/test/cli/vcxproj_forced_includes_test.py new file mode 100644 index 00000000000..e86b254f7da --- /dev/null +++ b/test/cli/vcxproj_forced_includes_test.py @@ -0,0 +1,59 @@ + +# python -m pytest vcxproj_forced_includes_test.py + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) +__proj_dir = os.path.join(__script_dir, 'vcxproj_forced_includes') + +def get_lines(s): + return sorted(s.split('\n')) + +def test_vcxproj_forced_includes_debug(): + args = [ + '--template=cppcheck1', + '--project=vcxproj_forced_includes/vcxproj_forced_includes.slnx', + '--project-configuration=Debug|x64', + '--no-cppcheck-build-dir' + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + filename1 = os.path.join('vcxproj_forced_includes', 'DebugX64.cpp') + filename2 = os.path.join('vcxproj_forced_includes', 'DebugX64.h') + filename3 = os.path.join('vcxproj_forced_includes', 'AllX64.h') + filename4 = os.path.join('vcxproj_forced_includes', 'GlobalDebugX64.h') + filename5 = os.path.join('vcxproj_forced_includes', 'PropsDebugX64.h') + assert ret == 0, stdout + expected = ( + '[%s:6]: (error) Division by zero.\n' + '[%s:4]: (error) Division by zero.\n' + '[%s:4]: (error) Division by zero.\n' + '[%s:4]: (error) Division by zero.\n' + '[%s:4]: (error) Division by zero.\n' % (filename1, filename2, filename3, filename4, filename5) + ) + assert get_lines(stderr) == get_lines(expected) + + +def test_vcxproj_forced_includes_release(): + args = [ + '--template=cppcheck1', + '--project=vcxproj_forced_includes/vcxproj_forced_includes.slnx', + '--project-configuration=Release|x64', + '--no-cppcheck-build-dir' + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + filename1 = os.path.join('vcxproj_forced_includes', 'ReleaseX64.cpp') + filename2 = os.path.join('vcxproj_forced_includes', 'ReleaseX64.h') + filename3 = os.path.join('vcxproj_forced_includes', 'AllX64.h') + filename4 = os.path.join('vcxproj_forced_includes', 'GlobalReleaseX64.h') + filename5 = os.path.join('vcxproj_forced_includes', 'PropsReleaseX64.h') + assert ret == 0, stdout + expected = ( + '[%s:6]: (error) Division by zero.\n' + '[%s:4]: (error) Division by zero.\n' + '[%s:4]: (error) Division by zero.\n' + '[%s:4]: (error) Division by zero.\n' + '[%s:4]: (error) Division by zero.\n' % (filename1, filename2, filename3, filename4, filename5) + ) + assert get_lines(stderr) == get_lines(expected) diff --git a/test/cli/vcxproj_unicode_test.py b/test/cli/vcxproj_unicode_test.py new file mode 100644 index 00000000000..7e1dd22954e --- /dev/null +++ b/test/cli/vcxproj_unicode_test.py @@ -0,0 +1,42 @@ + +# python -m pytest vcxproj_unicode_test.py + +from testutils import cppcheck + +import os +import shutil + +__script_dir = os.path.dirname(os.path.abspath(__file__)) +__proj_dir = os.path.join(__script_dir, 'vcxproj-unicode') + +def _get_dump_for_configuration(tmp_path, configuration): + proj_dir = tmp_path / 'vcxproj-unicode' + shutil.copytree(__proj_dir, proj_dir) + + args = [ + '--template=cppcheck1', + '--project=vcxproj-unicode/vcxproj_unicode.vcxproj', + f'--project-configuration={configuration}', + '--no-cppcheck-build-dir', + '--dump' + ] + ret, stdout, stderr = cppcheck(args, cwd=str(tmp_path)) + assert ret == 0, stdout + assert stderr == '', stderr + + dump_path = proj_dir / 'main.cpp.dump' + assert dump_path.exists(), f"Dump file not found at {dump_path}" + + with open(dump_path, 'rt') as f: + return f.read() + +def test_vcxproj_unicode_debug(tmp_path): + dump_content = _get_dump_for_configuration(tmp_path, 'Debug|Win32') + + # the resolved defines are recorded in the attribute + assert 'cfg="_WIN32=1;UNICODE=1;_UNICODE=1;_MSC_VER=1900"' in dump_content + +def test_vcxproj_unicode_release(tmp_path): + dump_content = _get_dump_for_configuration(tmp_path, 'Release|Win32') + + assert 'cfg="_WIN32=1;_MSC_VER=1900;__AFXWIN_H__=1"' in dump_content diff --git a/test/testimportproject.cpp b/test/testimportproject.cpp index 873272030f6..71dabd41029 100644 --- a/test/testimportproject.cpp +++ b/test/testimportproject.cpp @@ -23,10 +23,8 @@ #include "settings.h" #include "standards.h" #include "suppressions.h" -#include "xml.h" #include -#include #include #include #include @@ -37,8 +35,6 @@ class TestImporter final : public ImportProject { public: using ImportProject::importCompileCommands; using ImportProject::importCppcheckGuiProject; - using ImportProject::importVcxproj; - using ImportProject::SharedItemsProject; using ImportProject::collectArgs; using ImportProject::fsSetDefines; using ImportProject::fsSetIncludePaths; @@ -82,7 +78,6 @@ class TestImportProject : public TestFixture { TEST_CASE(importCppcheckGuiProjectDuplicateSuppressions); TEST_CASE(importCppcheckGuiProjectPremiumMisra); TEST_CASE(ignorePaths); - TEST_CASE(testVcxprojUnicode); TEST_CASE(testCollectArgs1); TEST_CASE(testCollectArgs2); TEST_CASE(testCollectArgs3); @@ -112,8 +107,9 @@ class TestImportProject : public TestFixture { void setIncludePaths1() const { FileSettings fs{"test.cpp", Standards::Language::CPP, 0}; std::list in(1, "../include"); - std::map variables; - TestImporter::fsSetIncludePaths(fs, "abc/def/", in, variables); + VariablesMap variables; + TestImporter importer; + importer.fsSetIncludePaths(fs, "abc/def/", in, variables); ASSERT_EQUALS(1U, fs.includePaths.size()); ASSERT_EQUALS("abc/include/", fs.includePaths.front()); } @@ -121,9 +117,10 @@ class TestImportProject : public TestFixture { void setIncludePaths2() const { FileSettings fs{"test.cpp", Standards::Language::CPP, 0}; std::list in(1, "$(SolutionDir)other"); - std::map variables; + VariablesMap variables; variables["SolutionDir"] = "c:/abc/"; - TestImporter::fsSetIncludePaths(fs, "/home/fred", in, variables); + TestImporter importer; + importer.fsSetIncludePaths(fs, "/home/fred", in, variables); ASSERT_EQUALS(1U, fs.includePaths.size()); ASSERT_EQUALS("c:/abc/other/", fs.includePaths.front()); } @@ -131,9 +128,10 @@ class TestImportProject : public TestFixture { void setIncludePaths3() const { // macro names are case insensitive FileSettings fs{"test.cpp", Standards::Language::CPP, 0}; std::list in(1, "$(SOLUTIONDIR)other"); - std::map variables; + VariablesMap variables; variables["SolutionDir"] = "c:/abc/"; - TestImporter::fsSetIncludePaths(fs, "/home/fred", in, variables); + TestImporter importer; + importer.fsSetIncludePaths(fs, "/home/fred", in, variables); ASSERT_EQUALS(1U, fs.includePaths.size()); ASSERT_EQUALS("c:/abc/other/", fs.includePaths.front()); } @@ -595,59 +593,6 @@ class TestImportProject : public TestFixture { ASSERT_EQUALS(0, project.fileSettings.size()); } - void testVcxprojUnicode() const - { - const char vcxproj[] = R"-( - - - - - Debug - Win32 - - - Release - Win32 - - - - - Unicode - - - Application - true - v143 - Unicode - - - Application - false - v143 - NotSet - Static - - - - - -)-"; - tinyxml2::XMLDocument doc; - ASSERT_EQUALS(tinyxml2::XML_SUCCESS, doc.Parse(vcxproj, sizeof(vcxproj))); - TestImporter project; - std::map variables; - std::vector cache; - ASSERT_EQUALS(project.importVcxproj("test.vcxproj", doc, variables, {}, {}, cache), true); - ASSERT_EQUALS(project.fileSettings.size(), 2); - ASSERT(project.fileSettings.front().defines.find(";UNICODE=1;") != std::string::npos); - ASSERT(project.fileSettings.front().defines.find(";_UNICODE=1") != std::string::npos); - ASSERT(project.fileSettings.front().defines.find(";_UNICODE=1;") == std::string::npos); // No duplicates - ASSERT_EQUALS(project.fileSettings.front().useMfc, false); - ASSERT(project.fileSettings.back().defines.find(";UNICODE=1;") == std::string::npos); - ASSERT(project.fileSettings.back().defines.find(";_UNICODE=1") == std::string::npos); - ASSERT_EQUALS(project.fileSettings.back().useMfc, true); - } - void testCollectArgs1() const { std::vector args; @@ -753,11 +698,33 @@ class TestImportProject : public TestFixture { 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.ToUpper().StartsWith('DEBUG'))", "Debug", "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.StartsWith('Release'))", "Debug-AddressSanitizer", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition(" $(Platform.Contains('32'))", "Debug", "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")); + // Relational operators - integer + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'14' >= '14'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'15' > '14'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'13' > '14'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'13' < '14'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'15' < '14'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'13' <= '14'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'14' <= '14'", "", "")); + // Relational operators - version + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'14.0' >= '14.0'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'14.1' >= '14.0'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'13.0' >= '14.0'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.10.0.0' > '1.9.0.0'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'v14.0' >= '14.0'", "", "")); + // Unknown variable + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'$(DoesNotExist)' == ''", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'$(PATH)' != ''", "", "")); + // Relational operators - error case + ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("'14.0' >= ''", "", ""), std::runtime_error, "Cannot compare '14.0' and ''"); ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("And", "", ""), std::runtime_error, "Invalid condition: 'And'"); ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("Or", "", ""), std::runtime_error, "Invalid condition: 'Or'"); ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("!", "", ""), std::runtime_error, "Invalid condition: '!'"); @@ -766,9 +733,84 @@ class TestImportProject : public TestFixture { ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("'' == '')", "", ""), std::runtime_error, "unmatched ')' in condition '' == '')"); ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("''", "", ""), std::runtime_error, "Invalid condition: ''''"); ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("'' == '", "", ""), std::runtime_error, "Can not tokenize condition"); - ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Lower())", "", ""), std::runtime_error, "Missing operator"); + // ToUpper / ToLower + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.ToUpper()) == 'DEBUG'", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.ToLower()) == 'debug'", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.ToUpper()) == 'debug'", "Debug", "Win32")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("$(Configuration.ToUpper()) == 'RELEASE'", "Debug", "Win32")); // invalid expression in => no error. We are ok with that as long as we don't crash ASSERT(!cppcheck::testing::evaluateVcxprojCondition("' ' && ' '", "", "")); + // case insensitive + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'Debug' == 'DEBUG'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'Debug' != 'DEBUG'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(configuration) == 'Debug'", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(configuration) == 'Debug'", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(CONFIGURATION) == 'Debug'", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration) == 'Debug'", "Debug", "Win32")); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition("true", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("TRUE", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("false", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("FALSE", "", "")); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition("true And true", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("true Or false", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("true And false", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("false Or false", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("!false", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("!true", "", "")); + + // HasTrailingSlash + ASSERT(cppcheck::testing::evaluateVcxprojCondition("HasTrailingSlash('foo/')", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("HasTrailingSlash('foo\\')", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("HasTrailingSlash('foo')", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("HasTrailingSlash('')", "", "")); + + // string manipulation + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Trim()) == 'Debug'", " Debug ", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.TrimStart()) == 'Debug '", " Debug ", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.TrimEnd()) == ' Debug'", " Debug ", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Substring(0, 5)) == 'Debug'", "Debug-Test", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Substring(6)) == 'Test'", "Debug-Test", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Replace('-', '_')) == 'Debug_Test'", "Debug-Test", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition( "$(Configuration.Trim().ToUpper()) == 'DEBUG'", " Debug ", "Win32")); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition("true", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("false", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("true And false", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("!false", "", "")); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition( "$(Configuration.Substring(5)) == ''", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition( "$(Configuration.Substring(5, 0)) == ''", "Debug", "Win32")); + ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Substring(-1)) == ''", "Debug", "Win32"), std::runtime_error, "Substring start index out of range"); + ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition( "$(Configuration.Substring(4, 2)) == ''", "Debug", "Win32"), std::runtime_error, "Substring length out of range"); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Trim()) == 'Debug'", " \tDebug\r\n", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.TrimStart()) == 'Debug '", " Debug ", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.TrimEnd()) == ' Debug'", " Debug ", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Trim('-')) == 'Debug'", "--Debug--", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.TrimStart('-')) == 'Debug--'", "--Debug--", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.TrimEnd('-')) == '--Debug'", "--Debug--", "Win32")); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Replace('-', '_')) == 'Debug_Test'","Debug-Test", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Replace('Debug', 'Release')) == 'Release-Test'", "Debug-Test", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Replace('x', 'y')) == 'Debug-Test'", "Debug-Test", "Win32")); + ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Replace('', 'x')) == 'Debug'", "Debug", "Win32"), std::runtime_error, "Replace search string cannot be empty"); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Replace('Debug', 'Release')) == 'Release-Test'", "Debug-Test", "Win32")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Replace('debug', 'Release')) == 'Release-Test'", "Debug-Test", "Win32")); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration) == DEBUG", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(configuration) == 'Debug'", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'0x10' > '0x0F'", "", "")); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'0x0F' < '0x10'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'0x10' < '0x0F'", "", "")); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'0x10' > '0x0F'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'0x0F' < '0x10'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'0x10' < '0x0F'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'010' > '9'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'0x10' == '16'", "", "")); } // TODO: test fsParseCommand()