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..685fab19150 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -23,18 +23,20 @@ #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 #include @@ -44,6 +46,7 @@ #include "json.h" + std::string ImportProject::collectArgs(const std::string &cmd, std::vector &args) { args.clear(); @@ -246,71 +249,671 @@ void ImportProject::fsSetDefines(FileSettings& fs, std::string defs) fs.defines.swap(defs); } -static bool simplifyPathWithVariables(std::string &s, std::map &variables) +// Find the ')' that matches the '(' at position parenPos, handling nested '$(' pairs. +static std::string::size_type findMatchingParen(const std::string &s, std::string::size_type parenPos) { - 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 - break; - } - variables[var] = std::string(envValue); - it1 = variables.find(var); + int depth = 0; + for (std::string::size_type i = parenPos; i < s.size(); ++i) { + if (s.compare(i, 2, "$(") == 0) { + ++depth; + ++i; // skip the '(' on next iteration increment + } else if (s[i] == ')') { + if (depth == 0) + return i; + --depth; } - s.replace(start, end - start + 1, it1->second); } - if (s.find("$(") != std::string::npos) - return false; - s = Path::simplifyPath(std::move(s)); - return true; + return std::string::npos; } -void ImportProject::fsSetIncludePaths(FileSettings& fs, const std::string &basepath, const std::list &in, std::map &variables) +// Apply an MSBuild property string method (ToLower, Replace, etc.). +// Used by both the condition evaluator and the property value expander. +static std::string applyPropertyMethod(std::string value, + const std::string &method, + const std::vector &args) { - 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 (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; + } + + 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); + } + + 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); } + 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 (endsWith(s,'/')) // this is a temporary hack, simplifyPath can crash if path ends with '/' - s.pop_back(); + 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); + } + 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 (s.find("$(") == std::string::npos) { - s = Path::simplifyPath(basepath + s); - } else { - if (!simplifyPathWithVariables(s, variables)) + 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 + "'"); +} + +// Evaluate a $([ClassName]::Method(args)) static property function. +// Returns an empty string for unknown or unimplementable functions rather +// than throwing, so import can continue gracefully. +static std::string applyMSBuildStaticFunction(const std::string &className, + const std::string &member, + const std::vector &args) +{ + const auto toInt = [](const std::string &s, long &out) -> bool { + if (s.empty()) return false; + char *end = nullptr; + out = std::strtol(s.c_str(), &end, 10); + return end != s.c_str() && *end == '\0'; + }; + + if (caseInsensitiveStringCompare(className, "MSBuild") == 0) { + + // $([MSBuild]::IsOSPlatform('Windows'|'Linux'|'OSX')) + if (caseInsensitiveStringCompare(member, "IsOSPlatform") == 0 && args.size() == 1) { +#if defined(_WIN32) + const bool onWindows = true, onLinux = false, onOSX = false; +#elif defined(__APPLE__) + const bool onWindows = false, onLinux = false, onOSX = true; +#else + const bool onWindows = false, onLinux = true, onOSX = false; +#endif + if (caseInsensitiveStringCompare(args[0], "Windows") == 0) + return onWindows ? "True" : "False"; + if (caseInsensitiveStringCompare(args[0], "Linux") == 0) + return onLinux ? "True" : "False"; + if (caseInsensitiveStringCompare(args[0], "OSX") == 0 || + caseInsensitiveStringCompare(args[0], "MacOS") == 0) + return onOSX ? "True" : "False"; + return "False"; + } + + // Arithmetic: Add, Subtract, Multiply, Divide, Modulo + if (args.size() == 2) { + long a = 0, b = 0; + if (toInt(args[0], a) && toInt(args[1], b)) { + if (caseInsensitiveStringCompare(member, "Add") == 0) + return std::to_string(a + b); + if (caseInsensitiveStringCompare(member, "Subtract") == 0) + return std::to_string(a - b); + if (caseInsensitiveStringCompare(member, "Multiply") == 0) + return std::to_string(a * b); + if (caseInsensitiveStringCompare(member, "Divide") == 0 && b != 0) + return std::to_string(a / b); + if (caseInsensitiveStringCompare(member, "Modulo") == 0 && b != 0) + return std::to_string(a % b); + } + // $([MSBuild]::ValueOrDefault(value, default)) + if (caseInsensitiveStringCompare(member, "ValueOrDefault") == 0) + return args[0].empty() ? args[1] : args[0]; + // $([MSBuild]::MakeRelative(base, path)) — approximate: return path unchanged + if (caseInsensitiveStringCompare(member, "MakeRelative") == 0) + return args[1]; + } + + // $([MSBuild]::NormalizePath(seg1[, seg2, ...])) — join segments (Path.Combine + // semantics: an absolute segment resets the accumulated path), normalize \ to /, + // and resolve . and .. components. The result is absolute only when the first + // evaluated segment is itself absolute; relative inputs stay relative. + if (caseInsensitiveStringCompare(member, "NormalizePath") == 0 && !args.empty()) { + // Join: an absolute segment resets the accumulated path (Path.Combine semantics). + std::string result = args[0]; + for (std::size_t i = 1; i < args.size(); ++i) { + const std::string &seg = args[i]; + const bool segAbsolute = !seg.empty() && + (seg[0] == '/' || seg[0] == '\\' || + (seg.size() >= 2 && std::isalpha(static_cast(seg[0])) && seg[1] == ':')); + if (segAbsolute) { + result = seg; + } else { + if (!result.empty() && result.back() != '/' && result.back() != '\\') + result += '/'; + result += seg; + } + } + // Unify separators. + // cppcheck-suppress useStlAlgorithm + for (char &c : result) if (c == '\\') c = '/'; + // Extract drive-letter or leading-slash prefix. + std::string prefix; + std::size_t pos = 0; + if (result.size() >= 2 && std::isalpha(static_cast(result[0])) && result[1] == ':') { + prefix = result.substr(0, 2) + '/'; + pos = (result.size() > 2 && result[2] == '/') ? 3 : 2; + } else if (!result.empty() && result[0] == '/') { + prefix = "/"; + pos = 1; + } + // Resolve . and .. components. + std::vector parts; + while (pos < result.size()) { + const std::size_t slash = result.find('/', pos); + const std::string seg = result.substr(pos, slash == std::string::npos ? std::string::npos : slash - pos); + pos = (slash == std::string::npos) ? result.size() : slash + 1; + if (seg.empty() || seg == ".") + continue; + if (seg == "..") { + if (!parts.empty()) parts.pop_back(); + } else { + parts.push_back(seg); + } + } + std::string normalized = prefix; + for (std::size_t i = 0; i < parts.size(); ++i) { + if (i > 0) normalized += '/'; + normalized += parts[i]; + } + return normalized; + } + + // $([MSBuild]::NormalizeDirectory(seg1[, seg2, ...])) — same as NormalizePath + // but always returns a path with a trailing slash. + if (caseInsensitiveStringCompare(member, "NormalizeDirectory") == 0 && !args.empty()) { + // Reuse NormalizePath logic via recursive call with renamed member. + const std::string normalized = applyMSBuildStaticFunction(className, "NormalizePath", args); + if (!normalized.empty() && normalized.back() != '/') + return normalized + '/'; + return normalized; + } + + if (args.size() == 1) { + // $([MSBuild]::EnsureTrailingSlash(path)) + if (caseInsensitiveStringCompare(member, "EnsureTrailingSlash") == 0) { + std::string s = args[0]; + if (!s.empty() && s.back() != '/' && s.back() != '\\') + s += '/'; + return s; + } + // $([MSBuild]::GetTargetPlatformVersion(version)) — pass through + if (caseInsensitiveStringCompare(member, "GetTargetPlatformVersion") == 0) + return args[0]; + // filesystem searches — not feasible during import + if (caseInsensitiveStringCompare(member, "GetDirectoryNameOfFileAbove") == 0 || + caseInsensitiveStringCompare(member, "GetPathOfFileAbove") == 0) + return ""; + } + + if (args.empty()) { + if (caseInsensitiveStringCompare(member, "GetCurrentToolsVersion") == 0) + return "Current"; + } + } + + if (caseInsensitiveStringCompare(className, "System.Environment") == 0) { + // $([System.Environment]::GetEnvironmentVariable('NAME')) + if (caseInsensitiveStringCompare(member, "GetEnvironmentVariable") == 0 && args.size() == 1) { + const char *env = std::getenv(args[0].c_str()); + return env ? env : ""; + } + // $([System.Environment]::GetFolderPath(SpecialFolder.X)) + if (caseInsensitiveStringCompare(member, "GetFolderPath") == 0 && args.size() == 1) { + const char *pf = std::getenv("ProgramFiles"); + if ((caseInsensitiveStringCompare(args[0], "ProgramFiles") == 0 || + caseInsensitiveStringCompare(args[0], "ProgramFilesX86") == 0) && pf) + return pf; + return ""; + } + } + + if (caseInsensitiveStringCompare(className, "System.IO.Path") == 0) { + if (args.size() == 1) { + if (caseInsensitiveStringCompare(member, "GetFileName") == 0) { + const auto slash = args[0].find_last_of("/\\"); + return slash != std::string::npos ? args[0].substr(slash + 1) : args[0]; + } + if (caseInsensitiveStringCompare(member, "GetFileNameWithoutExtension") == 0) { + const auto slash = args[0].find_last_of("/\\"); + std::string name = slash != std::string::npos ? args[0].substr(slash + 1) : args[0]; + const auto dot = name.rfind('.'); + return dot != std::string::npos ? name.substr(0, dot) : name; + } + if (caseInsensitiveStringCompare(member, "GetDirectoryName") == 0) { + const auto slash = args[0].find_last_of("/\\"); + return slash != std::string::npos ? args[0].substr(0, slash) : ""; + } + if (caseInsensitiveStringCompare(member, "GetExtension") == 0) { + const auto dot = args[0].rfind('.'); + return dot != std::string::npos ? args[0].substr(dot) : ""; + } + if (caseInsensitiveStringCompare(member, "IsPathRooted") == 0) { + const std::string &p = args[0]; + const bool rooted = !p.empty() && + (p[0] == '/' || p[0] == '\\' || + (p.size() >= 2 && + std::isalpha(static_cast(p[0])) && + p[1] == ':')); + return rooted ? "True" : "False"; + } + } + if (args.size() == 2 && caseInsensitiveStringCompare(member, "Combine") == 0) { + if (Path::isAbsolute(args[1])) + return args[1]; + const std::string sep = + (!args[0].empty() && args[0].back() != '/' && args[0].back() != '\\') ? "/" : ""; + return args[0] + sep + args[1]; + } + } + + if (caseInsensitiveStringCompare(className, "System.String") == 0) { + if (caseInsensitiveStringCompare(member, "IsNullOrEmpty") == 0 && args.size() == 1) + return args[0].empty() ? "True" : "False"; + if (caseInsensitiveStringCompare(member, "IsNullOrWhiteSpace") == 0 && args.size() == 1) { + for (const char c : args[0]) + // cppcheck-suppress useStlAlgorithm + if (!std::isspace(static_cast(c))) return "False"; + return "True"; + } + if (caseInsensitiveStringCompare(member, "Concat") == 0) { + std::string result; + for (const std::string &a : args) result += a; + return result; + } + if (caseInsensitiveStringCompare(member, "Join") == 0 && args.size() >= 2) { + std::string result; + for (std::size_t i = 1; i < args.size(); ++i) { + if (i > 1) result += args[0]; + result += args[i]; + } + return result; + } + // Format — very rough: replace {0},{1},... with positional args + if (caseInsensitiveStringCompare(member, "Format") == 0 && !args.empty()) { + std::string result = args[0]; + for (std::size_t i = 1; i < args.size(); ++i) { + const std::string placeholder = "{" + std::to_string(i - 1) + "}"; + std::size_t pos = 0; + while ((pos = result.find(placeholder, pos)) != std::string::npos) { + result.replace(pos, placeholder.size(), args[i]); + pos += args[i].size(); + } + } + return result; + } + } + + if (caseInsensitiveStringCompare(className, "System.Math") == 0) { + const auto toDouble = [](const std::string &s, double &out) -> bool { + if (s.empty()) return false; + char *end = nullptr; + out = std::strtod(s.c_str(), &end); + return end != s.c_str() && *end == '\0'; + }; + // Format a double as an integer string when the value is whole, + // otherwise use std::to_string (which gives 6 decimal places). + const auto fmtDouble = [](double d) -> std::string { + const auto i = static_cast(d); + if (!(static_cast(i) < d) && !(static_cast(i) > d)) + return std::to_string(i); + return std::to_string(d); + }; + if (args.size() == 1) { + double x = 0; + if (toDouble(args[0], x)) { + if (caseInsensitiveStringCompare(member, "Abs") == 0) + return fmtDouble(x < 0 ? -x : x); + if (caseInsensitiveStringCompare(member, "Floor") == 0) + return std::to_string(static_cast(x >= 0 ? x : x - 1)); + if (caseInsensitiveStringCompare(member, "Ceiling") == 0) + return std::to_string(static_cast(x <= 0 ? x : x + 1)); + if (caseInsensitiveStringCompare(member, "Round") == 0) + return std::to_string(static_cast(x >= 0 ? x + 0.5 : x - 0.5)); + if (caseInsensitiveStringCompare(member, "Sqrt") == 0 && x >= 0) + return fmtDouble(std::sqrt(x)); + if (caseInsensitiveStringCompare(member, "Log") == 0 && x > 0) + return fmtDouble(std::log(x)); + if (caseInsensitiveStringCompare(member, "Log10") == 0 && x > 0) + return fmtDouble(std::log10(x)); + } + } + if (args.size() == 2) { + double a = 0, b = 0; + if (toDouble(args[0], a) && toDouble(args[1], b)) { + if (caseInsensitiveStringCompare(member, "Max") == 0) + return fmtDouble(a > b ? a : b); + if (caseInsensitiveStringCompare(member, "Min") == 0) + return fmtDouble(a < b ? a : b); + if (caseInsensitiveStringCompare(member, "Pow") == 0) + return fmtDouble(std::pow(a, b)); + } + } + } + + // $([MSBuild]::Escape / Unescape) — encode/decode MSBuild special chars as %XX + if (caseInsensitiveStringCompare(className, "MSBuild") == 0 && args.size() == 1) { + if (caseInsensitiveStringCompare(member, "Escape") == 0) { + static const char special[] = "%$@';?*!"; + std::string result; + for (const unsigned char c : args[0]) { + if (std::strchr(special, static_cast(c))) { + const char hex[] = "0123456789ABCDEF"; + result += '%'; + result += hex[(c >> 4) & 0xF]; + result += hex[c & 0xF]; + } else { + result += static_cast(c); + } + } + return result; + } + if (caseInsensitiveStringCompare(member, "Unescape") == 0) { + std::string result; + const std::string &s = args[0]; + for (std::size_t i = 0; i < s.size(); ++i) { + if (s[i] == '%' && i + 2 < s.size() && + std::isxdigit(static_cast(s[i + 1])) && + std::isxdigit(static_cast(s[i + 2]))) { + const auto nibble = [](char c) -> unsigned char { + if (c >= '0' && c <= '9') return static_cast(c - '0'); + if (c >= 'a' && c <= 'f') return static_cast(c - 'a' + 10); + return static_cast(c - 'A' + 10); + }; + result += static_cast((nibble(s[i + 1]) << 4) | nibble(s[i + 2])); + i += 2; + } else { + result += s[i]; + } + } + return result; + } + // Bitwise operations + { + long a = 0; + if (toInt(args[0], a)) { + if (caseInsensitiveStringCompare(member, "BitwiseNot") == 0) + return std::to_string(~a); + } + } + } + + if (caseInsensitiveStringCompare(className, "MSBuild") == 0 && args.size() == 2) { + long a = 0, b = 0; + if (toInt(args[0], a) && toInt(args[1], b)) { + if (caseInsensitiveStringCompare(member, "BitwiseAnd") == 0) + return std::to_string(a & b); + if (caseInsensitiveStringCompare(member, "BitwiseOr") == 0) + return std::to_string(a | b); + if (caseInsensitiveStringCompare(member, "BitwiseXor") == 0) + return std::to_string(a ^ b); + } + } + + // $([MSBuild]::GetRegistryValue / GetRegistryValueFromView) + // Returns empty on non-Windows; on Windows would need registry access. + if (caseInsensitiveStringCompare(className, "MSBuild") == 0 && + (caseInsensitiveStringCompare(member, "GetRegistryValue") == 0 || + caseInsensitiveStringCompare(member, "GetRegistryValueFromView") == 0)) + return ""; + + // $([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform(...)) + // Arg is itself a static property like $([...OSPlatform]::Windows) which + // expands to the platform name string via the same mechanism. + if (caseInsensitiveStringCompare(className, "System.Runtime.InteropServices.RuntimeInformation") == 0 && + caseInsensitiveStringCompare(member, "IsOSPlatform") == 0 && args.size() == 1) + return applyMSBuildStaticFunction("MSBuild", "IsOSPlatform", args); + + // $([System.Runtime.InteropServices.OSPlatform]::Windows|Linux|OSX) — static property + if (caseInsensitiveStringCompare(className, "System.Runtime.InteropServices.OSPlatform") == 0) + return member; // return the platform name ("Windows", "Linux", "OSX") as a string + + // Unknown class or method — return empty so import continues + return ""; +} + +// Expands $(Name) and $(Name.Method(args)) references in property value strings. +// Unknown variables are left unexpanded. Use expandPropertyValue() to invoke. +struct PropertyValueExpander { + const PropertiesMap &mVars; + std::string mStr; + std::size_t mPos{0}; + bool mChanged{false}; + bool mReplaceUnknown{false}; // if true, unknown variables expand to "" + + PropertyValueExpander(const PropertiesMap &vars, std::string str) + : mVars(vars), mStr(std::move(str)) {} + + bool isKnown(const std::string &name) const { + if (mVars.count(name)) return true; + return std::getenv(name.c_str()) != nullptr; + } + + std::string lookup(const std::string &name) const { + const auto it = mVars.find(name); + if (it != mVars.end()) + return it->second; + const char *env = std::getenv(name.c_str()); + return env ? env : std::string(); + } + + // Parses an identifier, handling nested $(...) within the name. + std::string parseIdentifier() { + std::string result; + while (mPos < mStr.size()) { + if (mStr.compare(mPos, 2, "$(") == 0) { + result += tryParseExpr(); continue; + } + const auto c = static_cast(mStr[mPos]); + if (!std::isalnum(c) && c != '_' && c != '-') break; + result += mStr[mPos++]; } - if (s.empty()) - continue; - fs.includePaths.push_back(s.back() == '/' ? s : (s + '/')); + return result; } + + // Parses one method argument: a quoted string literal or a $(…) reference. + std::string parseArg() { + while (mPos < mStr.size() && std::isspace(static_cast(mStr[mPos]))) + ++mPos; + if (mPos < mStr.size() && mStr[mPos] == '\'') { + ++mPos; + std::string s; + while (mPos < mStr.size() && mStr[mPos] != '\'') + s += mStr[mPos++]; + if (mPos < mStr.size()) ++mPos; // consume closing '\'' + return s; + } + if (mStr.compare(mPos, 2, "$(") == 0) + return tryParseExpr(); + // Bare word — consume until ',' or ')'. + std::string s; + while (mPos < mStr.size() && mStr[mPos] != ',' && mStr[mPos] != ')') + s += mStr[mPos++]; + return s; + } + + // Parses and evaluates $(Name[.Method(args)…]) starting at mPos. + // Also handles $([ClassName]::Method(args)) static property functions. + // If the variable is unknown the token is left unchanged and mPos advances past it. + std::string tryParseExpr() { + const std::size_t start = mPos; + mPos += 2; // skip "$(" + + // $([ClassName]::Method(args)) — static property function + if (mPos < mStr.size() && mStr[mPos] == '[') { + ++mPos; // skip '[' + std::string className; + while (mPos < mStr.size() && mStr[mPos] != ']') + className += mStr[mPos++]; + if (mPos < mStr.size()) ++mPos; // skip ']' + if (mPos + 1 < mStr.size() && mStr[mPos] == ':' && mStr[mPos + 1] == ':') + mPos += 2; // skip '::' + std::string member; + while (mPos < mStr.size()) { + const auto c = static_cast(mStr[mPos]); + if (!std::isalnum(c) && c != '_') break; + member += mStr[mPos++]; + } + std::vector args; + if (mPos < mStr.size() && mStr[mPos] == '(') { + ++mPos; // skip '(' + while (mPos < mStr.size() && mStr[mPos] != ')') { + args.push_back(parseArg()); + while (mPos < mStr.size() && std::isspace(static_cast(mStr[mPos]))) + ++mPos; + if (mPos < mStr.size() && mStr[mPos] == ',') ++mPos; + } + if (mPos < mStr.size()) ++mPos; // skip inner ')' + } + if (mPos < mStr.size() && mStr[mPos] == ')') ++mPos; // skip outer ')' + mChanged = true; + return applyMSBuildStaticFunction(className, member, args); + } + + const std::string name = parseIdentifier(); + if (name.empty() || !isKnown(name)) { + const std::size_t end = findMatchingParen(mStr, start + 2); + mPos = (end != std::string::npos) ? end + 1 : mStr.size(); + if (mReplaceUnknown) { + mChanged = true; + return std::string(); + } + return mStr.substr(start, mPos - start); + } + mChanged = true; + std::string value = lookup(name); + // Parse optional .Method(args) chain. + while (mPos < mStr.size() && mStr[mPos] == '.') { + ++mPos; + std::string method; + while (mPos < mStr.size()) { + const auto c = static_cast(mStr[mPos]); + if (!std::isalnum(c) && c != '_') break; + method += mStr[mPos++]; + } + if (mPos >= mStr.size() || mStr[mPos] != '(') break; + ++mPos; // skip '(' + std::vector args; + while (mPos < mStr.size() && mStr[mPos] != ')') { + args.push_back(parseArg()); + while (mPos < mStr.size() && std::isspace(static_cast(mStr[mPos]))) + ++mPos; + if (mPos < mStr.size() && mStr[mPos] == ',') ++mPos; + } + if (mPos < mStr.size()) ++mPos; // skip ')' + try { value = applyPropertyMethod(value, method, args); } catch (...) {} + } + if (mPos < mStr.size() && mStr[mPos] == ')') ++mPos; // skip closing ')' + return value; + } + + // Expand all property expressions in mStr, multi-pass (capped at 50). + std::string expand() { + const int maxPasses = 50; + for (int pass = 0; pass < maxPasses; ++pass) { + mChanged = false; + mPos = 0; + std::string result; + result.reserve(mStr.size()); + while (mPos < mStr.size()) { + if (mStr.compare(mPos, 2, "$(") == 0) + result += tryParseExpr(); + else + result += mStr[mPos++]; + } + mStr = std::move(result); + if (!mChanged) break; + } + return mStr; + } +}; + +static void expandMSBuildVariables(std::string &s, PropertiesMap &properties) +{ + PropertyValueExpander expander{properties, s}; + s = expander.expand(); } ImportProject::Type ImportProject::import(const std::string &filename, Settings *settings, Suppressions *supprs) @@ -332,7 +935,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 +945,8 @@ 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)) { + PropertiesMap mVariables; + if (importVcxproj(toAbsolute(filename), mVariables, fileFilters)) { setRelativePaths(filename); return ImportProject::Type::VS_VCXPROJ; } @@ -452,8 +1054,8 @@ 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; - fsSetIncludePaths(fs, directory, fs.includePaths, variables); + PropertiesMap properties; + fsSetIncludePaths(fs, directory, fs.includePaths, properties); // 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. fs.file.setFsFileId(fsFileIds[path]++); @@ -463,10 +1065,68 @@ 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, PropertiesMap &properties) { + const std::string absolutePath = toAbsolute(filename); + properties["SolutionDir"] = Path::getPathFromFilename(absolutePath); + properties["SolutionExt"] = Path::getFilenameExtensionInLowerCase(absolutePath); + properties["SolutionPath"] = absolutePath; + + // Path::stripDirectoryPart doesn't work on windows with unix paths + // absolutePath is already normalized to '/' by toAbsolute() + const auto slash = absolutePath.rfind('/'); + properties["SolutionFileName"] = (slash != std::string::npos) ? absolutePath.substr(slash + 1) : absolutePath; + + std::string temp = properties["SolutionFileName"]; + findAndReplace(temp, Path::getFilenameExtension(temp), ""); + properties["SolutionName"] = temp; +} + +static std::string findFile(const std::string &startDirectory, const std::string &file) +{ + // startDirectory comes from MSBuildThisFileDirectory which is already + // normalized to '/' separators by Path::simplifyPath. + 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.rfind('/'); + if (lastSlash == std::string::npos) + break; + currentDir.resize(lastSlash); + } + + return ""; +} + +bool ImportProject::importDirectorySolutionProps(PropertiesMap &properties) +{ + const std::string directorySolutionProps = findFile(properties["ProjectDir"], "Directory.Solution.props"); + if (!directorySolutionProps.empty()) { + MetadataMap data; + std::unordered_set stack; + std::list projectConfigurationList; + const ImportResult result = importPropsOrTargets(directorySolutionProps, properties, data, projectConfigurationList, stack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + directorySolutionProps + "\" - " + importResultStr(result)); + return false; + } + } + return true; +} + +bool ImportProject::importSln(std::istream &istr, const std::string &filename, const std::vector &fileFilters) { + PropertiesMap mVariables; std::string line; + debugs.clear(); + if (!std::getline(istr,line)) { errors.emplace_back("Visual Studio solution file is empty"); return false; @@ -480,12 +1140,29 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const } } - std::map variables; - variables["SolutionDir"] = path; + PropertiesMap 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, "MinimumVisualStudioVersion = ")) { + const std::string ver = line.substr(std::strlen("MinimumVisualStudioVersion = ")); + 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["MinimumVisualStudioVersion"] = (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 +1173,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; } @@ -511,11 +1189,14 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const return false; } - return true; + return importDirectorySolutionProps(mVariables); } bool ImportProject::importSlnx(const std::string& filename, const std::vector& fileFilters) { + PropertiesMap mVariables; + debugs.clear(); + tinyxml2::XMLDocument doc; const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); if (error != tinyxml2::XML_SUCCESS) { @@ -523,423 +1204,1353 @@ bool ImportProject::importSlnx(const std::string& filename, const std::vectorName(), "Solution") != 0) { + errors.emplace_back("Invalid Visual Studio solution file format"); + return false; + } + + PropertiesMap solutionVariables; + setSolution(filename, solutionVariables); + + solutionVariables["VisualStudioVersion"] = "18.0"; + + bool found = false; + + auto processProject = [&](const tinyxml2::XMLElement* projectNode) -> bool { + const char* pathAttribute = projectNode->Attribute("Path"); + if (pathAttribute == nullptr) + return true; + + std::string vcxproj(pathAttribute); + vcxproj = Path::toNativeSeparators(std::move(vcxproj)); + + if (Path::getFilenameExtensionInLowerCase(vcxproj) != ".vcxproj") + return true; // skip other project types + + vcxproj = toAbsolute(vcxproj, solutionVariables["SolutionDir"], solutionVariables); + + vcxproj = Path::fromNativeSeparators(std::move(vcxproj)); + + mVariables = solutionVariables; + if (!importVcxproj(vcxproj, mVariables, fileFilters)) { + errors.emplace_back("failed to load '" + vcxproj + "' from Visual Studio solution"); + return false; + } + found = true; + return true; + }; + + for (const tinyxml2::XMLElement* node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { + const char* name = node->Name(); + if (std::strcmp(name, "Project") == 0) { + if (!processProject(node)) + return false; + } else if (std::strcmp(name, "Folder") == 0) { + // 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; + } + } + + if (!found) { + errors.emplace_back("no projects found in Visual Studio solution file"); + return false; + } + + return importDirectorySolutionProps(mVariables); +} + +ImportProject::ProjectConfiguration::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 if (platformStr == "ARM64") + platform = ARM64; + else if (platformStr == "ARM") + platform = ARM; + else + platform = Unknown; + } + } +} + +void ImportProject::checkUnexpandedExpressions(const std::string &text, const char *context) +{ + // these are emulated so ignore them + if (text == "$(VCTargetsPath)/Microsoft.Cpp.targets" || + text == "$(VCTargetsPath)/Microsoft.Cpp.props" || + text == "$(VCTargetsPath)/Microsoft.Cpp.Default.props") + return; + + 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; + 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; + debugs.emplace_back(message.str()); + pos = end + 1; + } +} + +namespace { + // see https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-conditions + class ConditionParser { + public: + ConditionParser(const std::string &condition, const PropertiesMap &properties) + : mCondition(condition), mVariables(properties) {} + + 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"; + } + + private: + const std::string &mCondition; + const PropertiesMap &mVariables; + std::size_t mPos = 0; + bool mEvaluate = true; // false while parsing a short-circuited operand + + 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; + } + + 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")) { + const bool savedEvaluate = mEvaluate; + if (lhs == "True") mEvaluate = false; + const std::string rhs = parseAnd(); + mEvaluate = savedEvaluate; + if (lhs != "True") + lhs = (rhs == "True") ? "True" : "False"; + } + return lhs; + } + + std::string parseAnd() { + std::string lhs = parseUnary(); + while (matchWord("and")) { + const bool savedEvaluate = mEvaluate; + if (lhs == "False") mEvaluate = false; + const std::string rhs = parseUnary(); + mEvaluate = savedEvaluate; + if (lhs != "False") + lhs = (rhs == "True") ? "True" : "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(); + } + + std::string parsePrimary() { + skipWhitespace(); + + if (match("(")) { + std::string value = parseOr(); + expect(")"); + return value; + } + + 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) { + if (match(op)) { + const std::string rhs = parseValue(); + if (!mEvaluate) return "False"; + return compare(lhs, op, rhs) ? "True" : "False"; + } + } + + 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); + } + + 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; + } + + if (mPos != begin) + return mCondition.substr(begin, mPos - begin); + if (!mEvaluate) + return std::string(); + 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; + } + + if (!mEvaluate) + return std::string(); + throw std::runtime_error("Can not tokenize condition"); + } + + static bool parseInteger(const std::string &s, long &value) + { + if (s.empty()) + return false; + + const char *begin = s.c_str(); + char *end = nullptr; + int base = 10; + + 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'; + } + + std::string parseIdentifier() { + skipWhitespace(); + std::string result; + while (mPos < mCondition.size()) { + if (mCondition.compare(mPos, 2, "$(") == 0) { + result += parsePropertyExpression(); + continue; + } + const auto c = static_cast(mCondition[mPos]); + if (!std::isalnum(c) && c != '_' && c != '-') + break; + result += mCondition[mPos++]; + } + if (result.empty()) + throw std::runtime_error("Expected identifier in condition '" + mCondition + "'"); + return result; + } + + std::string parsePropertyExpression() { + expect("$("); + + // $([ClassName]::Method(args)) — static property function + if (mPos < mCondition.size() && mCondition[mPos] == '[') { + ++mPos; // skip '[' + std::string className; + while (mPos < mCondition.size() && mCondition[mPos] != ']') + className += mCondition[mPos++]; + if (mPos < mCondition.size()) ++mPos; // skip ']' + if (mPos + 1 < mCondition.size() && mCondition[mPos] == ':' && mCondition[mPos + 1] == ':') + mPos += 2; // skip '::' + std::string member; + while (mPos < mCondition.size()) { + const auto c = static_cast(mCondition[mPos]); + if (!std::isalnum(c) && c != '_') break; + member += mCondition[mPos++]; + } + std::vector args; + if (mPos < mCondition.size() && mCondition[mPos] == '(') { + ++mPos; // skip '(' + skipWhitespace(); + if (!match(")")) { + do { + args.push_back(parseValue()); + skipWhitespace(); + } while (match(",")); + expect(")"); + } + } + expect(")"); // outer closing paren of $(...) + return mEvaluate ? applyMSBuildStaticFunction(className, member, args) : std::string(); + } + + 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 = mEvaluate ? applyMethod(value, method, args) : std::string(); + } + + expect(")"); + return value; + } + + 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; + } + + return (Path::isFile(path) || Path::isDirectory(path)) ? "True" : "False"; + } + + std::string parseHasTrailingSlash() { + expect("("); + const std::string value = parseValue(); + expect(")"); + + 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 { + // Delegate to PropertyValueExpander. In condition context unknown + // variables must expand to "" (MSBuild semantics for quoted strings). + PropertyValueExpander expander{mVariables, input}; + expander.mReplaceUnknown = true; + return expander.expand(); + } + + static std::string applyMethod(std::string value, + const std::string &method, + const std::vector &args) { + return applyPropertyMethod(std::move(value), method, args); + } + + 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) { + // Missing trailing components are treated as 0, + // so {17} == {17, 0, 0} and {17, 1} > {17, 0, 5} is correct. + const int l = (i < lhs.size()) ? lhs[i] : 0; + const int r = (i < rhs.size()) ? rhs[i] : 0; + if (l < r) + return -1; + if (l > r) + 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; + } + + 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()) + return {}; + + return parts; + }; + + if (op == "==" || op == "!=") { + const bool strEqual = caseInsensitiveStringCompare(lhs, rhs) == 0; + if (!strEqual) { + // "17" and "17.0.0.0" represent the same version; try version comparison + const auto lv = parseVersion(lhs); + const auto rv = parseVersion(rhs); + if (!lv.empty() && !rv.empty()) { + const bool verEqual = compareVersions(lv, rv) == 0; + return (op == "==") ? verEqual : !verEqual; + } + } + return (op == "==") ? strEqual : !strEqual; + } + + if (caseInsensitiveStringCompare(lhs, "Current") == 0) { + const auto rhsVersion = parseVersion(rhs); + if (!rhsVersion.empty()) { + const std::vector currentVersion{ 18 }; + return compareVersionResult(compareVersions(currentVersion, rhsVersion), op); + } + } + + if (caseInsensitiveStringCompare(rhs, "Current") == 0) { + const auto lhsVersion = parseVersion(lhs); + if (!lhsVersion.empty()) { + const std::vector currentVersion{ 18 }; + return compareVersionResult(compareVersions(lhsVersion, currentVersion), 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 PropertiesMap &properties) { + try { + return ConditionParser(condition, properties).parse(); + } catch (const std::exception &) { + // malformed or unhandled condition syntax (e.g. property functions, + // unknown methods, bare .Property access) — treat as false so import continues + return false; + } + } + + bool conditionIsTrue(const tinyxml2::XMLElement *node, const PropertiesMap &properties) { + const char *condAttr = node->Attribute("Condition"); + if (!condAttr) + return true; + return evalCondition(condAttr, properties); + } + + bool hasName(const tinyxml2::XMLElement *node, const char *nodeName, const PropertiesMap &properties) { + const char *name = node->Name(); + if (!name || std::strcmp(nodeName, name) != 0) + return false; + return conditionIsTrue(node, properties); + } + + bool hasNameAndAttribute(const tinyxml2::XMLElement *node, const char *nodeName, const char *attrName, const PropertiesMap &properties) { + const char *name = node->Name(); + const char *attr = node->Attribute(attrName); + if (!name || !attr || std::strcmp(nodeName, name) != 0) + return false; + return conditionIsTrue(node, properties); + } + + bool hasNameAndLabel(const tinyxml2::XMLElement *node, const char *nodeName, const char *nodeAttr, const PropertiesMap &properties) { + 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, properties); + } + + bool hasNameAndNotLabel(const tinyxml2::XMLElement *node, const char *nodeName, const char *nodeAttr, const PropertiesMap &properties) { + 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, properties); + } + + 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) { + if (pos2 > pos1) + 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; + } + + struct MSBuildThis { + PropertiesMap &propertiesMap; + std::string thisFile; + std::string thisFileName; + std::string thisFileExtension; + std::string thisFileDirectory; + std::string thisFileDirectoryNoRoot; + std::string thisFileFullPath; + + MSBuildThis(const std::string &filename, PropertiesMap &properties) + : propertiesMap(properties) + , thisFile(properties["MSBuildThisFile"]) + , thisFileName(properties["MSBuildThisFileName"]) + , thisFileExtension(properties["MSBuildThisFileExtension"]) + , thisFileDirectory(properties["MSBuildThisFileDirectory"]) + , thisFileDirectoryNoRoot(properties["MSBuildThisFileDirectoryNoRoot"]) + , thisFileFullPath(properties["MSBuildThisFileFullPath"]) { + setMSBuildThis(filename, properties); + } + + static void setMSBuildThis(const std::string &filename, PropertiesMap &properties) { + // Normalize once so all subsequent path ops can assume '/' separators. + const std::string nfilename = Path::simplifyPath(Path::fromNativeSeparators(filename)); + properties["MSBuildThisFileFullPath"] = nfilename; + const auto slash1 = nfilename.rfind('/'); + std::string temp = (slash1 != std::string::npos) ? nfilename.substr(slash1 + 1) : nfilename; + properties["MSBuildThisFile"] = temp; + findAndReplace(temp, Path::getFilenameExtension(temp), ""); + properties["MSBuildThisFileName"] = temp; + properties["MSBuildThisFileDirectory"] = Path::getPathFromFilename(nfilename); + temp = Path::getPathFromFilename(nfilename); + std::string::size_type pos = temp.find('/', 0); + temp.erase(0, pos + 1); + properties["MSBuildThisFileDirectoryNoRoot"] = temp; + properties["MSBuildThisFileExtension"] = Path::getFilenameExtensionInLowerCase(nfilename); + } + + ~MSBuildThis() { + propertiesMap["MSBuildThisFile"] = thisFile; + propertiesMap["MSBuildThisFileName"] = thisFileName; + propertiesMap["MSBuildThisFileExtension"] = thisFileExtension; + propertiesMap["MSBuildThisFileDirectory"] = thisFileDirectory; + propertiesMap["MSBuildThisFileDirectoryNoRoot"] = thisFileDirectoryNoRoot; + propertiesMap["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); + } + }; +} + +std::string ImportProject::toAbsolute(const std::string &path) +{ + std::string internal(Path::fromNativeSeparators(path)); + if (Path::isAbsolute(internal)) + return Path::simplifyPath(internal); + return Path::simplifyPath(Path::getCurrentPath() + "/" + internal); +} + +std::string ImportProject::toAbsolute(const std::string &filename, const std::string &baseDir, PropertiesMap &properties) +{ + std::string resolved(Path::fromNativeSeparators(filename)); + if (!simplifyPathWithVariables(resolved, properties)) + return resolved; + + if (Path::isAbsolute(resolved)) + return Path::simplifyPath(resolved); + return Path::simplifyPath(baseDir + resolved); +} + +bool ImportProject::simplifyPathWithVariables(std::string &s, PropertiesMap &properties) +{ + // Normalize native separators before expansion so the expander sees clean + // paths and debug messages report '/' not '\\'. + s = Path::fromNativeSeparators(std::move(s)); + expandMSBuildVariables(s, properties); + checkUnexpandedExpressions(s, "path"); + if (s.find("$(") != std::string::npos) + return false; + // Property values substituted above may also carry native separators; normalize again. + s = Path::fromNativeSeparators(std::move(s)); + s = Path::simplifyPath(std::move(s)); + return true; +} + +void ImportProject::fsSetIncludePaths(FileSettings &fs, const std::string &basepath, const std::list &in, PropertiesMap &properties) +{ + 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, properties)) + continue; + } + if (s.empty()) + continue; + fs.includePaths.push_back(s.back() == '/' ? s : (s + '/')); + } +} + +void ImportProject::addProperty(const tinyxml2::XMLElement *node, PropertiesMap &properties) { + const char *eName = node->Name(); + if (!eName || !conditionIsTrue(node, properties)) + return; + const char *eText = node->GetText(); + std::string text(eText ? eText : ""); + // Normalize native path separators before expansion so property values are + // stored with '/' and debug messages show normalized paths. + text = Path::fromNativeSeparators(std::move(text)); + const std::string original = properties[eName]; + findAndReplace(text, "$(" + std::string(eName) + ")", original); + expandMSBuildVariables(text, properties); + properties[eName] = text; + checkUnexpandedExpressions(text, eName); +} + +void ImportProject::addMetadata(const tinyxml2::XMLElement *node, PropertiesMap &properties, MetadataMap &metadata) { + const char *eName = node->Name(); + if (!eName || !conditionIsTrue(node, properties)) + return; + const char *eText = node->GetText(); + std::string text(eText ? eText : ""); + text = Path::fromNativeSeparators(std::move(text)); + const std::string original = metadata[eName]; + findAndReplace(text, "%(" + std::string(eName) + ")", original); + std::string::size_type pos = 0; + while ((pos = text.find("%(", pos)) != std::string::npos) { + const std::string::size_type end = text.find(')', pos); + if (end == std::string::npos) + break; + const std::string key = text.substr(pos + 2, end - pos - 2); + const auto it = metadata.find(key); + const std::string replacement = (it != metadata.end()) ? it->second : std::string(); + text.replace(pos, end - pos + 1, replacement); + pos += replacement.size(); + } + expandMSBuildVariables(text, properties); + metadata[eName] = text; + checkUnexpandedExpressions(text, eName); +} + +std::string ImportProject::getMetadata(const tinyxml2::XMLElement *node, PropertiesMap &properties, const MetadataMap &metadata, const std::string &original) { + const char *eName = node->Name(); + const char *eText = node->GetText(); + if (!eName || !eText || !conditionIsTrue(node, properties)) + return original; + std::string text(Path::fromNativeSeparators(eText)); + findAndReplace(text, "%(" + std::string(eName) + ")", original); + { + std::string::size_type pos = 0; + while ((pos = text.find("%(", pos)) != std::string::npos) { + const std::string::size_type end = text.find(')', pos); + if (end == std::string::npos) break; + const std::string key = text.substr(pos + 2, end - pos - 2); + const auto it = metadata.find(key); + const std::string replacement = (it != metadata.end()) ? it->second : std::string(); + text.replace(pos, end - pos + 1, replacement); + pos += replacement.size(); + } + } + expandMSBuildVariables(text, properties); + 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; +} - if (std::strcmp(rootnode->Name(), "Solution") != 0) { - errors.emplace_back("Invalid Visual Studio solution file format"); - return false; +ImportProject::ImportResult ImportProject::importCompile(const tinyxml2::XMLElement *node, + const std::string &projectDir, + PropertiesMap &properties, + const MetadataMap &metadata, + std::list &compileList) { + const char *include = node->Attribute("Include"); + if (!include) + return ImportResult::NotFound; + + std::string toInclude = toAbsolute(include, projectDir, properties); + if (!Path::acceptFile(toInclude)) + return ImportResult::NotFound; + + ItemGroupClCompile compile(toInclude); + // a file with no override of its own inherits the ItemDefinitionGroup value outright + compile.metadata = metadata; + 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", properties)) { + if (caseInsensitiveStringCompare(text, "true") == 0) { + excludedFromBuild = true; + break; + } + } else if (hasName(e1, "AdditionalIncludeDirectories", properties)) { + auto &v = compile.metadata["AdditionalIncludeDirectories"]; + v = getMetadata(e1, properties, compile.metadata, v); + } else if (hasName(e1, "ForcedIncludeFiles", properties)) { + auto &v = compile.metadata["ForcedIncludeFiles"]; + v = getMetadata(e1, properties, compile.metadata, v); + } else if (hasName(e1, "PreprocessorDefinitions", properties)) { + auto &v = compile.metadata["PreprocessorDefinitions"]; + v = getMetadata(e1, properties, compile.metadata, v); + } else if (hasName(e1, "LanguageStandard", properties)) { + auto &v = compile.metadata["LanguageStandard"]; + v = getMetadata(e1, properties, compile.metadata, v); + } else if (hasName(e1, "AdditionalOptions", properties)) { + auto &v = compile.metadata["AdditionalOptions"]; + v = getMetadata(e1, properties, compile.metadata, v); + } else if (hasName(e1, "AdditionalUsingDirectories", properties)) { + auto &v = compile.metadata["AdditionalUsingDirectories"]; + v = getMetadata(e1, properties, compile.metadata, v); + } } - std::map variables; - variables["SolutionDir"] = Path::simplifyPath(Path::getPathFromFilename(filename)); + if (!compile.metadata["AdditionalOptions"].empty()) { + std::vector args; + std::string arg; + bool quoted = false; + const std::string &additionalOptions = compile.metadata["AdditionalOptions"]; + + for (std::size_t i = 0; i < additionalOptions.size(); ++i) { + const char c = additionalOptions[i]; + + if (c == '"') { + quoted = !quoted; + } else if (std::isspace(static_cast(c)) && !quoted) { + if (!arg.empty()) { + args.emplace_back(std::move(arg)); + arg.clear(); + } + } else { + arg += c; + } + } - bool found = false; - std::vector sharedItemsProjects; + if (!arg.empty()) + args.emplace_back(std::move(arg)); - auto processProject = [&](const tinyxml2::XMLElement* projectNode) { - const char* pathAttribute = projectNode->Attribute("Path"); - if (pathAttribute == nullptr) - return true; + for (std::size_t i = 0; i < args.size(); ++i) { + const std::string &option = args[i]; - std::string vcxproj(pathAttribute); - vcxproj = Path::toNativeSeparators(std::move(vcxproj)); + if (option.size() >= 2 && + (option[0] == '/' || option[0] == '-') && + (option[1] == 'D' || option[1] == 'd')) { - if (Path::getFilenameExtensionInLowerCase(vcxproj) != ".vcxproj") - return true; // skip other project types + std::string define = option.substr(2); - if (!Path::isAbsolute(vcxproj)) - vcxproj = variables["SolutionDir"] + vcxproj; + // /D NAME + if (define.empty() && i + 1 < args.size()) + define = args[++i]; - vcxproj = Path::fromNativeSeparators(std::move(vcxproj)); - if (!importVcxproj(vcxproj, variables, "", fileFilters, sharedItemsProjects)) { - errors.emplace_back("failed to load '" + vcxproj + "' from Visual Studio solution"); - return false; - } - found = true; - return true; - }; + if (!define.empty()) { + if (!compile.metadata["PreprocessorDefinitions"].empty()) + compile.metadata["PreprocessorDefinitions"] += ';'; + compile.metadata["PreprocessorDefinitions"] += define; + } - for (const tinyxml2::XMLElement* node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { - const char* name = node->Name(); - if (std::strcmp(name, "Project") == 0) { - if (!processProject(node)) - return false; - } else if (std::strcmp(name, "Folder") == 0) { - for (const tinyxml2::XMLElement* childNode = node->FirstChildElement(); childNode; childNode = childNode->NextSiblingElement()) { - if (std::strcmp(childNode->Name(), "Project") == 0) { - if (!processProject(childNode)) - return false; + } else if (option.size() >= 2 && + (option[0] == '/' || option[0] == '-') && + (option[1] == 'I' || option[1] == 'i')) { + + std::string path = option.substr(2); + + // /I path + if (path.empty() && i + 1 < args.size()) + path = args[++i]; + + if (!path.empty()) { + if (!compile.metadata["AdditionalIncludeDirectories"].empty()) + compile.metadata["AdditionalIncludeDirectories"] += ';'; + compile.metadata["AdditionalIncludeDirectories"] += path; } + } else if (option == "/std:c++11" || option == "-std=c++11") { + compile.metadata["LanguageStandard"] = "stdcpp11"; + } else if (option == "/std:c++14" || option == "-std=c++14") { + compile.metadata["LanguageStandard"] = "stdcpp14"; + } else if (option == "/std:c++17" || option == "-std=c++17") { + compile.metadata["LanguageStandard"] = "stdcpp17"; + } else if (option == "/std:c++20" || option == "-std=c++20") { + compile.metadata["LanguageStandard"] = "stdcpp20"; + } else if (option == "/std:c++23" || option == "-std=c++23") { + compile.metadata["LanguageStandard"] = "stdcpp23"; + } else if (option == "/std:c++latest" || option == "-std=c++latest") { + compile.metadata["LanguageStandard"] = "stdcpplatest"; } } } - if (!found) { - errors.emplace_back("no projects found in Visual Studio solution file"); - return false; - } + if (!excludedFromBuild) + compileList.emplace_back(compile); - return true; + return ImportResult::Ok; } -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; +ImportProject::ImportResult ImportProject::importProject(const tinyxml2::XMLElement *node, + const std::string &projectDir, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &projectConfigurationList, + std::unordered_set &importStack) { + const char *projectAttribute = node->Attribute("Project"); + if (!projectAttribute) + return ImportResult::Ok; + std::string file = toAbsolute(projectAttribute, projectDir, properties); + std::string extension = Path::getFilenameExtensionInLowerCase(file); + if (extension == ".props" || extension == ".targets") { + const char *sdk = node->Attribute("Sdk"); + if (sdk) + return ImportResult::NotResolvable; + + if (file.find("Microsoft.Cpp.targets") != std::string::npos) { + auto it = properties.find("ForceImportBeforeCppTargets"); + if (it != properties.end()) { + ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); + return result; } } - } - 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; - } - 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); + if (file.find("$(") != std::string::npos) { + std::string directoryBuildTargets = findFile(projectDir, "Directory.Build.targets"); + if (!directoryBuildTargets.empty()) { + ImportResult result = importPropsOrTargets(directoryBuildTargets, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + directoryBuildTargets + "\" - " + importResultStr(result)); + return result; + } + } + } else { + ImportResult result = importPropsOrTargets(file, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); + return result; + } } - } - // 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); - } - catch (const std::runtime_error& r) - { - errors.emplace_back(filename + ": Can not evaluate condition '" + mCondition + "': " + r.what()); - return false; + it = properties.find("ForceImportAfterCppTargets"); + if (it != properties.end()) { + ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); + return result; + } } - } - 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); + return ImportResult::Ok; + } - 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 (file.find("Microsoft.Cpp.Default.props") != std::string::npos) { + auto it = properties.find("ForceImportBeforeCppDefaultProps"); + if (it != properties.end()) { + ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); + return result; + } } - // 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 (file.find("$(") != std::string::npos) { + // $(Configuration) = Debug, $(ConfigurationType) = Application, $(ApplicationType) + } else { + ImportResult result = importPropsOrTargets(file, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); + return result; } - if (!lpar.empty()) - throw std::runtime_error("'(' without closing ')'!"); } - // 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("||"); + it = properties.find("ForceImportAfterCppDefaultProps"); + if (it != properties.end()) { + ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); + return result; + } } - tokenlist.createAst(); - - // 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"; + return ImportResult::Ok; + } + + if (file.find("Microsoft.Cpp.props") != std::string::npos) { + // If ForceImportBeforeCppProps is already set (e.g. by the vcxproj itself), + // honour it now before anything else. + const bool hadForceImportBefore = properties.count("ForceImportBeforeCppProps") > 0; + std::string forceImportBeforeCppProps; + if (hadForceImportBefore) { + auto it = properties.find("ForceImportBeforeCppProps"); + forceImportBeforeCppProps = it->second; + ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); + return result; } } - throw std::runtime_error("Invalid condition: '" + condition + "'"); - } - - - private: - - static std::string executeOp1(const Token* tok, const ProjectConfiguration &p) { - return execute(tok->astOperand1(), p); - } - - static std::string executeOp2(const Token* tok, const ProjectConfiguration &p) { - return execute(tok->astOperand2(), p); - } - - 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 + "'"); - } - if (tok->str().size() >= 2 && tok->str()[0] == '\'') // String Literal - return tok->str(); - - throw std::runtime_error("Unknown/unhandled operator/operand '" + tok->str() + "'"); - } - - std::string mCondition; - }; - 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; - } + if (file.find("$(") != std::string::npos) { + // $(Platform), $(PlatformToolset), $(TargetName), $(TargetExt), $(LanguageStandard) + properties["IntDir"] = "$(Platform)/$(Configuration)/"; + properties["OutDir"] = "$(SolutionDir)$(Platform)/$(Configuration)/"; + properties["GeneratedFilesDir"] = "$(IntDir)Generated Files/"; + + std::string directoryBuildProps = findFile(projectDir, "Directory.Build.props"); + if (!directoryBuildProps.empty()) { + ImportResult result = importPropsOrTargets(directoryBuildProps, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + directoryBuildProps + "\" - " + importResultStr(result)); + return result; } } - 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; + + // Directory.Build.props may have newly set ForceImportBeforeCppProps + // (e.g. PowerToys sets it to Cpp.Build.props which defines ProjectConfigurations). + // Real MSBuild auto-imports Directory.Build.props before evaluating + // Microsoft.Cpp.props, so ForceImportBeforeCppProps set there must be + // honoured here. + auto it = properties.find("ForceImportBeforeCppProps"); + if (it != properties.end()) { + if (it->second != forceImportBeforeCppProps) { + ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); + return result; } } } + } else { + ImportResult result = importPropsOrTargets(file, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); + return result; + } } - } - - std::string enhancedInstructionSet; - std::string preprocessorDefinitions; - std::string additionalIncludePaths; - std::string entryPointSymbol; // TODO: use this - Standards::cppstd_t cppstd = Standards::CPPLatest; - }; - 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; + auto it = properties.find("ForceImportAfterCppProps"); + if (it != properties.end()) { + ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); + return result; } } - } - bool useOfMfc = false; - bool useUnicode = false; - }; + return ImportResult::Ok; + } - 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); - } - // TODO: ForcedIncludeFiles and PrecompiledHeaderFile - } + ImportResult result = importPropsOrTargets(file, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); + return result; } - 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; - } - return false; + if (result == ImportResult::NotResolvable) { + debugs.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); } - std::string mFilename; - std::list mConditions; - }; -} - -static 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; + } else { + debugs.emplace_back("Could not import \"" + file + "\" unsupported extension " + extension); } - if (pos1 < s.size()) - ret.push_back(s.substr(pos1)); - return ret; + return ImportResult::Ok; } -static void importPropertyGroup(const tinyxml2::XMLElement *node, std::map &variables, std::string &includePath) +ImportProject::ImportResult ImportProject::importPropsOrTargets(const std::string &file, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &projectConfigurationList, + std::unordered_set &importStack) { - 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 filename(file); + // properties can't be resolved + if (!simplifyPathWithVariables(filename, properties)) + return ImportResult::NotResolvable; - } 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) - 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); + // prepend project dir (if it exists) to transform relative paths into absolute ones + if (!Path::isAbsolute(filename) && properties.count("ProjectDir") > 0) + filename = toAbsolute(filename, properties.at("ProjectDir"), properties); + + // 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 ImportResult::NotFound; + + const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement(); + if (rootnode == nullptr) + return ImportResult::NotValid; + + MSBuildThis msBuildThis(filename, properties); + std::string propsDir = Path::getPathFromFilename(filename); + + ImportResult ret = ImportResult::Ok; + for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { + if (hasName(node, "ImportGroup", properties)) { + // 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) || + (std::strcmp(label, "ExtensionSettings") == 0) || + (std::strcmp(label, "ExtensionTargets") == 0); + if (isPropertySheets) { + for (const tinyxml2::XMLElement *importGroup = node->FirstChildElement(); importGroup; importGroup = importGroup->NextSiblingElement()) { + if (hasNameAndAttribute(importGroup, "Import", "Project", properties)) { + ImportResult result = importProject(importGroup, propsDir, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) + return result; + } + } + } + } else if (hasName(node, "PropertyGroup", properties)) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) + addProperty(e, properties); + } else if (hasName(node, "ItemDefinitionGroup", properties)) { + for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { + if (hasName(e1, "ClCompile", properties)) { + for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) { + addMetadata(e2, properties, metadata); + } + } + } + } else if (hasNameAndLabel(node, "ItemGroup", "ProjectConfigurations", properties)) { + for (const tinyxml2::XMLElement *pcNode = node->FirstChildElement("ProjectConfiguration"); pcNode; pcNode = pcNode->NextSiblingElement("ProjectConfiguration")) { + const ProjectConfiguration pc(pcNode); + if (!pc.configuration.empty()) { + // Deduplicate: the same config can arrive again when Directory.Build.props / + // Cpp.Build.props is re-imported inside the per-config loop. + const bool already = std::any_of(projectConfigurationList.cbegin(), + projectConfigurationList.cend(), + [&pc](const ProjectConfiguration &existing) { + return existing.name == pc.name; + }); + if (!already) { + projectConfigurationList.emplace_back(pc); + mAllVSConfigs.insert(pc.configuration); + } + } + } + } else if (hasNameAndAttribute(node, "Import", "Project", properties)) { + ImportResult result = importProject(node, propsDir, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) + return result; } } + + return ret; } -static void loadVisualStudioProperties(const std::string &props, std::map &variables, std::string &includePath, const std::string &additionalIncludeDirectories, std::list &itemDefinitionGroupList) +ImportProject::ImportResult ImportProject::importVcxitems(const std::string &items, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack) { - std::string filename(props); - // variables can't be resolved - if (!simplifyPathWithVariables(filename, variables)) - return; + std::string filename(items); + // properties can't be resolved + if (!simplifyPathWithVariables(filename, properties)) + 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); + 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; - const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement(); + 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; + return ImportResult::NotValid; + + const std::string itemsDir = Path::simplifyPath(Path::getPathFromFilename(filename)); + MSBuildThis msBuildThis(filename, properties); + 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; - } - loadVisualStudioProperties(loadprj, variables, includePath, additionalIncludeDirectories, itemDefinitionGroupList); + if (hasName(node, "ItemGroup", properties)) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { + if (hasName(e, "ClCompile", properties)) { + importCompile(e, itemsDir, properties, metadata, compileList); + } + } + } else if (hasName(node, "PropertyGroup", properties)) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) + addProperty(e, properties); + } else if (hasName(node, "ItemDefinitionGroup", properties)) { + for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { + if (hasName(e1, "ClCompile", properties)) { + for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) + addMetadata(e2, properties, metadata); } } - } 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 (hasNameAndAttribute(node, "Import", "Project", properties)) { + const ImportResult result = importProject(node, itemsDir, properties, metadata,projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) + return result; } } + + return ImportResult::Ok; } bool ImportProject::importVcxproj(const std::string &filename, - std::map &variables, - const std::string &additionalIncludeDirectories, - const std::vector &fileFilters, - std::vector &cache) + PropertiesMap &properties, + const std::vector &fileFilters) { tinyxml2::XMLDocument doc; const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); @@ -947,250 +2558,283 @@ 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); -} + MetadataMap metadata; + + // Normalize separators once; callers typically pass toAbsolute() results + // but normalize here as a safety net so all subsequent rfind('/') are correct. + const std::string nfilename = Path::simplifyPath(Path::fromNativeSeparators(filename)); + + properties.emplace("VisualStudioVersion", "17.0"); + + properties["ProjectPath"] = nfilename; + const auto projSlash = nfilename.rfind('/'); + std::string temp = (projSlash != std::string::npos) ? nfilename.substr(projSlash + 1) : nfilename; + properties["ProjectFileName"] = temp; + findAndReplace(temp, Path::getFilenameExtension(temp), ""); + properties["ProjectName"] = temp; + temp.resize(std::min(temp.size(), size_t(16))); + properties["ShortProjectName"] = temp; + properties["ProjectExt"] = Path::getFilenameExtensionInLowerCase(nfilename); + properties["ProjectDir"] = Path::getPathFromFilename(nfilename); + + // importVcxproj called directly + if (properties.find("SolutionDir") == properties.end()) { + debugs.clear(); + properties["SolutionDir"] = properties["ProjectDir"]; + } -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["ProjectDir"] = Path::simplifyPath(Path::getPathFromFilename(filename)); + properties["MSBuildProjectName"] = properties["ProjectName"]; + properties["MSBuildProjectExtension"] = properties["ProjectExt"]; + properties["MSBuildProjectDirectory"] = properties["ProjectDir"]; + // remove file seperator on end of path + if (!properties["MSBuildProjectDirectory"].empty() && + (properties["MSBuildProjectDirectory"].back() == '/' || + properties["MSBuildProjectDirectory"].back() == '\\')) { + properties["MSBuildProjectDirectory"].pop_back(); + } + properties["MSBuildProjectFile"] = properties["ProjectFileName"]; + properties["MSBuildProjectFullPath"] = properties["ProjectPath"]; + + MSBuildThis::setMSBuildThis(nfilename, properties); + std::string projectDir = properties["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"); + properties["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", properties)) { + 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); + } + } + + // Discovery pass: if no ProjectConfigurations were found inline in the vcxproj, walk + // its / nodes through importProject so that every MSBuild import + // mechanism (Directory.Build.props, ForceImportBeforeCppProps, etc.) is honoured + // generically — no special-casing of individual property names required. + // We also process nodes so that properties needed to resolve import + // paths are available. Stop as soon as configurations are found. + // Use isolated copies of properties, metadata and importStack so that side-effects + // of the discovery imports (extra properties, pre-populated import stack, etc.) do + // not bleed into the real per-configuration import pass that follows. + if (projectConfigurationList.empty()) { + PropertiesMap discoverProps = properties; + MetadataMap discoverMeta; + std::unordered_set discoverStack; + for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); + node && projectConfigurationList.empty(); + node = node->NextSiblingElement()) { + if (hasName(node, "PropertyGroup", discoverProps)) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) + addProperty(e, discoverProps); + } else if (hasName(node, "ImportGroup", discoverProps)) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e && projectConfigurationList.empty(); e = e->NextSiblingElement()) { + if (hasNameAndAttribute(e, "Import", "Project", discoverProps)) + importProject(e, projectDir, discoverProps, discoverMeta, projectConfigurationList, discoverStack); + } + } else if (hasNameAndAttribute(node, "Import", "Project", discoverProps)) { + importProject(node, projectDir, discoverProps, discoverMeta, projectConfigurationList, discoverStack); } - } else if (std::strcmp(name, "ImportGroup") == 0) { - const char *labelAttribute = node->Attribute("Label"); - if (labelAttribute && std::strcmp(labelAttribute, "PropertySheets") == 0) { + } + } + + PropertiesMap originalVariables = properties; + + bool first = true; + + for (const ProjectConfiguration &pc : projectConfigurationList) { + if (!first) { + compileList.clear(); + properties = originalVariables; + metadata.clear(); + } else + first = false; + + properties["Configuration"] = pc.configuration; + properties["Platform"] = pc.platformStr; + + for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { + if (hasNameAndNotLabel(node, "ItemGroup", "ProjectConfigurations", properties)) { 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", properties)) + importCompile(e, projectDir, properties, metadata, compileList); + } + } else if (hasName(node, "ItemDefinitionGroup", properties)) { + for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { + if (hasName(e1, "ClCompile", properties)) { + for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) + addMetadata(e2, properties, metadata); } } - } 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"); + } else if (hasName(node, "PropertyGroup", properties)) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) + addProperty(e, properties); + } else if (hasName(node, "ImportGroup", properties)) { + 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", properties)) { + const char *projectAttribute = e->Attribute("Project"); + if (!projectAttribute) + continue; + if (importProject(e, projectDir, properties, metadata, 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", properties)) { + const char *projectAttribute = e->Attribute("Project"); + if (!projectAttribute) + continue; + std::string file = toAbsolute(projectAttribute, projectDir, properties); + std::string extension = Path::getFilenameExtensionInLowerCase(file); + if (extension == ".vcxitems") { + ImportResult result = importVcxitems(file, properties, metadata, 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 \"" + file + "\" unsupported extension " + extension); } - - 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 { + // Unlabeled or other-labeled ImportGroup (e.g. ExtensionSettings, + // ExtensionTargets) — process children like PropertySheets. + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { + if (hasName(e, "Import", properties)) { + const char *projectAttribute = e->Attribute("Project"); + if (!projectAttribute) + continue; + if (importProject(e, projectDir, properties, metadata, projectConfigurationList, importStack) > ImportResult::NotResolvable) return false; - } - sharedItemsProjects.emplace_back(toAdd); } } } + } else if (hasNameAndAttribute(node, "Import", "Project", properties)) { + if (importProject(node, projectDir, properties, metadata, 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)); - } - } - // Project files - PathMatch filtermatcher(fileFilters, Path::getCurrentPath()); - for (const ItemGroupClCompile& compile : compileList) { - if (!fileFilters.empty() && !filtermatcher.match(compile.mFilename)) - continue; + // # 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 - 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"; + } else if (pc.platform == ProjectConfiguration::ARM64) { + fs.platformType = Platform::Type::WinARM64; + fs.defines += ";_M_ARM64=1"; + } else if (pc.platform == ProjectConfiguration::ARM) { + fs.platformType = Platform::Type::WinARM; + fs.defines += ";_M_ARM=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.get("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 = compile.get("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 = properties.find("CharacterSet"); + const std::string charSet = (charSetIt != properties.end()) ? charSetIt->second : std::string(); + + const auto useOfMfcIt = properties.find("UseOfMfc"); + fs.useMfc = useOfMfcIt != properties.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; - 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; - } - } + std::string defines = fs.defines; + if (!compile.get("PreprocessorDefinitions").empty()) + defines += (";" + compile.get("PreprocessorDefinitions")); + fsSetDefines(fs, defines); + { + const auto includePathIt = properties.find("IncludePath"); + fsSetIncludePaths(fs, projectDir, toStringList(includePathIt != properties.end() ? includePathIt->second : std::string()), properties); } - } else if (std::strcmp(node->Name(), "ItemDefinitionGroup") == 0) { - ItemDefinitionGroup temp(node, ""); - for (const auto& includePath : toStringList(temp.additionalIncludePaths)) { - if (includePath == "%(AdditionalIncludeDirectories)") - continue; + fs.systemIncludePaths = std::move(fs.includePaths); + fsSetIncludePaths(fs, projectDir, toStringList(compile.get("AdditionalIncludeDirectories")), properties); + fs.forcedIncludes = toStringList(compile.get("ForcedIncludeFiles")); + for (auto &forcedInclude : fs.forcedIncludes) + forcedInclude = toAbsolute(forcedInclude, projectDir, properties); - 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) @@ -1427,15 +3071,15 @@ bool ImportProject::importBcb6Prj(const std::string &projectFilename) predefines += ";__WIN32__=1"; } - // Include paths may contain variables like "$(BCB)\include" or "$(BCB)\include\vcl". + // Include paths may contain properties like "$(BCB)\include" or "$(BCB)\include\vcl". // Those get resolved by ImportProject::FileSettings::setIncludePaths by - // 1. checking the provided variables map ("BCB" => "C:\\Program Files (x86)\\Borland\\CBuilder6") - // 2. checking env variables as a fallback - // Setting env is always possible. Configuring the variables via cli might be an addition. + // 1. checking the provided properties map ("BCB" => "C:\\Program Files (x86)\\Borland\\CBuilder6") + // 2. checking env properties as a fallback + // Setting env is always possible. Configuring the properties via cli might be an addition. // 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; + PropertiesMap properties; const std::string defines = predefines + ";" + sysdefines + ";" + userdefines; const std::string cppDefines = cppPredefines + ";" + defines; const bool forceCppMode = (cflags.find("-P") != cflags.end()); @@ -1453,7 +3097,7 @@ bool ImportProject::importBcb6Prj(const std::string &projectFilename) const bool cppMode = forceCppMode || Path::getFilenameExtensionInLowerCase(c) == ".cpp"; // TODO: needs to set language and ignore later identification and language enforcement FileSettings fs{Path::simplifyPath(Path::isAbsolute(c) ? c : projectDir + c), Standards::Language::None, 0}; // file will be identified later on - fsSetIncludePaths(fs, projectDir, toStringList(includePath), variables); + fsSetIncludePaths(fs, projectDir, toStringList(includePath), properties); fsSetDefines(fs, cppMode ? cppDefines : defines); fileSettings.push_back(std::move(fs)); } @@ -1723,11 +3367,20 @@ void ImportProject::selectOneVsConfig(Platform::Type platform) } const FileSettings &fs = *it; bool remove = false; - if (!startsWith(fs.cfg,"Debug")) + const std::string cfgName = fs.cfg.substr(0, fs.cfg.find('|')); + if (cfgName.size() < 5 || caseInsensitiveStringCompare(cfgName.substr(0, 5), "Debug") != 0) remove = true; - if (platform == Platform::Type::Win64 && fs.platformType != platform) + + if (platform == Platform::Type::Win64 && fs.platformType != Platform::Type::Win64) + remove = true; + else if (platform == Platform::Type::WinARM64 && fs.platformType != Platform::Type::WinARM64) remove = true; - else if ((platform == Platform::Type::Win32A || platform == Platform::Type::Win32W) && fs.platformType == Platform::Type::Win64) + else if (platform == Platform::Type::WinARM && fs.platformType != Platform::Type::WinARM) + remove = true; + else if ((platform == Platform::Type::Win32A || platform == Platform::Type::Win32W) && + (fs.platformType == Platform::Type::Win64 || + fs.platformType == Platform::Type::WinARM64 || + fs.platformType == Platform::Type::WinARM)) remove = true; else if (filenames.find(fs.filename()) != filenames.end()) remove = true; @@ -1752,9 +3405,16 @@ void ImportProject::selectVsConfigurations(Platform::Type platform, const std::v bool remove = false; if (std::find(configurations.begin(), configurations.end(), config) == configurations.end()) remove = true; - if (platform == Platform::Type::Win64 && fs.platformType != platform) + if (platform == Platform::Type::Win64 && fs.platformType != Platform::Type::Win64) + remove = true; + else if (platform == Platform::Type::WinARM64 && fs.platformType != Platform::Type::WinARM64) remove = true; - else if ((platform == Platform::Type::Win32A || platform == Platform::Type::Win32W) && fs.platformType == Platform::Type::Win64) + else if (platform == Platform::Type::WinARM && fs.platformType != Platform::Type::WinARM) + remove = true; + else if ((platform == Platform::Type::Win32A || platform == Platform::Type::Win32W) && + (fs.platformType == Platform::Type::Win64 || + fs.platformType == Platform::Type::WinARM64 || + fs.platformType == Platform::Type::WinARM)) remove = true; if (remove) { it = fileSettings.erase(it); @@ -1780,16 +3440,34 @@ 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); + PropertiesMap properties; + properties["Platform"] = platform; + properties["Configuration"] = configuration; + // Use ConditionParser directly so exceptions propagate to the caller; + // evalCondition swallows them (by design for production use). + return ConditionParser(condition, properties).parse(); +} + +// cppcheck-suppress unusedFunction +std::string cppcheck::testing::expandMSBuildExpression(const std::string& expr) +{ + PropertiesMap properties; + std::string s = expr; + expandMSBuildVariables(s, properties); + return s; } diff --git a/lib/importproject.h b/lib/importproject.h index b8bbbed3fa3..93dc7e11c87 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 @@ -53,14 +55,21 @@ namespace cppcheck { namespace testing { CPPCHECKLIB bool evaluateVcxprojCondition(const std::string& condition, const std::string& configuration, const std::string& platform); + /** Expand MSBuild property expressions ($(Name), $([Class]::Method(args))) in \p expr + * against an empty property map and return the result. Intended for unit tests. */ + CPPCHECKLIB std::string expandMSBuildExpression(const std::string& expr); } } +using PropertiesMap = std::map; +using MetadataMap = std::map; + /** * @brief Importing project settings. */ class CPPCHECKLIB WARN_UNUSED ImportProject { public: + enum class Type : std::uint8_t { NONE, UNKNOWN, @@ -73,14 +82,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, PropertiesMap &properties); public: std::list fileSettings; std::vector errors; + std::vector debugs; ImportProject() = default; virtual ~ImportProject() = default; @@ -106,30 +123,77 @@ 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; - }; - - 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, ARM64, ARM, Unknown } platform = Unknown; + std::string platformStr; + }; + + struct ItemGroupClCompile { + explicit ItemGroupClCompile(std::string filename) : filename(std::move(filename)) {} + std::string filename; + MetadataMap metadata; + const std::string &get(const std::string &key) const { + static const std::string empty; + const auto it = metadata.find(key); + return (it != metadata.end()) ? it->second : empty; + } + }; + + bool importSln(std::istream &istr, const std::string &filename, const std::vector &fileFilters); + bool importSlnx(const std::string& filename, const std::vector& fileFilters); + bool importDirectorySolutionProps(PropertiesMap &properties); + bool importVcxproj(const std::string &filename, PropertiesMap &properties, const std::vector &fileFilters); + + ImportResult importPropsOrTargets(const std::string &file, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &projectConfigurationList, + std::unordered_set &importStack); + ImportResult importVcxitems(const std::string &items, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack); + ImportResult importProject(const tinyxml2::XMLElement *node, + const std::string &projectDir, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &projectConfigurationList, + std::unordered_set &importStack); + ImportResult importCompile(const tinyxml2::XMLElement *node, + const std::string &projectDir, + PropertiesMap &properties, + const MetadataMap &metadata, + std::list &compileList); + void checkUnexpandedExpressions(const std::string &text, const char *context); + bool simplifyPathWithVariables(std::string &s, PropertiesMap &properties); + void addProperty(const tinyxml2::XMLElement *node, PropertiesMap &properties); + void addMetadata(const tinyxml2::XMLElement *node, PropertiesMap &properties, MetadataMap &metadata); + std::string getMetadata(const tinyxml2::XMLElement *node, PropertiesMap &properties, const MetadataMap &metadata, const std::string &original); + std::string toAbsolute(const std::string &filename, const std::string &baseDir, PropertiesMap &properties); + static std::string toAbsolute(const std::string &path); + static void setSolution(const std::string &filename, PropertiesMap &properties); + + std::string mPath; std::set mAllVSConfigs; }; @@ -201,10 +265,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/lib/platform.cpp b/lib/platform.cpp index 0e202b9f49a..3a960404cd7 100644 --- a/lib/platform.cpp +++ b/lib/platform.cpp @@ -97,6 +97,42 @@ bool Platform::set(Type t) char_bit = 8; calculateBitMembers(); return true; + case Type::WinARM64: + type = t; + windows = true; + sizeof_bool = 1; + sizeof_short = 2; + sizeof_int = 4; + sizeof_long = 4; + sizeof_long_long = 8; + sizeof_float = 4; + sizeof_double = 8; + sizeof_long_double = 8; + sizeof_wchar_t = 2; + sizeof_size_t = 8; + sizeof_pointer = 8; + defaultSign = 's'; + char_bit = 8; + calculateBitMembers(); + return true; + case Type::WinARM: + type = t; + windows = true; + sizeof_bool = 1; + sizeof_short = 2; + sizeof_int = 4; + sizeof_long = 4; + sizeof_long_long = 8; + sizeof_float = 4; + sizeof_double = 8; + sizeof_long_double = 8; + sizeof_wchar_t = 2; + sizeof_size_t = 4; + sizeof_pointer = 4; + defaultSign = 's'; + char_bit = 8; + calculateBitMembers(); + return true; case Type::Unix32: type = t; windows = false; @@ -150,6 +186,10 @@ bool Platform::set(const std::string& platformstr, std::string& errstr, const st set(Type::Win32W); else if (platformstr == "win64") set(Type::Win64); + else if (platformstr == "winARM64") + set(Type::WinARM64); + else if (platformstr == "winARM") + set(Type::WinARM); else if (platformstr == "unix32") set(Type::Unix32); else if (platformstr == "unix64") diff --git a/lib/platform.h b/lib/platform.h index 97e7d296fba..cd1601df6bb 100644 --- a/lib/platform.h +++ b/lib/platform.h @@ -140,6 +140,8 @@ class CPPCHECKLIB Platform { Win32A, Win32W, Win64, + WinARM64, + WinARM, Unix32, Unix64, File @@ -188,6 +190,10 @@ class CPPCHECKLIB Platform { return "win32W"; case Type::Win64: return "win64"; + case Type::WinARM64: + return "winARM64"; + case Type::WinARM: + return "winARM"; case Type::Unix32: return "unix32"; case Type::Unix64: 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..84dfa73224a 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); @@ -91,6 +86,7 @@ class TestImportProject : public TestFixture { TEST_CASE(testCollectArgs6); TEST_CASE(testCollectArgs7); TEST_CASE(testVcxprojConditions); + TEST_CASE(testMSBuildStaticFunctions); } void setDefines() const { @@ -112,8 +108,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); + PropertiesMap properties; + TestImporter importer; + importer.fsSetIncludePaths(fs, "abc/def/", in, properties); ASSERT_EQUALS(1U, fs.includePaths.size()); ASSERT_EQUALS("abc/include/", fs.includePaths.front()); } @@ -121,9 +118,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; - variables["SolutionDir"] = "c:/abc/"; - TestImporter::fsSetIncludePaths(fs, "/home/fred", in, variables); + PropertiesMap properties; + properties["SolutionDir"] = "c:/abc/"; + TestImporter importer; + importer.fsSetIncludePaths(fs, "/home/fred", in, properties); ASSERT_EQUALS(1U, fs.includePaths.size()); ASSERT_EQUALS("c:/abc/other/", fs.includePaths.front()); } @@ -131,9 +129,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; - variables["SolutionDir"] = "c:/abc/"; - TestImporter::fsSetIncludePaths(fs, "/home/fred", in, variables); + PropertiesMap properties; + properties["SolutionDir"] = "c:/abc/"; + TestImporter importer; + importer.fsSetIncludePaths(fs, "/home/fred", in, properties); ASSERT_EQUALS(1U, fs.includePaths.size()); ASSERT_EQUALS("c:/abc/other/", fs.includePaths.front()); } @@ -595,59 +594,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 +699,77 @@ 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'", "", "")); + // Version comparison: full 4-part #.#.#.# + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4' == '1.2.3.4'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4' == '1.2.3.5'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.5' > '1.2.3.4'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4' < '1.2.3.5'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'2.0.0.0' > '1.9.9.9'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1.9.9.9' > '2.0.0.0'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4' >= '1.2.3.4'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4' <= '1.2.3.4'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4' > '1.2.3.4'", "", "")); + // Version comparison: more than 4 parts (no truncation) + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4.5' > '1.2.3.4.4'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4.4' > '1.2.3.4.5'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4.0' == '1.2.3.4'", "", "")); + // Version comparison: missing trailing components treated as 0 + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17' == '17.0.0.0'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'17' != '17.0.0.0'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17' >= '17.0.0.0'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17' <= '17.0.0.0'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'17' > '17.0.0.0'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'17' < '17.0.0.0'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17.0' == '17'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17.0' == '17.0.0.0'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17.0.0' == '17'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17.1' > '17'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17.1' > '17.0.0.0'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'16.9' > '17'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1' < '1.0.0.1'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3' < '1.2.3.1'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3' > '1.2.2.9'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'2' > '1.9.9.9'", "", "")); + // 'Current' on LHS + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'Current' > '14'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'Current' >= '18'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'Current' < '14'", "", "")); + // 'Current' on RHS + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'14' < 'Current'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'18' <= 'Current'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'19' < 'Current'", "", "")); + // Static property functions in conditions + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$([MSBuild]::Add(1, 2)) == '3'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$([MSBuild]::EnsureTrailingSlash('foo')) == 'foo/'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$([System.String]::IsNullOrEmpty('')) == 'True'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$([System.Math]::Max(1, 2)) == '2'", "", "")); + // 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 +778,156 @@ 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"); - // invalid expression in => no error. We are ok with that as long as we don't crash - ASSERT(!cppcheck::testing::evaluateVcxprojCondition("' ' && ' '", "", "")); + // 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")); + // C-style && is not a valid MSBuild operator — throws rather than silently returning false + ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("' ' && ' '", "", ""), std::runtime_error, "Invalid condition: '' ' && ' ''"); + // 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'", "", "")); + } + + void testMSBuildStaticFunctions() const { + // --- $([MSBuild]::...) arithmetic --- + ASSERT_EQUALS("3", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Add(1, 2))")); + ASSERT_EQUALS("5", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Subtract(8, 3))")); + ASSERT_EQUALS("12", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Multiply(3, 4))")); + ASSERT_EQUALS("3", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Divide(9, 3))")); + ASSERT_EQUALS("2", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Modulo(5, 3))")); + + // --- $([MSBuild]::...) path helpers --- + ASSERT_EQUALS("foo/", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::EnsureTrailingSlash('foo'))")); + ASSERT_EQUALS("foo/", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::EnsureTrailingSlash('foo/'))")); + // NormalizePath: join segments, normalise separators, resolve . and .. + // Use absolute first segments so results are deterministic (CWD-independent). + ASSERT_EQUALS("/a/b/c", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizePath('/a', 'b', 'c'))")); + ASSERT_EQUALS("/a/b/c", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizePath('/a\\b\\c'))")); + ASSERT_EQUALS("/a/c", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizePath('/a/b/../c'))")); + ASSERT_EQUALS("/a/b/c", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizePath('/a/b/./c'))")); + ASSERT_EQUALS("C:/a/c", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizePath('C:\\a\\b\\..\\c'))")); + ASSERT_EQUALS("C:/a/b/c", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizePath('C:\\a', 'b', 'c'))")); + // NormalizeDirectory: same as NormalizePath but always has a trailing slash + ASSERT_EQUALS("/a/b/c/", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizeDirectory('/a', 'b', 'c'))")); + ASSERT_EQUALS("/a/b/c/", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizeDirectory('/a\\b\\c'))")); + ASSERT_EQUALS("C:/a/b/c/", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizeDirectory('C:\\a', 'b', 'c'))")); + // ValueOrDefault: return first arg when non-empty, else second + ASSERT_EQUALS("x", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::ValueOrDefault('x', 'y'))")); + ASSERT_EQUALS("y", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::ValueOrDefault('', 'y'))")); + // GetCurrentToolsVersion + ASSERT_EQUALS("Current", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::GetCurrentToolsVersion())")); + + // --- $([MSBuild]::...) bitwise --- + ASSERT_EQUALS("2", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::BitwiseAnd(6, 3))")); + ASSERT_EQUALS("7", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::BitwiseOr(5, 3))")); + ASSERT_EQUALS("6", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::BitwiseXor(5, 3))")); + + // --- $([MSBuild]::...) Escape / Unescape --- + ASSERT_EQUALS("%3B", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Escape(';'))")); + ASSERT_EQUALS(";", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Unescape('%3B'))")); + ASSERT_EQUALS("%24", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Escape('$'))")); + ASSERT_EQUALS("$", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Unescape('%24'))")); + + // --- $([System.String]::...) --- + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([System.String]::IsNullOrEmpty(''))")); + ASSERT_EQUALS("False", cppcheck::testing::expandMSBuildExpression("$([System.String]::IsNullOrEmpty('x'))")); + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([System.String]::IsNullOrWhiteSpace(' '))")); + ASSERT_EQUALS("False", cppcheck::testing::expandMSBuildExpression("$([System.String]::IsNullOrWhiteSpace('x'))")); + ASSERT_EQUALS("ab", cppcheck::testing::expandMSBuildExpression("$([System.String]::Concat('a', 'b'))")); + ASSERT_EQUALS("a,b", cppcheck::testing::expandMSBuildExpression("$([System.String]::Join(',', 'a', 'b'))")); + + // --- $([System.Math]::...) --- + ASSERT_EQUALS("10", cppcheck::testing::expandMSBuildExpression("$([System.Math]::Max(5, 10))")); + ASSERT_EQUALS("5", cppcheck::testing::expandMSBuildExpression("$([System.Math]::Min(5, 10))")); + ASSERT_EQUALS("5", cppcheck::testing::expandMSBuildExpression("$([System.Math]::Abs(-5))")); + ASSERT_EQUALS("5", cppcheck::testing::expandMSBuildExpression("$([System.Math]::Abs(5))")); + ASSERT_EQUALS("2", cppcheck::testing::expandMSBuildExpression("$([System.Math]::Floor(2.9))")); + ASSERT_EQUALS("3", cppcheck::testing::expandMSBuildExpression("$([System.Math]::Ceiling(2.1))")); + + // --- $([System.IO.Path]::...) --- + ASSERT_EQUALS("bar.cpp", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::GetFileName('C:/foo/bar.cpp'))")); + ASSERT_EQUALS("bar", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::GetFileNameWithoutExtension('C:/foo/bar.cpp'))")); + ASSERT_EQUALS("C:/foo", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::GetDirectoryName('C:/foo/bar.cpp'))")); + ASSERT_EQUALS(".cpp", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::GetExtension('bar.cpp'))")); + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::IsPathRooted('C:/foo'))")); + ASSERT_EQUALS("False", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::IsPathRooted('foo'))")); + ASSERT_EQUALS("a/b", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::Combine('a', 'b'))")); + + // --- Composite / nesting --- + ASSERT_EQUALS("6", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Add($([MSBuild]::Multiply(2, 2)), 2))")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$([MSBuild]::Add(1, 2)) == '3'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$([System.String]::IsNullOrEmpty('')) == 'True'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$([System.Math]::Max(10, 5)) == '10'", "", "")); } // TODO: test fsParseCommand()