diff --git a/Makefile b/Makefile index a7f3feefba6..84699a0244a 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/tokenize.h lib/tokenlist.h lib/utils.h test/fixture.h test/helpers.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..892601aa35e 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -23,27 +23,45 @@ #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 #include #include +// Directory listing for wildcard support -- see listDirectoryFiles() +// just above processImport(). +#ifndef _WIN32 +#include +#else +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif + #include "xml.h" #include "json.h" + std::string ImportProject::collectArgs(const std::string &cmd, std::vector &args) { args.clear(); @@ -112,78 +130,104 @@ std::string ImportProject::collectArgs(const std::string &cmd, std::vector &args) { + // Accept index by value, return a completion state pair to prevent loop corruption const auto getOptArg = [&args](std::initializer_list optNames, - std::size_t &i) { + std::size_t i) -> std::pair { const auto &arg = args[i]; const auto *const it = std::find_if(optNames.begin(), optNames.end(), - [&arg] (const std::string &optName) { + [&arg](const std::string &optName) { return startsWith(arg, optName); }); if (it == optNames.end()) - return std::string(); + return std::make_pair(std::string(), false); const std::size_t optLen = it->size(); - if (arg.size() == optLen) - return ++i >= args.size() ? std::string() : args[i]; + if (arg.size() == optLen) { + // Space-separated argument format (e.g., /I path) + if (i + 1 >= args.size()) + return std::make_pair(std::string(), false); + return std::make_pair(args[i + 1], true); // true flags lookahead consumption + } - return arg.substr(optLen); + // Glued argument format (e.g., /Ipath) + return std::make_pair(arg.substr(optLen), false); }; std::string defs; for (std::size_t i = 0; i < args.size(); i++) { - std::string optArg; + if (args[i].empty()) + continue; - if (!(optArg = getOptArg({ "-I", "/I" }, i)).empty()) { + std::pair optResult; + + if (!(optResult = getOptArg({ "-I", "/I" }, i)).first.empty()) { if (std::none_of(fs.includePaths.cbegin(), fs.includePaths.cend(), [&](const std::string &path) { - return path == optArg; - })) - fs.includePaths.push_back(std::move(optArg)); + return path == optResult.first; + })) { + fs.includePaths.push_back(std::move(optResult.first)); + } + if (optResult.second) + i++; // Safely advance only if the separate lookahead was consumed continue; } - if (!(optArg = getOptArg({ "-isystem" }, i)).empty()) { - fs.systemIncludePaths.push_back(std::move(optArg)); + if (!(optResult = getOptArg({ "-isystem" }, i)).first.empty()) { + fs.systemIncludePaths.push_back(std::move(optResult.first)); + if (optResult.second) + i++; continue; } - if (!(optArg = getOptArg({ "-include", "/FI", "-FI" }, i)).empty()) { - fs.forcedIncludes.push_back(std::move(optArg)); + if (!(optResult = getOptArg({ "-include", "/FI", "-FI" }, i)).first.empty()) { + fs.forcedIncludes.push_back(std::move(optResult.first)); + if (optResult.second) + i++; continue; } - if (!(optArg = getOptArg({ "-D", "/D" }, i)).empty()) { - defs += optArg + ";"; + if (!(optResult = getOptArg({ "-D", "/D" }, i)).first.empty()) { + defs += optResult.first + ";"; + if (optResult.second) + i++; continue; } - if (!(optArg = getOptArg({ "-U", "/U" }, i)).empty()) { - fs.undefs.insert(std::move(optArg)); + if (!(optResult = getOptArg({ "-U", "/U" }, i)).first.empty()) { + fs.undefs.insert(std::move(optResult.first)); + if (optResult.second) + i++; continue; } - if (!(optArg = getOptArg({ "-std=", "/std:" }, i)).empty()) { - fs.standard = std::move(optArg); + if (!(optResult = getOptArg({ "-std=", "/std:" }, i)).first.empty()) { + fs.standard = std::move(optResult.first); + if (optResult.second) + i++; continue; } - if (!(optArg = getOptArg({ "-f" }, i)).empty()) { - if (optArg == "pic") + if (!(optResult = getOptArg({ "-f" }, i)).first.empty()) { + if (optResult.first == "pic") defs += "__pic__;"; - else if (optArg == "PIC") + else if (optResult.first == "PIC") defs += "__PIC__;"; - else if (optArg == "pie") + else if (optResult.first == "pie") defs += "__pie__;"; - else if (optArg == "PIE") + else if (optResult.first == "PIE") defs += "__PIE__;"; + if (optResult.second) + i++; continue; } - if (!(optArg = getOptArg({ "-m" }, i)).empty()) { - if (optArg == "unicode") + if (!(optResult = getOptArg({ "-m" }, i)).first.empty()) { + if (optResult.first == "unicode") defs += "UNICODE;"; + if (optResult.second) + i++; continue; } } @@ -215,8 +259,38 @@ void ImportProject::ignoreOtherConfigs(const std::string &cfg) } } +// Decode %XX sequences back to their original characters. +static std::string msbuildUnescape(const std::string &s) +{ + std::string result; + result.reserve(s.size()); + 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 hexVal = [](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((hexVal(s[i + 1]) << 4) | hexVal(s[i + 2])); + i += 2; + } else { + result += s[i]; + } + } + return result; +} + void ImportProject::fsSetDefines(FileSettings& fs, std::string defs) { + // Strip %(metadata) tokens at the start of the string (no preceding semicolon). + while (startsWith(defs, "%(")) { + const std::string::size_type pos2 = defs.find(';'); + defs.erase(0, pos2 == std::string::npos ? std::string::npos : pos2 + 1); + } while (defs.find(";%(") != std::string::npos) { const std::string::size_type pos1 = defs.find(";%("); const std::string::size_type pos2 = defs.find(';', pos1+1); @@ -243,703 +317,4267 @@ void ImportProject::fsSetDefines(FileSettings& fs, std::string defs) } if (!eq && !defs.empty()) defs += "=1"; + defs = msbuildUnescape(defs); fs.defines.swap(defs); } -static bool simplifyPathWithVariables(std::string &s, std::map &variables) -{ - std::set expanded; - std::string::size_type start = 0; - while ((start = s.find("$(")) != std::string::npos) { - const std::string::size_type end = s.find(')',start); - if (end == std::string::npos) - break; - const std::string var = s.substr(start+2,end-start-2); - if (expanded.find(var) != expanded.end()) - break; - expanded.insert(var); - auto it1 = utils::as_const(variables).find(var); - // variable was not found within defined variables - if (it1 == variables.end()) { - const char *envValue = std::getenv(var.c_str()); - if (!envValue) { - //! \todo generate a debug/info message about undefined variable - break; - } - variables[var] = std::string(envValue); - it1 = variables.find(var); - } - s.replace(start, end - start + 1, it1->second); +void ImportProject::addDebug(const std::string &msg) { + for (const auto &debug : debugs) { + // cppcheck-suppress useStlAlgorithm + if (debug == msg) + return; } - if (s.find("$(") != std::string::npos) - return false; - s = Path::simplifyPath(std::move(s)); - return true; + debugs.emplace_back(msg); } -void ImportProject::fsSetIncludePaths(FileSettings& fs, const std::string &basepath, const std::list &in, std::map &variables) -{ - std::set found; - // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) - const std::list copyIn(in); - fs.includePaths.clear(); - for (const std::string &ipath : copyIn) { - if (ipath.empty()) - continue; - if (startsWith(ipath,"%(")) - continue; - std::string s(Path::fromNativeSeparators(ipath)); - if (!found.insert(s).second) - continue; - if (s[0] == '/' || (s.size() > 1U && s.compare(1,2,":/") == 0)) { - if (!endsWith(s,'/')) - s += '/'; - fs.includePaths.push_back(std::move(s)); +// Find the ')' that matches the '(' at position parenPos. +// Tracks depth for every '(' and ')', not just '$(' pairs: bare parentheses +// inside static-function argument lists (e.g. Pow(2,3), Format(...)) must +// also open and close a nesting level, otherwise the inner ')' would be +// mistaken for the match of the outer one. +static std::string::size_type findMatchingParen(const std::string &s, std::string::size_type parenPos) { + if (parenPos >= s.size() || s[parenPos] != '(') + return std::string::npos; + + int depth = 0; + char quote = '\0'; + + for (std::string::size_type i = parenPos; i < s.size(); ++i) { + const char c = s[i]; + + if (quote != '\0') { + if (c == quote) { + // A doubled quote represents a literal quote. + if (i + 1 < s.size() && s[i + 1] == quote) { + ++i; + continue; + } + quote = '\0'; + } continue; } - if (endsWith(s,'/')) // this is a temporary hack, simplifyPath can crash if path ends with '/' - s.pop_back(); - - if (s.find("$(") == std::string::npos) { - s = Path::simplifyPath(basepath + s); - } else { - if (!simplifyPathWithVariables(s, variables)) - continue; + if (c == '\'' || c == '"') { + quote = c; + } else if (c == '(') { + ++depth; + } else if (c == ')') { + --depth; + if (depth == 0) + return i; } - if (s.empty()) - continue; - fs.includePaths.push_back(s.back() == '/' ? s : (s + '/')); } + + return std::string::npos; } -ImportProject::Type ImportProject::import(const std::string &filename, Settings *settings, Suppressions *supprs) -{ - std::ifstream fin(filename); - if (!fin.is_open()) - return ImportProject::Type::MISSING; +// 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) { + if (caseInsensitiveStringCompare(method, "ToUpper") == 0) { + if (!args.empty()) + throw std::runtime_error("ToUpper takes no arguments"); + std::transform(value.cbegin(), value.cend(), value.begin(), static_cast(std::toupper)); + return value; + } - mPath = Path::getPathFromFilename(Path::fromNativeSeparators(filename)); - if (!mPath.empty() && !endsWith(mPath,'/')) - mPath += '/'; + if (caseInsensitiveStringCompare(method, "ToLower") == 0) { + if (!args.empty()) + throw std::runtime_error("ToLower takes no arguments"); + std::transform(value.cbegin(), value.cend(), value.begin(), static_cast(std::tolower)); + return value; + } - const std::vector fileFilters = - settings ? settings->fileFilters : std::vector(); + if (caseInsensitiveStringCompare(method, "Contains") == 0) { + if (args.size() != 1) + throw std::runtime_error("Contains requires one argument"); + // .NET String.Contains is case-sensitive by default + return value.find(args[0]) != std::string::npos ? "True" : "False"; + } - if (endsWith(filename, ".json")) { - if (importCompileCommands(fin)) { - setRelativePaths(filename); - return ImportProject::Type::COMPILE_DB; + if (caseInsensitiveStringCompare(method, "StartsWith") == 0) { + if (args.size() != 1) + throw std::runtime_error("StartsWith requires one argument"); + // .NET String.StartsWith is case-sensitive by default + if (args[0].size() > value.size()) + return "False"; + return value.compare(0, args[0].size(), args[0]) == 0 ? "True" : "False"; + } + + if (caseInsensitiveStringCompare(method, "EndsWith") == 0) { + if (args.size() != 1) + throw std::runtime_error("EndsWith requires one argument"); + // .NET String.EndsWith is case-sensitive by default + if (args[0].size() > value.size()) + return "False"; + return value.compare(value.size() - args[0].size(), args[0].size(), args[0]) == 0 ? "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); } - } else if (endsWith(filename, ".sln")) { - if (importSln(fin, mPath, fileFilters)) { - setRelativePaths(filename); - return ImportProject::Type::VS_SLN; + 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); } - } else if (endsWith(filename, ".slnx")) { - if (importSlnx(filename, fileFilters)) { - setRelativePaths(filename); - return ImportProject::Type::VS_SLNX; + std::string chars; + for (const std::string &arg : args) + chars += arg; + const std::size_t first = value.find_first_not_of(chars); + return first == std::string::npos ? "" : value.substr(first); + } + + if (caseInsensitiveStringCompare(method, "TrimEnd") == 0) { + if (args.empty()) { + const std::size_t last = value.find_last_not_of(" \t\r\n"); + return last == std::string::npos ? "" : value.substr(0, last + 1); } - } else if (endsWith(filename, ".vcxproj")) { - std::map variables; - std::vector sharedItemsProjects; - if (importVcxproj(filename, variables, "", fileFilters, sharedItemsProjects)) { - setRelativePaths(filename); - return ImportProject::Type::VS_VCXPROJ; + std::string chars; + for (const std::string &arg : args) + chars += arg; + const std::size_t last = value.find_last_not_of(chars); + return last == std::string::npos ? "" : value.substr(0, last + 1); + } + + if (caseInsensitiveStringCompare(method, "Substring") == 0) { + if (args.size() != 1 && args.size() != 2) + throw std::runtime_error("Substring requires one or two arguments"); + char *end = nullptr; + const long start = std::strtol(args[0].c_str(), &end, 10); + if (end == args[0].c_str() || *end != '\0') + throw std::runtime_error("Invalid Substring start index"); + if (start < 0 || static_cast(start) > value.size()) + throw std::runtime_error("Substring start index out of range"); + const auto index = static_cast(start); + if (args.size() == 1) + return value.substr(index); + end = nullptr; + const long length = std::strtol(args[1].c_str(), &end, 10); + if (end == args[1].c_str() || *end != '\0') + throw std::runtime_error("Invalid Substring length"); + if (length < 0 || static_cast(length) > value.size() - index) + throw std::runtime_error("Substring length out of range"); + return value.substr(index, static_cast(length)); + } + + if (caseInsensitiveStringCompare(method, "Replace") == 0) { + if (args.size() != 2) + throw std::runtime_error("Replace requires two arguments"); + if (args[0].empty()) + throw std::runtime_error("Replace search string cannot be empty"); + std::size_t pos = 0; + while ((pos = value.find(args[0], pos)) != std::string::npos) { + value.replace(pos, args[0].size(), args[1]); + pos += args[1].size(); } - } else if (endsWith(filename, ".bpr")) { - if (importBcb6Prj(filename)) { - setRelativePaths(filename); - return ImportProject::Type::BORLAND; + return value; + } + + if (caseInsensitiveStringCompare(method, "IndexOf") == 0) { + if (args.empty() || args.size() > 2) + throw std::runtime_error("IndexOf requires one or two arguments"); + std::size_t from = 0; + if (args.size() == 2) { + char *end = nullptr; + const long idx = std::strtol(args[1].c_str(), &end, 10); + if (end == args[1].c_str() || *end != '\0') + throw std::runtime_error("Invalid IndexOf start index"); + if (idx < 0 || static_cast(idx) > value.size()) + throw std::runtime_error("IndexOf start index out of range"); + from = static_cast(idx); } - } else if (settings && supprs && endsWith(filename, ".cppcheck")) { - if (importCppcheckGuiProject(fin, *settings, *supprs)) { - setRelativePaths(filename); - return ImportProject::Type::CPPCHECK_GUI; + const std::size_t found = value.find(args[0], from); + return std::to_string(found == std::string::npos ? -1L : static_cast(found)); + } + + if (caseInsensitiveStringCompare(method, "LastIndexOf") == 0) { + if (args.empty() || args.size() > 2) + throw std::runtime_error("LastIndexOf requires one or two arguments"); + std::size_t from = std::string::npos; + if (args.size() == 2) { + char *end = nullptr; + const long idx = std::strtol(args[1].c_str(), &end, 10); + if (end == args[1].c_str() || *end != '\0') + throw std::runtime_error("Invalid LastIndexOf start index"); + if (idx < 0 || static_cast(idx) > value.size()) + throw std::runtime_error("LastIndexOf start index out of range"); + from = static_cast(idx); } - } else { - return ImportProject::Type::UNKNOWN; + const std::size_t found = value.rfind(args[0], from); + return std::to_string(found == std::string::npos ? -1L : static_cast(found)); } - return ImportProject::Type::FAILURE; + + const bool isPadLeft = caseInsensitiveStringCompare(method, "PadLeft") == 0; + if (isPadLeft || caseInsensitiveStringCompare(method, "PadRight") == 0) { + if (args.empty() || args.size() > 2) + throw std::runtime_error(method + " requires one or two arguments"); + char *end = nullptr; + const long totalWidth = std::strtol(args[0].c_str(), &end, 10); + if (end == args[0].c_str() || *end != '\0' || totalWidth < 0) + throw std::runtime_error(method + " totalWidth must be a non-negative integer"); + const char padChar = (args.size() == 2 && !args[1].empty()) ? args[1][0] : ' '; + const auto width = static_cast(totalWidth); + if (value.size() >= width) + return value; + const std::string padding(width - value.size(), padChar); + return isPadLeft ? padding + value : value + padding; + } + + throw std::runtime_error("Unhandled method '" + method + "'"); } -bool ImportProject::importCompileCommands(std::istream &istr) +static std::string findFileAbove(const std::string &startDirectory, const std::string &file) { - picojson::value compileCommands; - istr >> compileCommands; - if (!compileCommands.is()) { - errors.emplace_back("compilation database is not a JSON array"); - return false; + // startDirectory comes from MSBuildThisFileDirectory which is already + // normalized to '/' separators by Path::simplifyPath. + std::string currentDir = startDirectory; + if (currentDir.size() > 1 && currentDir.back() == '/' && 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; + const std::size_t lastSlash = currentDir.rfind('/'); + if (lastSlash == std::string::npos) + break; + currentDir.resize(lastSlash); } - std::map fsFileIds; + return ""; +} - for (const picojson::value &fileInfo : compileCommands.get()) { - picojson::object obj = fileInfo.get(); +/// Five-way Windows/Unix path classification used by pathCombineAppend and isPathRooted. +enum class PathKind : std::uint8_t { + Empty, ///< "" + UNC, ///< \\server\share or //server/share + DriveAbsolute, ///< C:\foo or C:/foo + RootRelative, ///< \foo or /foo (no drive letter) + DriveRelative, ///< C:foo (drive letter, no separator) + Relative, ///< foo (everything else) +}; + +static PathKind classifyPath(const std::string &path) +{ + if (path.empty()) + return PathKind::Empty; + // UNC: two leading separators. + if ((path[0] == '/' || path[0] == '\\') && + path.size() >= 2 && (path[1] == '/' || path[1] == '\\')) + return PathKind::UNC; + // Windows drive letter. + if (path.size() >= 2 && + std::isalpha(static_cast(path[0])) && + path[1] == ':') { + if (path.size() >= 3 && (path[2] == '/' || path[2] == '\\')) + return PathKind::DriveAbsolute; + return PathKind::DriveRelative; + } + // Single leading separator. + if (path[0] == '/' || path[0] == '\\') + return PathKind::RootRelative; + return PathKind::Relative; +} - if (obj.count("directory") == 0) { - errors.emplace_back("'directory' field in compilation database entry missing"); - return false; +static bool isRelative(const std::string &path) { + return classifyPath(path) >= PathKind::DriveRelative; +} + +// Append one path segment to `result` using Windows path-combination semantics. +// classifyPath() is the sole authority for path kind; Path::isAbsolute() is +// intentionally NOT used here because it is host-dependent and would misclassify +// root-relative Windows paths (\foo -> /foo after fromNativeSeparators) as +// fully-absolute on Linux. +// +// Returns true if the segment was fully resolved, false if it could not be +// resolved correctly and `result` was left unchanged. Callers must emit a +// diagnostic on false; they must NOT silently continue with a wrong path. +// +// Two modes, selected by `checkIsAbsolute`: +// +// false -- MSBuild property-evaluation combine (e.g. $(A)\$(B)): +// UNC / DriveAbsolute -> full reset, returns true +// RootRelative \foo -> reset path, inherit drive: "C:" + "\foo" = "C:/foo", returns true +// DriveRelative C:foo -> UNSUPPORTED: resolving "C:foo" requires the per-drive CWD +// for drive C:, which is a Windows kernel concept unavailable +// here. Leaves result unchanged and returns false. +// Relative / Empty -> plain join, returns true +// +// true -- System.IO.Path.Combine semantics: +// UNC / DriveAbsolute -> full reset, returns true +// RootRelative \foo -> full reset (Path.IsPathRooted = true; drive NOT inherited, +// matching .NET Path.Combine behaviour), returns true +// DriveRelative C:foo -> full reset (Path.IsPathRooted = true for "C:foo"), returns true +// Relative / Empty -> plain join, returns true +static bool pathCombineAppend(std::string &result, const std::string &seg, bool checkIsAbsolute = false) { + if (seg.empty()) + return true; + switch (classifyPath(seg)) { + case PathKind::UNC: + case PathKind::DriveAbsolute: + // Full reset in both modes. + result = seg; + return true; + case PathKind::RootRelative: + if (checkIsAbsolute) { + // System.IO.Path.Combine: root-relative is rooted; discard accumulated + // base (including any drive letter) and return the segment as-is. + result = seg; + } else { + // MSBuild property eval: reset the path component but preserve the + // accumulated drive letter so "\foo" on "C:/project/" -> "C:/foo". + if (result.size() >= 2 && std::isalpha(static_cast(result[0])) && result[1] == ':') + result.replace(0, 2, seg); + else + result = seg; } + return true; + case PathKind::DriveRelative: + if (checkIsAbsolute) { + // System.IO.Path.Combine: drive-relative is rooted (Path.IsPathRooted + // returns true for "C:foo"); discard the accumulated base. + result = seg; + return true; + } + // MSBuild property eval: "C:foo" means foo relative to the current directory + // of drive C:, a per-drive CWD that is a Windows kernel concept. We have no + // way to resolve this correctly in a cross-platform context. Leave result + // unchanged and signal the caller to emit a diagnostic. + return false; + case PathKind::Relative: + case PathKind::Empty: + if (!result.empty() && result.back() != '/' && result.back() != '\\') + result += '/'; + result += seg; + return true; + } + return true; // unreachable; silences -Wreturn-type +} - if (!obj["directory"].is()) { - errors.emplace_back("'directory' field in compilation database entry is not a string"); - return false; +// MSBuild special characters that must be percent-encoded in property values. +static const char MSBUILD_SPECIAL_CHARS[] = "%$@';?*!"; + +// Encode every MSBuild special character in `s` as %XX. +static std::string msbuildEscape(const std::string &s) +{ + static const char hex[] = "0123456789ABCDEF"; + std::string result; + result.reserve(s.size()); + for (const unsigned char c : s) { + if (std::strchr(MSBUILD_SPECIAL_CHARS, static_cast(c))) { + result += '%'; + result += hex[c >> 4]; + result += hex[c & 0xF]; + } else { + result += static_cast(c); } + } + return result; +} - std::string dirpath = Path::fromNativeSeparators(obj["directory"].get()); +static std::string stripDirectoryPart(const std::string &filename) { + const auto slash = filename.rfind('/'); + return (slash != std::string::npos) ? filename.substr(slash + 1) : filename; +} - /* CMAKE produces the directory without trailing / so add it if not - * there - it is needed by setIncludePaths() */ - if (!endsWith(dirpath, '/')) - dirpath += '/'; +/// Return the stem of \p filename (basename with its final extension removed). +/// Strips only the tail extension so "foo.targets.props" -> "foo.targets", not "foo". +static std::string fileStem(const std::string &filename) { + const std::string ext = Path::getFilenameExtension(filename); + if (ext.empty() || filename.size() <= ext.size()) + return filename; + return filename.substr(0, filename.size() - ext.size()); +} - const std::string directory = std::move(dirpath); - std::vector arguments; - if (obj.count("arguments")) { - if (obj["arguments"].is()) { - for (const picojson::value& arg : obj["arguments"].get()) { - if (arg.is()) - arguments.push_back(arg.get()); - } - } else { - errors.emplace_back("'arguments' field in compilation database entry is not a JSON array"); - return false; - } - } else if (obj.count("command")) { - std::string command; - if (obj["command"].is()) { - command = obj["command"].get(); - } else { - errors.emplace_back("'command' field in compilation database entry is not a string"); - return false; - } +static bool isPathRooted(const std::string &filename) { + switch (classifyPath(filename)) { + case PathKind::UNC: + case PathKind::DriveAbsolute: + case PathKind::RootRelative: + case PathKind::DriveRelative: // "C:foo" -- Path.IsPathRooted returns true on Windows + return true; + default: + return false; + } +} - std::string error = collectArgs(command, arguments); - if (!error.empty()) { - errors.emplace_back(error); - return false; +static std::string getRelativePath(const std::string &absolutePath, const std::vector &basePaths) { + const std::string normAbs = Path::fromNativeSeparators(absolutePath); + + // Split a forward-slash path into components where the first element is + // the root token -- keeping roots distinct prevents the common-prefix + // algorithm from emitting relative paths that cross root boundaries. + // + // UNC absolute //server/share/a -> ["//server/share", "a"] + // Drive absolute C:/foo/a -> ["C:", "foo", "a"] + // Root relative /foo/a -> ["/", "foo", "a"] + // Relative foo/a -> ["foo", "a"] (no root token) + // + // Cross-root pairs (different drive letters, or UNC paths whose server OR + // share differs) will get common == 0 and be rejected before any ".." are + // emitted, which is correct: no well-formed relative path can cross roots. + const auto split = [](const std::string &s) { + std::vector parts; + std::size_t pos = 0; + if (s.size() >= 2 && s[0] == '/' && s[1] == '/') { + // UNC path: root is the "//server/share" unit. + const std::size_t serverEnd = s.find('/', 2); + if (serverEnd == std::string::npos) { + // Degenerate "//server" with no share -- treat whole string as root. + parts.push_back(s); + return parts; } - } else { - errors.emplace_back("no 'arguments' or 'command' field found in compilation database entry"); - return false; + const std::size_t shareEnd = s.find('/', serverEnd + 1); + if (shareEnd == std::string::npos) { + // "//server/share" with no trailing path. + parts.push_back(s); + return parts; + } + parts.push_back(s.substr(0, shareEnd)); // "//server/share" + pos = shareEnd + 1; + } else if (s.size() >= 2 && std::isalpha(static_cast(s[0])) && s[1] == ':') { + // Drive-letter path: root is the two-character drive token "C:". + parts.push_back(s.substr(0, 2)); + pos = 2; + if (pos < s.size() && s[pos] == '/') + ++pos; + } else if (!s.empty() && s[0] == '/') { + // Root-relative path: root is "/". + parts.emplace_back("/"); + pos = 1; + } + // else: relative path -- no root token is prepended. + + while (pos < s.size()) { + const std::size_t slash = s.find('/', pos); + std::string seg = s.substr(pos, slash == std::string::npos ? std::string::npos : slash - pos); + if (!seg.empty()) + parts.push_back(std::move(seg)); + if (slash == std::string::npos) + break; + pos = slash + 1; } + return parts; + }; - if (!obj.count("file") || !obj["file"].is()) { - errors.emplace_back("skip compilation database entry because it does not have a proper 'file' field"); + for (const std::string &bp : basePaths) { + if (absolutePath == bp || bp.empty()) // Seems to be a file, or path is empty continue; - } - std::string file = Path::fromNativeSeparators(obj["file"].get()); + const std::string normBase = Path::fromNativeSeparators(bp); - // Accept file? - if (!Path::acceptFile(file)) - continue; + // Fast path: absolutePath is directly under basePath -- strip the prefix. + if (normAbs.compare(0, normBase.length(), normBase) == 0) { + if (endsWith(normBase, '/')) + return normAbs.substr(normBase.length()); + if (normAbs.size() > normBase.size() && normAbs[normBase.size()] == '/') + return normAbs.substr(normBase.size() + 1); + } - std::string path; - if (Path::isAbsolute(file)) - path = Path::simplifyPath(std::move(file)); -#ifdef _WIN32 - else if (file[0] == '/' && directory.size() > 2 && std::isalpha(directory[0]) && directory[1] == ':') - // directory: C:\foo\bar - // file: /xy/z.c - // => c:/xy/z.c - path = Path::simplifyPath(directory.substr(0,2) + file); -#endif - else - 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); - // 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]++); - fileSettings.push_back(std::move(fs)); + // Slow path: build a relative path using ".." when absolutePath is above + // or beside basePath but shares a common ancestor. + std::string base = normBase; + if (!base.empty() && base.back() != '/') + base += '/'; + + const std::vector absParts = split(normAbs); + const std::vector baseParts_ = split(base); + + // Find the length of the common component prefix. + std::size_t common = 0; + while (common < absParts.size() && common < baseParts_.size() && + Path::sameFileName(absParts[common], baseParts_[common])) + ++common; + + if (common == 0) + continue; // truly different roots -- skip this basePath + + // One ".." for every extra component in base beyond the common prefix, + // then the remaining components of absolutePath. + std::string rel; + for (std::size_t i = common; i < baseParts_.size(); ++i) { + if (!rel.empty()) + rel += '/'; + rel += ".."; + } + for (std::size_t i = common; i < absParts.size(); ++i) { + if (!rel.empty()) + rel += '/'; + rel += absParts[i]; + } + if (!rel.empty()) + return rel; } - - return true; + // No base path shares a root with absolutePath. Return the normalized form + // so the caller always gets forward slashes, consistent with every other + // path in the property map. + return normAbs; } -bool ImportProject::importSln(std::istream &istr, const std::string &path, const std::vector &fileFilters) -{ - std::string line; +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wmissing-format-attribute" +#pragma clang diagnostic ignored "-Wformat-nonliteral" +#endif - if (!std::getline(istr,line)) { - errors.emplace_back("Visual Studio solution file is empty"); - return false; - } +template +static std::string safeFormat(const char *fmt, Args... args) { + const int needed = std::snprintf(nullptr, 0, fmt, args ...); + if (needed < 0) + return std::string(); + std::vector buf(static_cast(needed) + 1); + std::snprintf(buf.data(), buf.size(), fmt, args ...); + return std::string(buf.data()); +} - if (!startsWith(line, "Microsoft Visual Studio Solution File")) { - // Skip BOM - if (!std::getline(istr, line) || !startsWith(line, "Microsoft Visual Studio Solution File")) { - errors.emplace_back("Visual Studio solution file header not found"); +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +// 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. +std::string ImportProject::applyMSBuildStaticFunction(const std::string &className, + const std::string &member, + const std::vector &args, + const PropertiesMap *properties) { + const auto toInt = [](const std::string &s, long long &out) -> bool { + if (s.empty()) return false; - } - } + char *end = nullptr; + out = std::strtoll(s.c_str(), &end, 10); + return end != s.c_str() && *end == '\0'; + }; - std::map variables; - variables["SolutionDir"] = path; + if (caseInsensitiveStringCompare(className, "MSBuild") == 0) { - bool found = false; - std::vector sharedItemsProjects; - while (std::getline(istr,line)) { - if (!startsWith(line,"Project(")) - continue; - const std::string::size_type pos = line.find(".vcxproj"); - if (pos == std::string::npos) - continue; - const std::string::size_type pos1 = line.rfind('\"',pos); - if (pos1 == std::string::npos) - 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 = Path::fromNativeSeparators(std::move(vcxproj)); - if (!importVcxproj(vcxproj, variables, "", fileFilters, sharedItemsProjects)) { - errors.emplace_back("failed to load '" + vcxproj + "' from Visual Studio solution"); - return false; + // $([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"; } - found = true; - } - if (!found) { - errors.emplace_back("no projects found in Visual Studio solution file"); - return false; + // Arithmetic: Add, Subtract, Multiply, Divide, Modulo + if (args.size() == 2) { + long 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) { + if (b == 0) { + addDebug("MSBuild::Divide: division by zero"); + return std::string(); + } + return std::to_string(a / b); + } + if (caseInsensitiveStringCompare(member, "Modulo") == 0) { + if (b == 0) { + addDebug("MSBuild::Modulo: division by zero"); + return std::string(); + } + 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(basePath, path)) + // Returns path expressed relative to basePath. If the two paths + // share no common root (e.g. different drive letters) path is + // returned unchanged. + if (caseInsensitiveStringCompare(member, "MakeRelative") == 0) + return getRelativePath(args[1], {args[0]}); + // $([MSBuild]::GetDirectoryNameOfFileAbove(startingDirectory, fileName)) + // Walks up from startingDirectory looking for fileName; returns the + // containing directory (no trailing separator) or "" if not found. + if (caseInsensitiveStringCompare(member, "GetDirectoryNameOfFileAbove") == 0) { + const std::string found = findFileAbove(args[0], args[1]); + if (found.empty()) + return ""; + std::string dir = Path::getPathFromFilename(found); + if (!dir.empty() && (dir.back() == '/' || dir.back() == '\\')) + dir.pop_back(); + return dir; + } + // $([MSBuild]::GetPathOfFileAbove(file, startingDirectory)) + // Walks up from startingDirectory looking for file; returns the full + // path of the file or "" if not found. + if (caseInsensitiveStringCompare(member, "GetPathOfFileAbove") == 0) + return findFileAbove(args[1], args[0]); + } + + // $([MSBuild]::GetPathOfFileAbove(file)) -- 1-arg form. MSBuild uses the + // directory of the file being evaluated (MSBuildThisFileDirectory) as the + // start directory. We read it from the supplied properties map when available; + // if absent (properties is null or the key is missing) findFile receives an + // empty start directory and returns "" without crashing. + if (caseInsensitiveStringCompare(member, "GetPathOfFileAbove") == 0 && args.size() == 1) { + std::string startDir; + if (properties) { + const auto it = properties->find("MSBuildThisFileDirectory"); + if (it != properties->end()) + startDir = it->second; + } + return findFileAbove(startDir, args[0]); + } + + // $([MSBuild]::NormalizePath(seg1[, seg2, ...])) -- join segments, normalize + // \ to /, and resolve . and .. components. Internally MSBuild calls + // Path.GetFullPath(Path.Combine(paths)), so multi-segment joining follows + // System.IO.Path.Combine semantics: a rooted segment (UNC, DriveAbsolute, + // RootRelative, or DriveRelative) resets the accumulated path. + if (caseInsensitiveStringCompare(member, "NormalizePath") == 0) { + if (args.empty()) { + addDebug("NormalizePath: called with no arguments"); + return ""; + } + // If any arg still contains an unexpanded $(...) reference, the property was + // not defined at evaluation time. Log it and bail out rather than treating + // the raw reference text as a literal path component. + for (const std::string &a : args) { + // cppcheck-suppress useStlAlgorithm + if (a.find("$(") != std::string::npos) { + addDebug("NormalizePath: arg contains unexpanded property reference: " + a); + return ""; + } + } + std::string result = args[0]; + for (std::size_t i = 1; i < args.size(); ++i) { + // cppcheck-suppress useStlAlgorithm + if (!pathCombineAppend(result, args[i], true)) { + addDebug("NormalizePath: could not combine path segments"); + return ""; + } + } + const PathKind resultKind = classifyPath(result); + if (resultKind == PathKind::DriveRelative) { + addDebug("NormalizePath: drive-relative path cannot be resolved: " + result); + return ""; + } + // Delegate separator normalization and . / .. resolution to the + // central Path utilities so all path handling stays consistent. + return Path::simplifyPath(Path::fromNativeSeparators(result)); + } + + // $([MSBuild]::NormalizeDirectory(seg1[, seg2, ...])) -- same as NormalizePath + // but always returns a path with a trailing slash. + if (caseInsensitiveStringCompare(member, "NormalizeDirectory") == 0) { + if (args.empty()) { + addDebug("NormalizeDirectory: called with no arguments"); + return ""; + } + // 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]; + } + + // $([MSBuild]::VersionGreaterThan / VersionGreaterThanOrEquals / + // VersionLessThan / VersionLessThanOrEquals / + // VersionEquals / VersionNotEquals(v1, v2)) + // Component-by-component numeric comparison; missing trailing components + // are treated as 0 (so '1.0' == '1.0.0'), which differs from the + // condition-expression < / > operators that use -1 for missing parts. + if (args.size() == 2 && + (caseInsensitiveStringCompare(member, "VersionGreaterThan") == 0 || + caseInsensitiveStringCompare(member, "VersionGreaterThanOrEquals") == 0 || + caseInsensitiveStringCompare(member, "VersionLessThan") == 0 || + caseInsensitiveStringCompare(member, "VersionLessThanOrEquals") == 0 || + caseInsensitiveStringCompare(member, "VersionEquals") == 0 || + caseInsensitiveStringCompare(member, "VersionNotEquals") == 0)) { + const auto parseVer = [](const std::string &s) -> std::vector { + std::vector parts; + std::size_t pos = 0; + while (pos <= s.size()) { + char *end = nullptr; + const long v = std::strtol(s.c_str() + pos, &end, 10); + if (end == s.c_str() + pos) + break; + parts.push_back(static_cast(v)); + pos = static_cast(end - s.c_str()); + if (pos < s.size() && s[pos] == '.') + ++pos; + else + break; + } + return parts; + }; + const std::vector lv = parseVer(args[0]); + const std::vector rv = parseVer(args[1]); + const std::size_t count = std::max(lv.size(), rv.size()); + int cmp = 0; + for (std::size_t i = 0; i < count && cmp == 0; ++i) { + const int l = (i < lv.size()) ? lv[i] : 0; + const int r = (i < rv.size()) ? rv[i] : 0; + if (l < r) + cmp = -1; + else if (l > r) + cmp = 1; + } + bool result = false; + if (caseInsensitiveStringCompare(member, "VersionGreaterThan") == 0) + result = cmp > 0; + else if (caseInsensitiveStringCompare(member, "VersionGreaterThanOrEquals") == 0) + result = cmp >= 0; + else if (caseInsensitiveStringCompare(member, "VersionLessThan") == 0) + result = cmp < 0; + else if (caseInsensitiveStringCompare(member, "VersionLessThanOrEquals") == 0) + result = cmp <= 0; + else if (caseInsensitiveStringCompare(member, "VersionEquals") == 0) + result = cmp == 0; + else // VersionNotEquals + result = cmp != 0; + return result ? "True" : "False"; + } + + 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) { + std::string folderName = args[0]; + static constexpr char specialFolderPrefix[] = "SpecialFolder."; + if (folderName.size() > sizeof(specialFolderPrefix) - 1 && + caseInsensitiveStringCompare(folderName.substr(0, sizeof(specialFolderPrefix) - 1), + specialFolderPrefix) == 0) { + folderName.erase(0, sizeof(specialFolderPrefix) - 1); + } + + if (caseInsensitiveStringCompare(folderName, "ProgramFiles") == 0) { + const char *pf = std::getenv("ProgramFiles"); + return pf ? pf : ""; + } + + if (caseInsensitiveStringCompare(folderName, "ProgramFilesX86") == 0) { + const char *pf86 = std::getenv("ProgramFiles(x86)"); + if (pf86) + return pf86; + + // On a 32-bit Windows environment there may be no separate + // ProgramFiles(x86); ProgramFiles is the x86 Program Files directory. + const char *pf = std::getenv("ProgramFiles"); + return pf ? pf : ""; + } + + return ""; + } + } + + if (caseInsensitiveStringCompare(className, "System.IO.Path") == 0) { + if (args.size() == 1) { + const std::string filename = Path::simplifyPath(Path::fromNativeSeparators(args[0])); // FIXME can this be relative (toAbsolute) + if (caseInsensitiveStringCompare(member, "GetFileName") == 0) + return stripDirectoryPart(filename); + if (caseInsensitiveStringCompare(member, "GetFileNameWithoutExtension") == 0) + return fileStem(stripDirectoryPart(filename)); + if (caseInsensitiveStringCompare(member, "GetDirectoryName") == 0) { + std::string path = Path::getPathFromFilename(filename); + if (!path.empty() && path.back() == '/') + path.pop_back(); + return path; + } + if (caseInsensitiveStringCompare(member, "GetExtension") == 0) + return Path::getFilenameExtension(filename); + if (caseInsensitiveStringCompare(member, "IsPathRooted") == 0) + return isPathRooted(filename) ? "True" : "False"; + if (caseInsensitiveStringCompare(member, "GetFullPath") == 0) { + if (classifyPath(filename) == PathKind::DriveRelative) { + addDebug("GetFullPath: drive-relative path cannot be resolved: " + filename); + return ""; + } + return toAbsolute(filename); + } + } + if (!args.empty() && caseInsensitiveStringCompare(member, "Combine") == 0) { + std::string result = args[0]; + for (std::size_t i = 1; i < args.size(); ++i) { + // cppcheck-suppress useStlAlgorithm + if (!pathCombineAppend(result, args[i], true)) { + addDebug("Path.Combine: could not combine path segments"); + return ""; + } + } + return result; + } + if (args.size() == 2) { + if (caseInsensitiveStringCompare(member, "GetFullPath") == 0) { + const std::string path = Path::fromNativeSeparators(args[0]); + const std::string basePath = Path::fromNativeSeparators(args[1]); + // Use classifyPath so that root-relative paths (\foo) are resolved + // against the drive of basePath rather than being misidentified as + // fully-absolute on Linux via Path::isAbsolute. + const PathKind pathKind = classifyPath(path); + const PathKind baseKind = classifyPath(basePath); + // basePath must be a fully-qualified absolute path; root-relative or + // relative bases cannot be used for GetFullPath resolution. + if (baseKind != PathKind::UNC && baseKind != PathKind::DriveAbsolute) + return ""; + switch (pathKind) { + case PathKind::UNC: + case PathKind::DriveAbsolute: + return Path::simplifyPath(path); + case PathKind::RootRelative: { + // "\foo" is rooted but not fully qualified. Resolve it using the + // volume/root represented by basePath. + if (baseKind == PathKind::DriveAbsolute) { + // "\foo" with "C:/base/" -> "C:/foo" + return Path::simplifyPath(basePath.substr(0, 2) + path); + } + + // A UNC base has a UNC root of "//server/share". + if (baseKind == PathKind::UNC) { + const std::size_t firstSlash = basePath.find('/', 2); + if (firstSlash == std::string::npos) + return ""; + + const std::size_t secondSlash = basePath.find('/', firstSlash + 1); + if (secondSlash == std::string::npos) + return Path::simplifyPath(basePath + path); + + return Path::simplifyPath(basePath.substr(0, secondSlash) + path); + } + + return ""; + } + case PathKind::DriveRelative: + // "C:foo" requires the per-drive current directory for drive C:, + // a Windows kernel concept unavailable in a cross-platform context. + addDebug("GetFullPath: drive-relative path cannot be resolved: " + path); + return ""; + default: { + std::string combined = basePath; + if (!combined.empty() && combined.back() != '/') + combined += '/'; + combined += path; + return Path::simplifyPath(combined); + } + } + } + } + } + + 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: replace {n} and {n:spec} placeholders; {{ and }} are literal braces. + if (caseInsensitiveStringCompare(member, "Format") == 0 && !args.empty()) { + // Apply a .NET composite-format specifier to a string value. + const auto applySpec = [this](const std::string &arg, const std::string &spec) -> std::string { + if (spec.empty()) + return arg; + const char specChar = spec[0]; + + int width = -1; + if (spec.size() > 1) { + char *wend = nullptr; + const long w = std::strtol(spec.c_str() + 1, &wend, 10); + if (wend != spec.c_str() + 1 && *wend == '\0' && w >= 0) + width = static_cast(w); + } + + char *iend = nullptr; + const long long lval = std::strtoll(arg.c_str(), &iend, 10); + const bool isInt = !arg.empty() && iend != arg.c_str() && *iend == '\0'; + + char *dend = nullptr; + const double dval = std::strtod(arg.c_str(), &dend); + const bool isDouble = !arg.empty() && dend != arg.c_str() && *dend == '\0'; + + if (specChar == 'D' || specChar == 'd') { + if (!isInt) + return arg; + if (width > 0) { + // Handle negative numbers manually to keep '-' outside the zero padding + if (lval == LLONG_MIN) { + // Prevents -lval overflow: LLONG_MIN magnitude is 19 digits. + const std::string magnitude = "9223372036854775808"; + const int padding = width - static_cast(magnitude.size()); + + if (padding > 0) + return safeFormat("-%s%s", std::string(padding, '0').c_str(), magnitude.c_str()); + + return safeFormat("-%s", magnitude.c_str()); + } + if (lval < 0) + return safeFormat("-%0*lld", width, -lval); + return safeFormat("%0*lld", width, lval); + } + return safeFormat("%lld", lval); + } + + if (specChar == 'X' || specChar == 'x') { + if (!isInt) + return arg; + const auto uval = static_cast(lval); + if (width > 0) { + return specChar == 'X' + ? safeFormat("%0*llX", width, uval) + : safeFormat("%0*llx", width, uval); + } + return specChar == 'X' + ? safeFormat("%llX", uval) + : safeFormat("%llx", uval); + } + + if (specChar == 'F' || specChar == 'f') { + if (!isDouble) + return arg; + const int p = width >= 0 ? width : 2; + return safeFormat("%.*f", p, dval); + } + + if (specChar == 'E' || specChar == 'e') { + if (!isDouble) + return arg; + const int p = width >= 0 ? width : 6; + return specChar == 'E' + ? safeFormat("%.*E", p, dval) + : safeFormat("%.*e", p, dval); + } + + if (specChar == 'G' || specChar == 'g') { + if (!isDouble) + return arg; + // .NET G/g without explicit precision uses 15 significant digits + // for double; C's %g default of 6 is not the same thing. + const int p = width > 0 ? width : 15; + return specChar == 'G' + ? safeFormat("%.*G", p, dval) + : safeFormat("%.*g", p, dval); + } + // Unrecognised specifier -- return the raw argument unchanged and log + // so that MSBuild incompatibilities are visible rather than silent. + this->addDebug("String.Format: unsupported format specifier '" + spec + "'"); + return arg; + }; + + const std::string &fmt = args[0]; + std::string result; + result.reserve(fmt.size()); + for (std::size_t i = 0; i < fmt.size(); ++i) { + if (fmt[i] == '{') { + if (i + 1 < fmt.size() && fmt[i + 1] == '{') { + result += '{'; ++i; + continue; + } + const std::size_t close = fmt.find('}', i + 1); + if (close == std::string::npos) { + result += '{'; + continue; + } + const std::string inner = fmt.substr(i + 1, close - i - 1); + const std::size_t colon = inner.find(':'); + const std::string indexStr = inner.substr(0, colon == std::string::npos ? inner.size() : colon); + const std::string spec = colon != std::string::npos ? inner.substr(colon + 1) : ""; + char *end = nullptr; + const long idx = std::strtol(indexStr.c_str(), &end, 10); + if (end != indexStr.c_str() && *end == '\0' && idx >= 0) { + const std::size_t argIdx = static_cast(idx) + 1; + result += applySpec(argIdx < args.size() ? args[argIdx] : "", spec); + i = close; + } else { + result += '{'; + } + } else if (fmt[i] == '}' && i + 1 < fmt.size() && fmt[i + 1] == '}') { + result += '}'; ++i; + } else { + result += fmt[i]; + } + } + 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 fmtDouble(std::floor(x)); + if (caseInsensitiveStringCompare(member, "Ceiling") == 0) + return fmtDouble(std::ceil(x)); + if (caseInsensitiveStringCompare(member, "Round") == 0) { + // .NET Math.Round defaults to banker's rounding (round-half-to-even), + // not round-half-away-from-zero. + const double fl = std::floor(x); + const double frac = x - fl; + long long rounded; + if (frac < 0.5) + rounded = static_cast(fl); + else if (frac > 0.5) + rounded = static_cast(fl) + 1; + else { // exactly 0.5 -- round to nearest even integer + const auto ifl = static_cast(fl); + rounded = ((ifl % 2) == 0) ? ifl : ifl + 1; + } + return std::to_string(rounded); + } + 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) + return msbuildEscape(args[0]); + if (caseInsensitiveStringCompare(member, "Unescape") == 0) + return msbuildUnescape(args[0]); + // Bitwise operations + { + long 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 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 + + // $([System.IO.FileInfo]::new('path')) / $([System.IO.DirectoryInfo]::new('path')) + // Model as a normalized path string so .FullName / .DirectoryName / .Name chains work. + if ((caseInsensitiveStringCompare(className, "System.IO.FileInfo") == 0 || + caseInsensitiveStringCompare(className, "System.IO.DirectoryInfo") == 0) && + caseInsensitiveStringCompare(member, "new") == 0 && !args.empty()) { + std::string path = Path::fromNativeSeparators(args[0]); + + if (classifyPath(path) != PathKind::UNC && classifyPath(path) != PathKind::DriveAbsolute) + path = Path::fromNativeSeparators(Path::getCurrentPath()) + '/' + path; + + return Path::simplifyPath(std::move(path)); + } + // Unknown class or method -- return empty so import continues + addDebug("unknown class " + className + " or member " + member); + return ""; +} + +// Parse the "[ClassName]::Member" portion of a $([ClassName]::Member(...)) static +// function reference. On entry pos must point at '['; on return pos is advanced +// past the member name. className and member are appended (not assigned) so the +// caller can initialise them to "" before the call. +static void parseMSBuildStaticRef(const std::string &s, std::size_t &pos, + std::string &className, std::string &member) +{ + ++pos; // skip '[' + while (pos < s.size() && s[pos] != ']') + className += s[pos++]; + if (pos < s.size()) // skip ']' + ++pos; + if (pos + 1 < s.size() && s[pos] == ':' && s[pos + 1] == ':') + pos += 2; // skip '::' + while (pos < s.size()) { + const auto c = static_cast(s[pos]); + if (!std::isalnum(c) && c != '_') + break; + member += s[pos++]; + } +} + +// Expands $(Name) and $(Name.Method(args)) references in property value strings. +// Unknown variables are left unexpanded. Use expandPropertyValue() to invoke. +struct ImportProject::PropertyValueExpander { + ImportProject &mProject; + const PropertiesMap &mVars; + std::string mStr; + std::size_t mPos{0}; + bool mUnknownAsEmpty{ false }; + + PropertyValueExpander(ImportProject &project, const PropertiesMap &vars, std::string str, bool unknownAsEmpty = false) + : mProject(project), mVars(vars), mStr(std::move(str)), mUnknownAsEmpty(unknownAsEmpty) {} + + bool hasValue(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 a PROPERTY name, handling nested $(...) within the name. + // MSBuild property names are [A-Za-z_][A-Za-z0-9_-]* -- unlike method + // names (see e.g. the inline scans in tryParseExpr() for '.Method(...)' + // chains, and parseMSBuildStaticRef()'s "[ClassName]::Member" scan, + // neither of which this function is used for), a property name MAY + // contain '-' after the first character, e.g. foo + // referenced as $(My-Property). This is the sole call site that reads + // the identifier directly after '$(' (see tryParseExpr()), so it's safe + // for this one to accept '-' without affecting method-name parsing. + 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++]; + } + return result; + } + + // Parses one method argument: a quoted string literal (with inner $(...) + // expansion) or an unquoted $(...) reference or bare word. + std::string parseArg() { + while (mPos < mStr.size() && std::isspace(static_cast(mStr[mPos]))) + ++mPos; + if (mPos < mStr.size() && mStr[mPos] == '\'') { + ++mPos; + std::string s; + // Expand $(...) references embedded inside the quoted string so that + // e.g. '$(MSBuildThisFileDirectory)' is resolved before being passed + // to NormalizePath / NormalizeDirectory / Path.Combine, etc. + while (mPos < mStr.size() && mStr[mPos] != '\'') { + if (mStr.compare(mPos, 2, "$(") == 0) + s += tryParseExpr(); + else + s += mStr[mPos++]; + } + if (mPos < mStr.size()) // consume closing '\'' + ++mPos; + return s; + } + if (mStr.compare(mPos, 2, "$(") == 0) { + std::string s = tryParseExpr(); + // Append any trailing bare-word text so that unquoted args like + // $(ReactNativeDir)..\..\node_modules become one concatenated arg + // instead of two separate args. + while (mPos < mStr.size() && mStr[mPos] != ',' && mStr[mPos] != ')') + s += mStr[mPos++]; + return s; + } + // Bare word -- consume until ',' or ')'. + std::string s; + while (mPos < mStr.size() && mStr[mPos] != ',' && mStr[mPos] != ')') + s += mStr[mPos++]; + return s; + } + + // Apply a no-parenthesis property access (.Length, .FullName, .DirectoryName, .Name) + // to `value`. `context` is appended to the "unhandled" debug message. + void applyNoParenProperty(std::string &value, const std::string &method, const std::string &context) { + if (caseInsensitiveStringCompare(method, "Length") == 0) + value = std::to_string(value.size()); + else if (caseInsensitiveStringCompare(method, "FullName") == 0) + value = Path::simplifyPath(value); + else if (caseInsensitiveStringCompare(method, "DirectoryName") == 0) + value = Path::getPathFromFilename(value); + else if (caseInsensitiveStringCompare(method, "Name") == 0) + value = stripDirectoryPart(Path::fromNativeSeparators(value)); + else + mProject.addDebug("unhandled property access '." + method + "'" + context); + } + + // Parse a ')'-terminated comma-separated arg list starting at mPos (which + // must already point past the opening '('). Advances mPos past the closing ')'. + std::vector parseArgList() { + 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()) // skip ')' + ++mPos; + return args; + } + + // Parses and evaluates $(Name[.Method(args)...]) starting at mPos. + // Also handles $([ClassName]::Method(args)) static property functions. + // Unknown properties expand to the empty string, except for explicitly + // recognized Visual Studio/MSBuild infrastructure properties which remain + // symbolic for importer/emulation detection. + std::string tryParseExpr() { + const std::size_t start = mPos; + mPos += 2; // skip "$(" + + // $([ClassName]::Method(args)) -- static property function + if (mPos < mStr.size() && mStr[mPos] == '[') { + std::string className, member; + parseMSBuildStaticRef(mStr, mPos, className, member); + std::vector args; + // Skip optional whitespace between method name and '(' -- legal in MSBuild. + while (mPos < mStr.size() && std::isspace(static_cast(mStr[mPos]))) + ++mPos; + if (mPos < mStr.size() && mStr[mPos] == '(') { + ++mPos; // skip '(' + args = parseArgList(); + } + std::string value = mProject.applyMSBuildStaticFunction(className, member, args, &mVars); + // Handle optional .Property or .Method(args) chain on the result, + // e.g. $([System.IO.FileInfo]::new('path').DirectoryName). + while (mPos < mStr.size() && mStr[mPos] == '.') { + ++mPos; + std::string chainMethod; + while (mPos < mStr.size()) { + const auto c = static_cast(mStr[mPos]); + if (!std::isalnum(c) && c != '_') + break; + chainMethod += mStr[mPos++]; + } + if (mPos >= mStr.size() || mStr[mPos] != '(') { + applyNoParenProperty(value, chainMethod, " after static function"); + continue; + } + ++mPos; // skip '(' + std::vector chainArgs = parseArgList(); + try { + value = applyPropertyMethod(value, chainMethod, chainArgs); + } catch (const std::exception &e) { + mProject.addDebug(std::string("applyPropertyMethod (chained): ") + e.what()); + } catch (...) { + mProject.addDebug("applyPropertyMethod (chained): unknown error for method '" + chainMethod + "'"); + } + } + if (mPos < mStr.size() && mStr[mPos] == ')') // skip outer ')' + ++mPos; + return value; + } + + const std::string name = parseIdentifier(); + if (name.empty() || !hasValue(name)) { + const std::size_t end = findMatchingParen(mStr, start + 2); + mPos = (end != std::string::npos) ? end + 1 : mStr.size(); + return mUnknownAsEmpty ? std::string() : mStr.substr(start, mPos - start); + } + + 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] != '(') { + // Property access without parentheses (e.g. $(Foo.Length)). + applyNoParenProperty(value, method, " on '" + name + "'"); + continue; + } + ++mPos; // skip '(' + std::vector args = parseArgList(); + try { + value = applyPropertyMethod(value, method, args); + } catch (const std::exception &e) { + mProject.addDebug(std::string("applyPropertyMethod: ") + e.what()); + } catch (...) { + mProject.addDebug("applyPropertyMethod: unknown error for method '" + method + "'"); + } + } + if (mPos < mStr.size() && mStr[mPos] == ')') // skip closing ')' + ++mPos; + return value; + } + + // Expand all property expressions in mStr in a single left-to-right pass. + // Property values are already evaluated when assigned; do not re-evaluate + // newly produced $(...) references in later passes. + std::string expand() { + 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++]; + } + return result; + } +}; + +void ImportProject::expandMSBuildVariables(std::string &s, const PropertiesMap &properties) +{ + PropertyValueExpander expander{*this, properties, s}; + s = expander.expand(); +} + +ImportProject::Type ImportProject::import(const std::string &filename, Settings *settings, Suppressions *supprs) +{ + std::ifstream fin(filename); + if (!fin.is_open()) + return ImportProject::Type::MISSING; + + mPath = Path::getPathFromFilename(Path::fromNativeSeparators(filename)); + if (!mPath.empty() && !endsWith(mPath,'/')) + mPath += '/'; + + const std::vector fileFilters = + settings ? settings->fileFilters : std::vector(); + + if (endsWith(filename, ".json")) { + if (processCompileCommands(fin)) { + setRelativePaths(filename); + return ImportProject::Type::COMPILE_DB; + } + } else if (endsWith(filename, ".sln")) { + if (importSln(fin, filename, fileFilters)) { + setRelativePaths(filename); + return ImportProject::Type::VS_SLN; + } + } else if (endsWith(filename, ".slnx")) { + if (importSlnx(filename, fileFilters)) { + setRelativePaths(filename); + return ImportProject::Type::VS_SLNX; + } + } else if (endsWith(filename, ".vcxproj")) { + PropertiesMap mVariables; + if (importVcxproj(toAbsolute(filename), mVariables, fileFilters)) { + setRelativePaths(filename); + return ImportProject::Type::VS_VCXPROJ; + } + } else if (endsWith(filename, ".bpr")) { + if (importBcb6Prj(filename)) { + setRelativePaths(filename); + return ImportProject::Type::BORLAND; + } + } else if (settings && supprs && endsWith(filename, ".cppcheck")) { + if (importCppcheckGuiProject(fin, *settings, *supprs)) { + setRelativePaths(filename); + return ImportProject::Type::CPPCHECK_GUI; + } + } else { + return ImportProject::Type::UNKNOWN; + } + return ImportProject::Type::FAILURE; +} + +bool ImportProject::processCompileCommands(std::istream &istr) +{ + picojson::value compileCommands; + istr >> compileCommands; + if (!compileCommands.is()) { + errors.emplace_back("compilation database is not a JSON array"); + return false; + } + + std::map fsFileIds; + + for (const picojson::value &fileInfo : compileCommands.get()) { + picojson::object obj = fileInfo.get(); + + if (obj.count("directory") == 0) { + errors.emplace_back("'directory' field in compilation database entry missing"); + return false; + } + + if (!obj["directory"].is()) { + errors.emplace_back("'directory' field in compilation database entry is not a string"); + return false; + } + + std::string dirpath = Path::fromNativeSeparators(obj["directory"].get()); + + /* CMAKE produces the directory without trailing / so add it if not + * there - it is needed by setIncludePaths() */ + if (!endsWith(dirpath, '/')) + dirpath += '/'; + + const std::string directory = std::move(dirpath); + + std::vector arguments; + if (obj.count("arguments")) { + if (obj["arguments"].is()) { + for (const picojson::value& arg : obj["arguments"].get()) { + if (arg.is()) + arguments.push_back(arg.get()); + } + } else { + errors.emplace_back("'arguments' field in compilation database entry is not a JSON array"); + return false; + } + } else if (obj.count("command")) { + std::string command; + if (obj["command"].is()) { + command = obj["command"].get(); + } else { + errors.emplace_back("'command' field in compilation database entry is not a string"); + return false; + } + + std::string error = collectArgs(command, arguments); + if (!error.empty()) { + errors.emplace_back(error); + return false; + } + } else { + errors.emplace_back("no 'arguments' or 'command' field found in compilation database entry"); + return false; + } + + if (!obj.count("file") || !obj["file"].is()) { + errors.emplace_back("skip compilation database entry because it does not have a proper 'file' field"); + continue; + } + + std::string file = Path::fromNativeSeparators(obj["file"].get()); + + // Accept file? + if (!Path::acceptFile(file)) + continue; + + std::string path; + if (Path::isAbsolute(file)) + path = Path::simplifyPath(std::move(file)); +#ifdef _WIN32 + else if (file[0] == '/' && directory.size() > 2 && std::isalpha(directory[0]) && directory[1] == ':') + // directory: C:\foo\bar + // file: /xy/z.c + // => c:/xy/z.c + path = Path::simplifyPath(directory.substr(0,2) + file); +#endif + else + path = Path::simplifyPath(directory + file); + FileSettings fs{path, Standards::Language::None, 0}; // file will be identified later on + parseArgs(fs, arguments); + 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]++); + fileSettings.push_back(std::move(fs)); + } + + return true; +} + +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; + properties["SolutionFileName"] = stripDirectoryPart(absolutePath); + properties["SolutionName"] = fileStem(properties["SolutionFileName"]); +} + +bool ImportProject::importSln(std::istream &istr, const std::string &filename, const std::vector &fileFilters) +{ + std::string line; + + debugs.clear(); + + // Strip trailing \r so that CRLF .sln files opened in text mode on Linux/macOS + // do not leave a trailing \r on every extracted value. + const auto stripCR = [](std::string &s) { + if (!s.empty() && s.back() == '\r') + s.pop_back(); + }; + + if (!std::getline(istr,line)) { + errors.emplace_back("Visual Studio solution file is empty"); + return false; + } + stripCR(line); + + // Strip UTF-8 BOM (\xEF\xBB\xBF) when present. + if (line.size() >= 3 && + static_cast(line[0]) == 0xEF && + static_cast(line[1]) == 0xBB && + static_cast(line[2]) == 0xBF) + line.erase(0, 3); + + // Visual Studio writes a blank line before the header (also handles a BOM-only line). + if (line.empty()) { + if (!std::getline(istr, line)) { + errors.emplace_back("Visual Studio solution file header not found"); + return false; + } + stripCR(line); + } + + if (!startsWith(line, "Microsoft Visual Studio Solution File")) { + errors.emplace_back("Visual Studio solution file header not found"); + return false; + } + + PropertiesMap solutionVariables; + setSolution(filename, solutionVariables); + + solutionVariables["VisualStudioVersion"] = "17.0"; + + const std::string solutionDir = solutionVariables["SolutionDir"]; + + // First pass: parse the solution file to collect the header properties and the list of vcxproj paths. + std::vector vcxprojs; + while (std::getline(istr,line)) { + stripCR(line); + if (startsWith(line, "VisualStudioVersion = ")) { + solutionVariables["VisualStudioVersion"] = line.substr(std::strlen("VisualStudioVersion = ")); + continue; + } + if (startsWith(line, "MinimumVisualStudioVersion = ")) { + solutionVariables["MinimumVisualStudioVersion"] = line.substr(std::strlen("MinimumVisualStudioVersion = ")); + continue; + } + if (!startsWith(line,"Project(")) + continue; + // Search for .vcxproj only in the path field (third quoted token), not in + // the project display name which precedes it. SLN line format is: + // Project("{TypeGUID}") = "DisplayName", "RelativePath", "{ProjGUID}" + // The separator between display name and path is the first ", " after ") = ". + const std::string::size_type eqMark = line.find(") = \""); + const std::string::size_type innerFind = (eqMark != std::string::npos) + ? line.find("\", \"", eqMark) + : std::string::npos; + const std::string::size_type searchStart = (innerFind != std::string::npos) ? innerFind + 3 : 0; + // searchStart points to the opening quote of the path field; extract it + // and check the extension properly rather than searching for a substring. + if (searchStart >= line.size() || line[searchStart] != '\"') + continue; + const std::string::size_type pathEnd = line.find('\"', searchStart + 1); + if (pathEnd == std::string::npos) + continue; + std::string vcxproj = line.substr(searchStart + 1, pathEnd - searchStart - 1); + if (Path::getFilenameExtensionInLowerCase(vcxproj) != ".vcxproj") + continue; + vcxproj = Path::toNativeSeparators(std::move(vcxproj)); + vcxproj = toAbsolute(vcxproj, solutionDir, solutionVariables); + vcxproj = Path::fromNativeSeparators(std::move(vcxproj)); + vcxprojs.push_back(std::move(vcxproj)); + } + + if (vcxprojs.empty()) { + errors.emplace_back("no projects found in Visual Studio solution file"); + return false; + } + + for (const std::string &vcxproj : vcxprojs) { + PropertiesMap mVariables = solutionVariables; + if (!importVcxproj(vcxproj, mVariables, fileFilters)) { + errors.emplace_back("failed to load '" + vcxproj + "' from Visual Studio solution"); + return false; + } + } + + return true; +} + +bool ImportProject::importSlnx(const std::string& filename, const std::vector& fileFilters) +{ + debugs.clear(); + + tinyxml2::XMLDocument doc; + const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); + if (error != tinyxml2::XML_SUCCESS) { + errors.emplace_back(std::string("Visual Studio solution file is not a valid XML - ") + tinyxml2::XMLDocument::ErrorIDToName(error)); + return false; + } + + const tinyxml2::XMLElement* const rootnode = doc.FirstChildElement(); + if (rootnode == nullptr) { + errors.emplace_back("Visual Studio solution file has no XML root node"); + return false; + } + + if (std::strcmp(rootnode->Name(), "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)); + + PropertiesMap 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 true; +} + +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 == "ARM64EC") + platform = ARM64EC; + 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 = findMatchingParen(text, pos + 1); + if (end == std::string::npos) + break; + const std::string propName = text.substr(pos + 2, end - pos - 2); + std::stringstream message; + message << "unexpanded property $(" + << propName + << ")" + << (context ? " in " : "") + << (context ? context : "") + << ": " << text; + addDebug(message.str()); + pos = end + 1; + } + pos = 0; + while ((pos = text.find("%(", pos)) != std::string::npos) { + const std::string::size_type end = findMatchingParen(text, pos + 1); + if (end == std::string::npos) + break; + std::stringstream message; + message << "unexpanded metadata %(" + << text.substr(pos + 2, end - pos - 2) + << ")" + << (context ? " in " : "") + << (context ? context : "") + << ": " << text; + addDebug(message.str()); + pos = end + 1; + } +} + +/** MSBuild version number: one or more dot-separated non-negative integers. + * + * Comparison follows .NET System.Version semantics: missing trailing + * components are treated as -1 rather than 0, so "17" < "17.0" (because + * new Version("17").Minor == -1 < 0 == new Version("17.0").Minor). + * + * Use MSBuildVersion::parse() to construct from a string. An empty + * (default-constructed or failed-parse) instance compares as invalid; + * callers should check empty() before comparing. + */ +class MSBuildVersion { +public: + MSBuildVersion() = default; + + /** Parse a version string ("17.0", "16.11.2", "v14.0", "17.0.0-preview.1"). + * Rules applied in order: + * 1. Strip a single leading 'v' or 'V'. + * 2. Truncate at the first '-' or '+' (semver pre-release / build-metadata). + * 3. Split the remainder on '.' and parse each segment as a non-negative integer. + * 4. Reject any component that is empty, contains whitespace, or is not a + * valid decimal integer (strtol would silently accept leading whitespace + * so we check explicitly). + * Returns an empty (invalid) instance on any parse failure. */ + static MSBuildVersion parse(const std::string &s) { + if (s.empty()) + return MSBuildVersion(); + + // Rule 1: optional leading 'v'/'V'. + std::size_t pos = (s[0] == 'v' || s[0] == 'V') ? 1 : 0; + if (pos == s.size()) + return MSBuildVersion(); + + // Rule 2: stop at the first pre-release or build-metadata separator. + const std::size_t sep = s.find_first_of("-+", pos); + const std::size_t limit = (sep == std::string::npos) ? s.size() : sep; + + MSBuildVersion ver; + while (pos < limit) { + const std::size_t dot = s.find('.', pos); + const std::size_t end = (dot == std::string::npos || dot > limit) ? limit : dot; + + // Rule 4a: reject empty components (e.g. "17..0" or trailing dot). + if (end == pos) + return MSBuildVersion(); + + // Rule 4b: reject leading whitespace -- strtol silently skips it. + if (std::isspace(static_cast(s[pos]))) + return MSBuildVersion(); + + // Rule 3: parse the numeric component. + const std::string part = s.substr(pos, end - pos); + char *endPtr = nullptr; + const long value = std::strtol(part.c_str(), &endPtr, 10); + + // Rule 4c: all characters must have been consumed and the value non-negative. + if (endPtr == part.c_str() || *endPtr != '\0' || value < 0) + return MSBuildVersion(); + + ver.mComponents.push_back(static_cast(value)); + + if (dot == std::string::npos || dot >= limit) + break; + pos = dot + 1; + } + + if (ver.mComponents.empty()) + return MSBuildVersion(); + + return ver; + } + + /** True when this instance could not be parsed or was default-constructed. */ + bool empty() const { + return mComponents.empty(); + } + + /** Return component \p i, or -1 if the version has fewer than i+1 components + * (.NET semantics: Major/Minor/Build/Revision default to -1). */ + int component(std::size_t i) const { + return (i < mComponents.size()) ? mComponents[i] : -1; + } + + bool operator==(const MSBuildVersion &rhs) const { + return cmp(rhs) == 0; + } + bool operator!=(const MSBuildVersion &rhs) const { + return cmp(rhs) != 0; + } + bool operator< (const MSBuildVersion &rhs) const { + return cmp(rhs) < 0; + } + bool operator> (const MSBuildVersion &rhs) const { + return cmp(rhs) > 0; + } + bool operator<=(const MSBuildVersion &rhs) const { + return cmp(rhs) <= 0; + } + bool operator>=(const MSBuildVersion &rhs) const { + return cmp(rhs) >= 0; + } + + // Apply an MSBuild comparison operator string. + bool compareOp(const std::string &op, const MSBuildVersion &rhs) const { + if (op == "==" || op == "!=") { + const std::size_t count = std::max(mComponents.size(), rhs.mComponents.size()); + + // For equality, MSBuild treats omitted trailing version components + // as zero: 1 == 1.0 == 1.0.0. + bool equal = true; + for (std::size_t i = 0; i < count; ++i) { + const int lhsComponent = i < mComponents.size() ? mComponents[i] : 0; + const int rhsComponent = i < rhs.mComponents.size() ? rhs.mComponents[i] : 0; + if (lhsComponent != rhsComponent) { + equal = false; + break; + } + } + + return op == "==" ? equal : !equal; + } + + if (op == "<") + return *this < rhs; + if (op == ">") + return *this > rhs; + if (op == "<=") + return *this <= rhs; + if (op == ">=") + return *this >= rhs; + + return false; + } + + std::string toString() const { + std::string s; + for (std::size_t i = 0; i < mComponents.size(); ++i) { + if (i > 0) + s += '.'; + s += std::to_string(mComponents[i]); + } + return s; + } + +private: + std::vector mComponents; + + int cmp(const MSBuildVersion &rhs) const { + const std::size_t count = std::max(mComponents.size(), rhs.mComponents.size()); + for (std::size_t i = 0; i < count; ++i) { + const int l = component(i); + const int r = rhs.component(i); + if (l < r) + return -1; + if (l > r) + return 1; + } + return 0; + } +}; + +// see https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-conditions +class ImportProject::ConditionParser { +public: + ConditionParser(ImportProject &project, const std::string &condition, const PropertiesMap &properties) + : mProject(project), 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: + ImportProject &mProject; + 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; + for (std::size_t i = 0; i < word.size(); ++i) { + if (std::tolower(static_cast(mCondition[mPos + i])) != + std::tolower(static_cast(word[i]))) + 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("!") || matchWord("not")) { + 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")) + 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"; + } + } + + // No operator -- normalize bare boolean-like values (e.g. from property + // expansion: $(EnableUnityBuild) == "true") so the rest of the parser, + // which uses exact "True"/"False" comparisons, sees a canonical form. + if (caseInsensitiveStringCompare(lhs, "true") == 0) + return "True"; + if (caseInsensitiveStringCompare(lhs, "false") == 0) + return "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++; + + // Skip leading '-' when checking for hex prefix. + const std::size_t digitStart = mPos; + // Hex literal: ([-]?)0x or ([-]?)0X + if (mCondition[digitStart] == '0' && + digitStart + 1 < mCondition.size() && + (mCondition[digitStart + 1] == 'x' || mCondition[digitStart + 1] == 'X') && + digitStart + 2 < mCondition.size() && + std::isxdigit(static_cast(mCondition[digitStart + 2]))) { + mPos = digitStart + 2; // skip '0x'/'0X' + while (mPos < mCondition.size() && std::isxdigit(static_cast(mCondition[mPos]))) + ++mPos; + } else { + 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'; + } + + // Parses a METHOD or chained-member name (e.g. the "Method" in + // $(Foo.Method(...)) or the "Member" in $([Class]::Member.Method(...))). + // MSBuild method/member names are [A-Za-z_][A-Za-z0-9_]* -- '-' is not + // valid here. See parsePropertyName() below for the different rule that + // applies to a PROPERTY name instead; do not use this function for one. + std::string parseMethodName() { + 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 != '_') + break; + result += mCondition[mPos++]; + } + if (result.empty()) + throw std::runtime_error("Expected identifier in condition '" + mCondition + "'"); + return result; + } + + // Parses a PROPERTY name -- the "Name" in $(Name) or $(Name.Method(...)). + // MSBuild property names are [A-Za-z_][A-Za-z0-9_-]*: unlike a method or + // member name (parseMethodName() above), '-' IS valid here after the + // first character, e.g. foo referenced as + // $(My-Property). Use this only for the property-name position, never + // for a method/member name -- see parseMethodName()'s own doc comment. + std::string parsePropertyName() { + 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] == '[') { + std::string className, member; + parseMSBuildStaticRef(mCondition, mPos, className, member); + std::vector args; + skipWhitespace(); // allow space between method name and '(' + if (mPos < mCondition.size() && mCondition[mPos] == '(') { + ++mPos; // skip '(' + skipWhitespace(); + if (!match(")")) { + do { + args.push_back(parseValue()); + skipWhitespace(); + } while (match(",")); + expect(")"); + } + } + std::string value = mEvaluate ? mProject.applyMSBuildStaticFunction(className, member, args, &mVariables) : std::string(); + // Optional .Property / .Method(args) chain on the static-function result. + while (true) { + skipWhitespace(); + if (!match(".")) + break; + const std::string chainMethod = parseMethodName(); + if (!match("(")) { + if (mEvaluate) { + if (caseInsensitiveStringCompare(chainMethod, "Length") == 0) + value = std::to_string(value.size()); + else if (caseInsensitiveStringCompare(chainMethod, "FullName") == 0) + value = Path::simplifyPath(value); + else if (caseInsensitiveStringCompare(chainMethod, "DirectoryName") == 0) + value = Path::getPathFromFilename(value); + else if (caseInsensitiveStringCompare(chainMethod, "Name") == 0) + value = stripDirectoryPart(Path::fromNativeSeparators(value)); + else + mProject.addDebug("unhandled property access '." + chainMethod + "' after static function in condition"); + } + continue; + } + std::vector chainArgs; + skipWhitespace(); + if (!match(")")) { + do { + chainArgs.push_back(parseValue()); + } while (match(",")); + expect(")"); + } + value = mEvaluate ? applyPropertyMethod(std::move(value), chainMethod, chainArgs) : std::string(); + } + expect(")"); // outer closing paren of $(...) + return value; + } + + std::string value = getPropertyValue(parsePropertyName()); + + while (true) { + skipWhitespace(); + if (!match(".")) + break; + + const std::string method = parseMethodName(); + if (!match("(")) { + // Property access without parentheses (e.g. $(Foo.Length), $(Foo.Name)). + if (mEvaluate) { + if (caseInsensitiveStringCompare(method, "Length") == 0) + value = std::to_string(value.size()); + else if (caseInsensitiveStringCompare(method, "FullName") == 0) + value = Path::simplifyPath(value); + else if (caseInsensitiveStringCompare(method, "DirectoryName") == 0) + value = Path::getPathFromFilename(value); + else if (caseInsensitiveStringCompare(method, "Name") == 0) + value = stripDirectoryPart(Path::fromNativeSeparators(value)); + else + mProject.addDebug("unhandled property access '." + method + "' in condition"); + } + continue; + } + std::vector args; + skipWhitespace(); + if (!match(")")) { + do { + args.push_back(parseValue()); + } while (match(",")); + expect(")"); + } + value = mEvaluate ? applyPropertyMethod(std::move(value), method, args) : std::string(); + } + + expect(")"); + return value; + } + + std::string parseExists() { + expect("("); + std::string path = parseValue(); + expect(")"); + + // Apply the same normalization used by toAbsolute() / processImport(): + // 1. Normalize native separators so classifyPath() and rfind('/') work. + path = Path::fromNativeSeparators(std::move(path)); + // 2. If any $(Property) survived expansion (unknown property), we cannot + // resolve the path -- return "False" rather than querying the filesystem + // with a literal "$(...)" in the name. + if (path.find("$(") != std::string::npos) + return "False"; + // 3. Resolve non-absolute paths against the file containing this condition. + // Use classifyPath so that root-relative paths (\foo -> /foo after separator + // normalization) are not misidentified as absolute on Linux. + { + const PathKind pk = classifyPath(path); + if (pk != PathKind::UNC && pk != PathKind::DriveAbsolute) { + auto it = mVariables.find("MSBuildThisFileDirectory"); + if (it == mVariables.end()) + it = mVariables.find("ProjectDir"); + if (it != mVariables.end()) + path = it->second + path; + } + } + // 4. Canonicalize: collapse . / .. and remove double slashes. + path = Path::simplifyPath(std::move(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 { + // Conditions use the same single-pass property expansion semantics. + PropertyValueExpander expander{mProject, mVariables, input, true}; + return expander.expand(); + } + + template + static bool applyComparison(const T &lhsValue, const T &rhsValue, const std::string &op) { + if (op == "==") + return lhsValue == rhsValue; + if (op == "!=") + return lhsValue != rhsValue; + if (op == "<") + return lhsValue < rhsValue; + if (op == ">") + return lhsValue > rhsValue; + if (op == "<=") + return lhsValue <= rhsValue; + if (op == ">=") + return lhsValue >= rhsValue; + throw std::runtime_error("Unsupported operation: " + op); + } + + bool compare(const std::string &lhs, const std::string &op, const std::string &rhs) const + { + // Real MSBuild documents <, >, <=, >= as usable only with numeric + // values -- a dotted, multi-component string like '17.9.34607.119' + // isn't a valid number, which is precisely why the dedicated + // $([MSBuild]::VersionGreaterThan(...)) family of functions exists. + // This emulation additionally accepts such version strings directly + // with these four RELATIONAL operators (matching how real vcxproj + // imports compare things like $(VCToolsVersion)), via + // MSBuildVersion::compareOp() below and the "Current" keyword + // handling right after this comment. + // + // == and != are different: MSBuild documents them as usable "with + // strings, or with numeric values" -- ordinary equality/inequality, + // numeric if both sides parse as plain numbers (see the + // parseInteger() case below -- unlike <,>,<=,>=, this uniform + // "numeric-if-possible" rule for ==/!= IS real, documented MSBuild + // behavior, e.g. '$(X)' == '01' matching '$(X)' == '1'), otherwise + // case-insensitive string comparison. That is NOT the same thing as + // the zero-padded, component-wise equality + // $([MSBuild]::VersionEquals(...)) implements (and that + // MSBuildVersion::compareOp()'s "==" branch below deliberately + // reproduces, for that function's use): applying THAT to a plain == + // would make e.g. '1.1' == '1.1.0' true, which is wrong -- that + // zero-padding is VersionEquals()'s own documented behavior, not + // =='s. So the version-comparison paths below (both the "Current" + // keyword and the general dotted-string one) are gated to the four + // relational operators only; ==/!= always fall through to the + // ordinary numeric-or-string handling at the bottom of this + // function. + const bool relational = (op == "<" || op == ">" || op == "<=" || op == ">="); + + if (relational) { + // MSBuild keyword "Current" represents the installed toolset version. + MSBuildVersion currentVersion; + const auto it = mVariables.find("VisualStudioVersion"); + if (it != mVariables.end()) + currentVersion = MSBuildVersion::parse(it->second); + + if (currentVersion.empty()) + currentVersion = MSBuildVersion::parse("18.0"); // VS 2026 fallback + + if (caseInsensitiveStringCompare(lhs, "Current") == 0) { + const MSBuildVersion rhsVersion = MSBuildVersion::parse(rhs); + if (!rhsVersion.empty()) + return currentVersion.compareOp(op, rhsVersion); + } + + if (caseInsensitiveStringCompare(rhs, "Current") == 0) { + const MSBuildVersion lhsVersion = MSBuildVersion::parse(lhs); + if (!lhsVersion.empty()) + return lhsVersion.compareOp(op, currentVersion); + } + } + + // MSBuild tries numeric comparison before other comparison types -- + // for every operator, == and != included (see this function's own + // comment above). + long lhsInt = 0; + long rhsInt = 0; + if (parseInteger(lhs, lhsInt) && parseInteger(rhs, rhsInt)) + return applyComparison(lhsInt, rhsInt, op); + + // Then it tries boolean comparison -- also for every operator. + const auto parseBoolean = [](const std::string &value, bool &result) { + if (caseInsensitiveStringCompare(value, "true") == 0) { + result = true; + return true; + } + if (caseInsensitiveStringCompare(value, "false") == 0) { + result = false; + return true; + } + return false; + }; + + bool lhsBool = false; + bool rhsBool = false; + if (parseBoolean(lhs, lhsBool) && parseBoolean(rhs, rhsBool)) + return applyComparison(lhsBool, rhsBool, op); + + if (relational) { + // Then it tries Version comparison -- relational operators only + // (see this function's own comment above). + const MSBuildVersion lhsVersion = MSBuildVersion::parse(lhs); + const MSBuildVersion rhsVersion = MSBuildVersion::parse(rhs); + if (!lhsVersion.empty() && !rhsVersion.empty()) + return lhsVersion.compareOp(op, rhsVersion); + } + + // Finally, == and != fall back to case-insensitive string comparison. + if (op == "==" || op == "!=") { + const bool equal = caseInsensitiveStringCompare(lhs, rhs) == 0; + return op == "==" ? equal : !equal; + } + + throw std::runtime_error("Cannot compare '" + lhs + "' and '" + rhs + "'"); + } +}; + +bool ImportProject::evalCondition(const std::string &condition, const PropertiesMap &properties) { + try { + return ConditionParser(*this, condition, properties).parse(); + } catch (const std::exception &e) { + // Malformed or unsupported condition syntax. Log so callers can + // distinguish "evaluated false" from "could not be evaluated" -- the + // build behavior (treat as false and continue) is unchanged. + addDebug(std::string("condition unsupported: '") + condition + "': " + e.what()); + return false; + } +} + +bool ImportProject::conditionIsTrue(const tinyxml2::XMLElement *node, const PropertiesMap &properties) { + // See mDiscovering's doc comment: while the discovery bootstrap scan is + // running, every Condition is treated as satisfied rather than evaluated + // against guessed property values. + if (mDiscovering) + return true; + const char *condAttr = node->Attribute("Condition"); + if (!condAttr) + return true; + return evalCondition(condAttr, properties); +} + +bool ImportProject::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 ImportProject::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 ImportProject::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); +} + +// Structural name check only -- unlike hasName(), does NOT evaluate `node`'s own +// Condition attribute. Microsoft documents that the Visual Studio C++ project +// system does not support Condition on project items themselves (item types +// governed by a rule definition, e.g. ClCompile/ClInclude/etc. inside an +// or ): "Conditions aren't supported for +// Project items (that is, item types that are treated as project items by +// rules definitions)." -- only Condition on the item's *metadata* children is +// honored there (see applyClCompileChild(), which still calls conditionIsTrue() +// for exactly that reason). Use this instead of hasName() when checking a +// project item element's name for that reason. +static bool hasElementName(const tinyxml2::XMLElement *node, const char *nodeName) { + const char *name = node->Name(); + return name && std::strcmp(nodeName, name) == 0; +} + +// Canonical key identifying one project/props/targets file within an evaluation: +// forward slashes, '.'/'..' collapsed, lower-cased (NTFS is case-insensitive, and +// MSBuild's own import bookkeeping is case-insensitive too). +static std::string importFileKey(const std::string &filename) +{ + std::string key = Path::simplifyPath(Path::fromNativeSeparators(filename)); + std::transform(key.begin(), key.end(), key.begin(), [](unsigned char c) { + return std::tolower(c); + }); + return key; +} + +bool ImportProject::importGraphDecision(bool conditionHolds, std::string &file) +{ + if (!mImportGraph.active) + return conditionHolds; + + auto &decisions = mImportGraph.decisions[mImportGraph.currentFile]; + + if (mImportGraph.replay) { + auto &pos = mImportGraph.cursor[mImportGraph.currentFile]; + if (pos >= decisions.size()) { + // Structural mismatch between the Properties pass and this replay pass -- + // should not happen (both walk the same, unchanged document), but fail + // closed (treat as not taken) rather than read out of bounds or guess. + addDebug("import graph replay desync in '" + mImportGraph.currentFile + "'"); + return false; + } + const ImportGraph::Decision &decision = decisions[pos++]; + file = decision.file; + return decision.taken; + } + + ImportGraph::Decision decision; + decision.taken = conditionHolds; + decision.file = conditionHolds ? file : std::string(); + decisions.push_back(std::move(decision)); + return conditionHolds; +} + +bool ImportProject::importGraphChooseDecision(bool matched, std::size_t &branch) +{ + if (!mImportGraph.active) + return matched; + + auto &decisions = mImportGraph.decisions[mImportGraph.currentFile]; + + if (mImportGraph.replay) { + auto &pos = mImportGraph.cursor[mImportGraph.currentFile]; + if (pos >= decisions.size()) { + addDebug("import graph replay desync in '" + mImportGraph.currentFile + "'"); + return false; + } + const ImportGraph::Decision &decision = decisions[pos++]; + branch = decision.branch; + return decision.taken; } - return true; + ImportGraph::Decision decision; + decision.taken = matched; + decision.branch = matched ? branch : 0; + decisions.push_back(std::move(decision)); + return matched; } -bool ImportProject::importSlnx(const std::string& filename, const std::vector& fileFilters) +ImportProject::ImportResult ImportProject::attemptSyntheticImport(std::string file, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack, + EvalPhase phase) { - tinyxml2::XMLDocument doc; - const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); - if (error != tinyxml2::XML_SUCCESS) { - errors.emplace_back(std::string("Visual Studio solution file is not a valid XML - ") + tinyxml2::XMLDocument::ErrorIDToName(error)); - return false; + if (!importGraphDecision(!file.empty(), file)) + return ImportResult::Ok; + + const ImportResult result = processImport(file, properties, metadata, compileList, projectConfigurationList, importStack, phase); + if (result > ImportResult::NotResolvable) + addDebug("Could not fully import \"" + file + "\" - " + importResultStr(result) + " (continuing)"); + return result; +} + +namespace { + // Trim leading and trailing ASCII whitespace in-place. + void trimWhitespace(std::string &s) { + const auto first = s.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) { + s.clear(); + return; + } + s.erase(0, first); + s.erase(s.find_last_not_of(" \t\r\n") + 1); } - const tinyxml2::XMLElement* const rootnode = doc.FirstChildElement(); - if (rootnode == nullptr) { - errors.emplace_back("Visual Studio solution file has no XML root node"); - return false; + 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) { + std::string piece = s.substr(pos1, pos2 - pos1); + trimWhitespace(piece); + if (!piece.empty()) + ret.push_back(msbuildUnescape(piece)); + } + pos1 = pos2 + 1; + } + + if (pos1 < s.size()) { + std::string piece = s.substr(pos1); + trimWhitespace(piece); + if (!piece.empty()) + ret.push_back(msbuildUnescape(piece)); + } + + return ret; } - if (std::strcmp(rootnode->Name(), "Solution") != 0) { - errors.emplace_back("Invalid Visual Studio solution file format"); - return false; + /// Return \p path with its root component stripped. + /// The path must already be normalised to forward slashes. + /// UNC paths "//server/share/foo/" -> "foo/" + /// Local paths "C:/foo/" -> "foo/" + /// The trailing separator (if any) is preserved unchanged. + std::string stripPathRoot(const std::string &path) { + if (path.size() >= 2 && path[0] == '/' && path[1] == '/') { + // UNC: //server/share/... -- skip server then share component. + const auto serverEnd = path.find('/', 2); + if (serverEnd == std::string::npos) + return {}; + const auto shareEnd = path.find('/', serverEnd + 1); + if (shareEnd == std::string::npos) + return {}; + return path.substr(shareEnd + 1); + } + // Local absolute (C:/...) or relative: strip up to and including the first '/'. + const auto pos = path.find('/'); + return (pos != std::string::npos) ? path.substr(pos + 1) : path; } - std::map variables; - variables["SolutionDir"] = Path::simplifyPath(Path::getPathFromFilename(filename)); + 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); + } - bool found = false; - std::vector sharedItemsProjects; + 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 std::string thisFile = stripDirectoryPart(nfilename); + properties["MSBuildThisFile"] = thisFile; + properties["MSBuildThisFileName"] = fileStem(thisFile); + properties["MSBuildThisFileDirectory"] = Path::getPathFromFilename(nfilename); + properties["MSBuildThisFileDirectoryNoRoot"] = stripPathRoot(Path::getPathFromFilename(nfilename)); + properties["MSBuildThisFileExtension"] = Path::getFilenameExtensionInLowerCase(nfilename); + } - auto processProject = [&](const tinyxml2::XMLElement* projectNode) { - const char* pathAttribute = projectNode->Attribute("Path"); - if (pathAttribute == nullptr) - return true; + ~MSBuildThis() { + propertiesMap["MSBuildThisFile"] = thisFile; + propertiesMap["MSBuildThisFileName"] = thisFileName; + propertiesMap["MSBuildThisFileExtension"] = thisFileExtension; + propertiesMap["MSBuildThisFileDirectory"] = thisFileDirectory; + propertiesMap["MSBuildThisFileDirectoryNoRoot"] = thisFileDirectoryNoRoot; + propertiesMap["MSBuildThisFileFullPath"] = thisFileFullPath; + } + }; - std::string vcxproj(pathAttribute); - vcxproj = Path::toNativeSeparators(std::move(vcxproj)); + // Sets ImportProject::mDiscovering for the lifetime of the discovery bootstrap + // scan in importVcxproj(), and guarantees it is cleared again even if that scan + // exits early -- a stuck mDiscovering would silently make every subsequent + // Condition in the whole import (not just this project) evaluate as true. + struct DiscoveringGuard { + bool &mFlag; - if (Path::getFilenameExtensionInLowerCase(vcxproj) != ".vcxproj") - return true; // skip other project types + explicit DiscoveringGuard(bool &flag) : mFlag(flag) { + mFlag = true; + } + + ~DiscoveringGuard() { + mFlag = false; + } + }; - if (!Path::isAbsolute(vcxproj)) - vcxproj = variables["SolutionDir"] + vcxproj; + struct ImportStackGuard { + std::unordered_set &mStack; + std::string mKey; - 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; + ImportStackGuard(std::unordered_set &stack, std::string key) + : mStack(stack), mKey(std::move(key)) {} + + ~ImportStackGuard() { + mStack.erase(mKey); } - 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) { - for (const tinyxml2::XMLElement* childNode = node->FirstChildElement(); childNode; childNode = childNode->NextSiblingElement()) { - if (std::strcmp(childNode->Name(), "Project") == 0) { - if (!processProject(childNode)) - return false; + // Tracks, for the duration of one processImport() call, which container file's + // children are being walked -- so importGraphDecision() keys the decision it + // records/replays by the right file. Restores the caller's file on return so + // recursion unwinds back to the correct context. + struct CurrentFileGuard { + std::string &mCurrent; + std::string mPrev; + + CurrentFileGuard(std::string ¤t, std::string next) + : mCurrent(current), mPrev(current) { + mCurrent = std::move(next); + } + + ~CurrentFileGuard() { + mCurrent = std::move(mPrev); + } + }; +} + +std::string ImportProject::toAbsoluteExpanded(const std::string &filename, const std::string &baseDir) +{ + if (filename.empty()) + return filename; + + const std::string normalized = Path::simplifyPath(filename); + + // classifyPath(), not the host-dependent Path::isAbsolute(), decides + // whether `normalized` is already fully rooted -- see the doc comment + // above pathCombineAppend() for why Path::isAbsolute() is avoided + // throughout this file: on a non-Windows host it only recognizes a + // leading '/', so a Windows drive-absolute path straight out of a + // .vcxproj, e.g. "C:/src/foo.cpp", would be misclassified as relative + // and joined onto baseDir -- "/C:/src/foo.cpp" -- which is not + // what Visual Studio does, and exactly the kind of host-dependent + // divergence this emulation exists to avoid. UNC and DriveAbsolute are + // the only PathKinds Path::isAbsolute() itself ever recognized as + // absolute, even natively on Windows (a bare "\foo" or "C:foo" both + // return false from it there too), so restricting to those two here + // keeps this in exact agreement with the previous, Windows-native + // behaviour -- just no longer host-dependent. + switch (classifyPath(normalized)) { + case PathKind::UNC: + case PathKind::DriveAbsolute: + return normalized; + default: + break; + } + + return Path::simplifyPath(Path::join(baseDir, normalized)); +} + +std::string ImportProject::toAbsolute(const std::string &path) +{ + std::string internal(Path::fromNativeSeparators(path)); + switch (classifyPath(internal)) { + case PathKind::UNC: + case PathKind::DriveAbsolute: + return Path::simplifyPath(internal); + case PathKind::RootRelative: { + // Inherit drive letter from CWD: "\foo" on drive C: -> "C:/foo" + const std::string cwd = Path::fromNativeSeparators(Path::getCurrentPath()); + const std::string drive = (cwd.size() >= 2 && + std::isalpha(static_cast(cwd[0])) && + cwd[1] == ':') ? cwd.substr(0, 2) : std::string(); + return Path::simplifyPath(drive + internal); + } + default: + return Path::simplifyPath(Path::fromNativeSeparators(Path::getCurrentPath()) + "/" + internal); + } +} + +std::string ImportProject::toAbsolute(const std::string &filename, const std::string &baseDir, const PropertiesMap &properties) +{ + std::string resolved(Path::fromNativeSeparators(filename)); + if (!simplifyPathWithVariables(resolved, properties)) + return resolved; + + switch (classifyPath(resolved)) { + case PathKind::UNC: + case PathKind::DriveAbsolute: + return Path::simplifyPath(resolved); + case PathKind::RootRelative: { + // Inherit drive letter from baseDir: "\foo" with base "C:/project/" -> "C:/foo" + const std::string drive = (baseDir.size() >= 2 && + std::isalpha(static_cast(baseDir[0])) && + baseDir[1] == ':') ? baseDir.substr(0, 2) : std::string(); + return Path::simplifyPath(drive + resolved); + } + case PathKind::DriveRelative: + // "C:foo" is relative to the current directory of drive C:, a per-drive + // CWD that is a Windows kernel concept unavailable in a cross-platform + // context. Return the path unmodified rather than inventing a wrong base. + addDebug("toAbsolute: drive-relative path cannot be resolved: " + resolved); + return resolved; + default: + return Path::simplifyPath(baseDir + resolved); + } +} + +bool ImportProject::simplifyPathWithVariables(std::string &s, const 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, const 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)) { + // A macro in this entry didn't resolve (simplifyPathWithVariables() + // already left `s` with the literal, unexpanded "$(...)" text in it + // This does NOT silently drop the entry: a directory that can never exist is + // harmless to keep in includePaths (nothing will ever match it), but + // dropping it silently left the user with no way to find out why headers + // that should have been under it went unfound -- addDebug() alone isn't + // visible by default (see debugs' doc comment in importproject.h), and + // even --enable=missingInclude only reports the symptom (a header not + // found) with no link back to this cause. So the literal entry is kept + // AND a normal, always-visible message is recorded -- errors (unlike + // debugs) is printed unconditionally by the CLI, matching how the + // missingFile/missingIncludeExplicit fixes for ClCompile/ForcedIncludeFiles + // are also always-visible, not gated behind --debug. + errors.emplace_back("AdditionalIncludeDirectories entry has an unresolved macro, " + "include path will not be found: '" + s + "'"); + } + if (s.empty()) + continue; + fs.includePaths.push_back(s.back() == '/' ? s : (s + '/')); + } +} + +static void findAndReplaceCaseInsensitive(std::string &s, + const std::string &search, + const std::string &replacement) +{ + if (search.empty()) + return; + + std::size_t pos = 0; + while ((pos = s.find(search[0], pos)) != std::string::npos) { + if (pos + search.size() <= s.size() && + caseInsensitiveStringCompare(s.substr(pos, search.size()), search) == 0) { + s.replace(pos, search.size(), replacement); + pos += replacement.size(); + } else { + ++pos; + } + } +} + +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 : ""); + // Trim indentation/newlines from pretty-printed XML text content. + trimWhitespace(text); + // 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 selfRef = "$(" + std::string(eName) + ")"; + // Properties are evaluated at assignment time. Only the explicit self-reference + // used for MSBuild-style accumulation needs to be substituted from the old value. + const auto it = properties.find(eName); + const std::string original = (it != properties.end()) ? it->second : std::string(); + findAndReplaceCaseInsensitive(text, selfRef, original); + expandMSBuildVariables(text, properties); + properties[eName] = text; + if (caseInsensitiveStringCompare(eName, "ConfigurationType") == 0) { + std::string targetExtension = ".exe"; // Windows standard floor default + bool extDetermined = false; + + if (caseInsensitiveStringCompare(text, "StaticLibrary") == 0) { + targetExtension = ".lib"; + extDetermined = true; + } else if (caseInsensitiveStringCompare(text, "DynamicLibrary") == 0) { + targetExtension = ".dll"; + extDetermined = true; + } else if (caseInsensitiveStringCompare(text, "Application") == 0) { + targetExtension = ".exe"; + extDetermined = true; + } else if (caseInsensitiveStringCompare(text, "Makefile") == 0 || + caseInsensitiveStringCompare(text, "Utility") == 0) { + targetExtension = ""; // Explicitly clear target extension for meta/script nodes + extDetermined = true; + } + + // Only overwrite TargetExt if the project file hasn't already provided + // an explicit, dedicated user extension override. + if (properties.find("TargetExt") == properties.end() || extDetermined) { + properties["TargetExt"] = targetExtension; + } + + // TargetName defaults to ProjectName when ConfigurationType is established + if (properties.find("TargetName") == properties.end() && properties.count("ProjectName") > 0) { + properties["TargetName"] = properties["ProjectName"]; + } + } + + checkUnexpandedExpressions(text, eName); +} + +void ImportProject::addMetadata(const tinyxml2::XMLElement *node, const 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 : ""); + trimWhitespace(text); + text = Path::fromNativeSeparators(std::move(text)); + const std::string metaSelfRef = "%(" + std::string(eName) + ")"; + const std::string propSelfRef = "$(" + std::string(eName) + ")"; + + // Pre-expand the accumulated metadata value before embedding it: resolve %(other) + // metadata refs and $(prop) refs inside `original` first, then break any tainted + // self-references, so they don't propagate into the new value and risk step-3 erasure. + std::string original = metadata[eName]; + findAndReplaceCaseInsensitive(original, metaSelfRef, ""); + std::string::size_type p = 0; + + // Add a substitution tracking depth limit to explicitly catch and break + // circular/infinite macro evaluation loops from malformed recursive property sheets. + std::size_t substitutionDepth = 0; + while ((p = original.find("%(", p)) != std::string::npos) { + if (++substitutionDepth > 100) { + addDebug("Circular metadata inheritance chain broken in addMetadata for: " + std::string(eName)); + break; + } + + const std::string::size_type e = findMatchingParen(original, p + 1); + if (e == std::string::npos) + break; + const std::string key = original.substr(p + 2, e - p - 2); + const auto it = metadata.find(key); + const std::string repl = (it != metadata.end()) ? it->second : std::string(); + original.replace(p, e - p + 1, repl); + p += repl.size(); + } + + expandMSBuildVariables(original, properties); + findAndReplaceCaseInsensitive(original, propSelfRef, ""); + findAndReplaceCaseInsensitive(text, metaSelfRef, original); + + std::string::size_type pos = 0; + std::size_t textSubstitutionDepth = 0; + while ((pos = text.find("%(", pos)) != std::string::npos) { + if (++textSubstitutionDepth > 100) { + addDebug("Circular text metadata reference chain broken in addMetadata for: " + std::string(eName)); + break; + } + + const std::string::size_type end = findMatchingParen(text, pos + 1); + 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); + + // Handle $(eName) self-references: some props files use property-style + // accumulation (e.g. $(DisableSpecificWarnings);4100 + // ) in ItemDefinitionGroup blocks. Property (and + // metadata) names are case-insensitive in MSBuild, matching the + // case-insensitive findAndReplaceCaseInsensitive() used for propSelfRef and + // metaSelfRef everywhere else in this function and in getMetadata() below -- + // the plain, case-sensitive findAndReplace() (lib/utils.h, used elsewhere for + // unrelated literal placeholder substitution) would miss a self-reference + // spelled with different casing than eName. + findAndReplaceCaseInsensitive(text, propSelfRef, original); + findAndReplaceCaseInsensitive(text, propSelfRef, ""); + metadata[eName] = text; + checkUnexpandedExpressions(text, eName); +} + +std::string ImportProject::getMetadata(const tinyxml2::XMLElement *node, const PropertiesMap &properties, const MetadataMap &metadata, const std::string &original) { + const char *eName = node->Name(); + if (!eName || !conditionIsTrue(node, properties)) + return original; + // An explicitly empty element () is a meaningful assignment + // to ""; do NOT treat GetText()==nullptr as "no change". + const char *eText = node->GetText(); + std::string text(eText ? eText : ""); + trimWhitespace(text); + text = Path::fromNativeSeparators(std::move(text)); + const std::string metaSelfRef = "%(" + std::string(eName) + ")"; + const std::string propSelfRef = "$(" + std::string(eName) + ")"; + + // Pre-expand `original` (the prior per-item value) before embedding it, + // matching the same strategy used in addMetadata and addProperty. + std::string expandedOriginal = original; + findAndReplaceCaseInsensitive(expandedOriginal, metaSelfRef, ""); + std::string::size_type p = 0; + + // Add a substitution tracking depth limit to explicitly catch and break + // circular/infinite macro evaluation loops from malformed recursive property sheets. + std::size_t substitutionDepth = 0; + while ((p = expandedOriginal.find("%(", p)) != std::string::npos) { + if (++substitutionDepth > 100) { + addDebug("Circular metadata inheritance chain broken in getMetadata for: " + std::string(eName)); + break; + } + + const std::string::size_type e = findMatchingParen(expandedOriginal, p + 1); + if (e == std::string::npos) + break; + const std::string key = expandedOriginal.substr(p + 2, e - p - 2); + const auto it = metadata.find(key); + const std::string repl = (it != metadata.end()) ? it->second : std::string(); + expandedOriginal.replace(p, e - p + 1, repl); + p += repl.size(); + } + + expandMSBuildVariables(expandedOriginal, properties); + findAndReplaceCaseInsensitive(expandedOriginal, propSelfRef, ""); + findAndReplaceCaseInsensitive(text, metaSelfRef, expandedOriginal); + + std::string::size_type pos = 0; + std::size_t textSubstitutionDepth = 0; + while ((pos = text.find("%(", pos)) != std::string::npos) { + if (++textSubstitutionDepth > 100) { + addDebug("Circular text metadata reference chain broken in getMetadata for: " + std::string(eName)); + break; + } + + const std::string::size_type end = findMatchingParen(text, pos + 1); + 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); + + // Handle $(eName) self-references: same accumulation pattern as addMetadata. + findAndReplaceCaseInsensitive(text, propSelfRef, expandedOriginal); + findAndReplaceCaseInsensitive(text, propSelfRef, ""); + checkUnexpandedExpressions(text, eName); + return text; +} + +const std::string &ImportProject::importResultStr(ImportProject::ImportResult result) { + static const std::string ok("ok"); + static const std::string notResolvable("Not Resolvable"); + static const std::string notFound("Not Found"); + static const std::string notValid("Not Valid"); + static const std::string cycle("Cycle"); + static const 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; +} + +void ImportProject::applyClCompileChild(const tinyxml2::XMLElement *e1, + const PropertiesMap &properties, + MetadataMap &metadata) +{ + const char *eName = e1->Name(); + if (!eName || !conditionIsTrue(e1, properties)) + return; + + // Visual Studio permits any item metadata to be overridden at item level. + // Keep the same metadata expansion and inheritance behavior used by + // ItemDefinitionGroup processing. + auto &value = metadata[eName]; + value = getMetadata(e1, properties, metadata, value); +} + +static void applyAdditionalOptions(MetadataMap &metadata) +{ + const std::string &additionalOptions = metadata["AdditionalOptions"]; + if (additionalOptions.empty()) + return; + + std::vector args; + std::string arg; + bool quoted = false; + + // Tokenize options by space boundaries, honoring double-quote scopes + 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; + } + } + if (!arg.empty()) + args.emplace_back(std::move(arg)); + + for (std::size_t i = 0; i < args.size(); ++i) { + const std::string &origOption = args[i]; + if (origOption.empty()) + continue; + + std::string option = origOption; + std::transform(option.begin(), option.end(), option.begin(), [](unsigned char c) { + return std::tolower(c); + }); + + // Ensure token starts with valid MSVC/GCC switch symbols + if (option[0] != '/' && option[0] != '-') + continue; + + // 1. Precise Match for Preprocessor Definitions (/D or -D) + if (option.size() >= 2 && option[1] == 'd') { + // Verify this isn't a long system flag prefix like /debug or /diagnostics + if (option.size() == 2 || (option.compare(0, 12, "/diagnostics") != 0 && option.compare(0, 12, "-diagnostics") != 0 && + option.compare(0, 6, "/debug") != 0 && option.compare(0, 6, "-debug") != 0 && + option.compare(0, 12, "/dynamicbase") != 0 && option.compare(0, 12, "-dynamicbase") != 0 && + option.compare(0, 6, "/delay") != 0 && option.compare(0, 6, "-delay") != 0 && + option.compare(0, 4, "/doc") != 0 && option.compare(0, 4, "-doc") != 0)) { + + std::string define; + if (option.size() == 2) { + // Form: /D MacroName (Space-separated) + if (i + 1 < args.size()) { + define = args[i + 1]; + args[i + 1] = ""; // Neutralize lookahead token to skip it cleanly next round + } + } else { + // Form: /DMacroName (Glued payload) + define = origOption.substr(2); + } + + if (!define.empty()) { + if (!metadata["PreprocessorDefinitions"].empty()) + metadata["PreprocessorDefinitions"] += ';'; + metadata["PreprocessorDefinitions"] += define; + } + continue; + } + } + + // 2. Precise Match for Include Directories (/I or -I) + if (option.size() >= 2 && option[1] == 'i') { + // Explicitly shield long option variants like GCC's -isystem + if (option.compare(0, 8, "-isystem") != 0 && option.compare(0, 8, "/isystem") != 0) { + + std::string path; + if (option.size() == 2) { + // Form: /I PathName (Space-separated) + if (i + 1 < args.size()) { + path = args[i + 1]; + args[i + 1] = ""; // Neutralize lookahead token to skip it cleanly next round + } + } else { + // Form: /IPathName (Glued payload) + path = origOption.substr(2); + } + + if (!path.empty()) { + if (!metadata["AdditionalIncludeDirectories"].empty()) + metadata["AdditionalIncludeDirectories"] += ';'; + metadata["AdditionalIncludeDirectories"] += path; } + continue; + } + } + + // 3. Strict String Matching for Language Standards + if (option == "/std:c++11" || option == "-std=c++11") { + metadata["LanguageStandard"] = "stdcpp11"; + } else if (option == "/std:c++14" || option == "-std=c++14") { + metadata["LanguageStandard"] = "stdcpp14"; + } else if (option == "/std:c++17" || option == "-std=c++17") { + metadata["LanguageStandard"] = "stdcpp17"; + } else if (option == "/std:c++20" || option == "-std=c++20") { + metadata["LanguageStandard"] = "stdcpp20"; + } else if (option == "/std:c++23" || option == "-std=c++23") { + metadata["LanguageStandard"] = "stdcpp23"; + } else if (option == "/std:c++latest" || option == "-std=c++latest") { + metadata["LanguageStandard"] = "stdcpplatest"; + } else if (option == "/std:c11" || option == "-std=c11") { + metadata["LanguageStandard_C"] = "stdc11"; + } else if (option == "/std:c17" || option == "-std=c17") { + metadata["LanguageStandard_C"] = "stdc17"; + } else if (option == "/std:clatest" || option == "-std=clatest") { + metadata["LanguageStandard_C"] = "stdclatest"; + } + } +} + +// Visual Studio's C++ project system does not reliably resolve a macro in a +// project item path if that macro's value COULD differ by configuration: +// "Macros that change their value for different configurations will cause +// problems... The IDE doesn't expect project item paths to be different for +// different project configurations." That is a statement about macros whose +// value varies by configuration, not about user-defined macros in general -- +// a macro a property sheet or PropertyGroup sets to a fixed location (e.g. +// $(BoostRoot) or $(wxMathPlot) pointing at a third-party library, unconditional +// on Configuration/Platform) resolves the same way regardless of which +// configuration the IDE happens to evaluate, so real Visual Studio expands it +// in an Include/Update/Remove path exactly as it would anywhere else. +// +// A small, fixed set of MSBuild "this file"/"this project" location +// properties are ALWAYS config-invariant by construction, no matter what a +// given project does with them -- these are exactly what Visual Studio's own +// Shared Items projects (.vcxitems) use to write portable Include paths, e.g. +// (see +// test/cli/shared-items-project). A property this project's own PropertyGroups +// never touch at all but that resolves to a real OS environment variable is +// config-invariant too -- it has exactly one value for the whole cppcheck +// invocation, and MSBuild property evaluation itself falls back to the +// environment for anything no PropertyGroup sets (PropertyValueExpander::lookup() +// already does this; the check here just has to agree, or expandMSBuildVariables() +// below would never even be called to do it). Everything else is only as +// safe as this particular project makes it: mConfigInvariantProperties (see +// its doc comment in importproject.h) is computed per project file by +// priming every configuration's Properties pass before the real +// per-configuration passes run, and holds every property name that came out +// with the same value in all of them. +// MSBuild property names are case-insensitive -- $(msbuildprojectdirectory) +// and $(MSBuildProjectDirectory) name the same built-in property to real +// Visual Studio -- so this uses the same cppcheck::stricmp comparator +// PropertiesMap itself does, not a plain (case-sensitive) std::set. +static const std::set &invariantItemPathProperties() { + static const std::set props = { + "MSBuildThisFileDirectory", "MSBuildThisFileFullPath", "MSBuildThisFile", + "MSBuildThisFileName", "MSBuildThisFileExtension", "MSBuildThisFileDirectoryNoRoot", + "MSBuildProjectDirectory", "MSBuildProjectFullPath", "MSBuildProjectFile", + "MSBuildProjectName", "MSBuildProjectExtension", "MSBuildProjectDirectoryNoRoot", + "SolutionDir", "ProjectDir", + }; + return props; +} + +// Returns whether every $(...) reference in `spec` is a bare reference (no +// .Method(...) chain, no $([Class]::...) static function) to a property that +// is always config-invariant (invariantItemPathProperties() above), that +// this project's configInvariant set says is config-invariant here (every +// configuration of THIS project resolved it to the same value -- see +// mConfigInvariantProperties's doc comment), or that resolves via a real OS +// environment variable of the same name (see this function's own doc +// comment above). A false result means `spec` depends on something Visual +// Studio's C++ project system does not reliably resolve in a project item +// path -- expandItemSpec() must not expand it, though (see its own comment) +// that does not mean discarding the item. +static bool hasOnlyInvariantItemPathVariables(const std::string &spec, const std::set &configInvariant) { + std::size_t pos = 0; + while ((pos = spec.find("$(", pos)) != std::string::npos) { + std::size_t i = pos + 2; + std::string name; + // MSBuild property names are [A-Za-z_][A-Za-z0-9_-]* -- '-' is valid + // after the first character (see PropertyValueExpander::parseIdentifier()'s + // doc comment) -- and this scan is only ever used for a bare $(Name) + // property reference (the caller requires the very next character to + // be ')', never '.' or '('), so there's no method name to worry about + // conflating this with here. + while (i < spec.size() && (std::isalnum(static_cast(spec[i])) || spec[i] == '_' || spec[i] == '-')) + name += spec[i++]; + if (name.empty() || i >= spec.size() || spec[i] != ')' || + (!invariantItemPathProperties().count(name) && !configInvariant.count(name) && + std::getenv(name.c_str()) == nullptr)) + return false; + pos = i + 1; + } + return true; +} + +std::pair ImportProject::expandItemSpec(const std::string &spec, + const std::string &projectDir, + const PropertiesMap &properties) +{ + if (spec.empty()) + return std::make_pair(std::string(), std::string()); + + std::string expandedSpec = spec; + if (hasOnlyInvariantItemPathVariables(spec, mConfigInvariantProperties)) { + // Every $(...) reference here is either one of Visual Studio's own + // "this file"/"this project" properties, a property this project + // resolves to the same value in every configuration, or a real + // environment variable -- all config-invariant, so expanding it here + // matches what the IDE itself does (see invariantItemPathProperties() + // and mConfigInvariantProperties). + expandMSBuildVariables(expandedSpec, properties); + } else if (spec.find("$(") != std::string::npos) { + // Some other macro reference, one whose value could differ by + // configuration in this project and that no environment variable + // resolves either -- Visual Studio's C++ project system does not + // support that in a project item path (see + // invariantItemPathProperties()'s doc comment), so it is not + // expanded here. + // + // That does NOT mean discarding the item, unlike the wildcard/glob + // and semicolon-list cases below: a silently vanished ClCompile item + // leaves the user with no idea their file went unchecked, short of + // rerunning with --debug to see this addDebug() line. Instead, + // `expandedSpec` is left as-is (the literal, unexpanded "$(...)" + // text stays in it) and falls through the rest of this function to + // become the item's resolved path below. That literal path can never + // exist on disk, so when cppcheck later tries to actually open it, + // simplecpp's normal FileStream failure path reports it exactly like + // any other missing source file -- a visible "File is missing: ..." + // error citing this literal, still-macro'd path -- which also tells + // the user precisely which macro it was that didn't resolve. + addDebug("Macro in item path not supported by Visual Studio IDE layout, left unexpanded: '" + spec + "'"); + } + + if (expandedSpec.find(';') != std::string::npos) { + addDebug("Multiple files or semicolon-delimited list detected in path attribute, skipped: '" + spec + "'"); + return std::make_pair(std::string(), std::string()); + } + + // Treat the ENTIRE expanded string as a single literal path. + // Visual Studio IDE does NOT split 'Include', 'Update', or 'Remove' attributes by semicolons. + std::size_t lo = 0, hi = expandedSpec.size(); + while (lo < hi && std::isspace(static_cast(expandedSpec[lo]))) ++lo; + while (hi > lo && std::isspace(static_cast(expandedSpec[hi - 1]))) --hi; + + if (lo < hi) { + std::string trimmed = expandedSpec.substr(lo, hi - lo); + + // Strip outer encapsulation quotes if present across the entire path string + if (trimmed.size() >= 2 && trimmed.front() == '"' && trimmed.back() == '"') { + trimmed = trimmed.substr(1, trimmed.size() - 2); + } + + const std::string decoded = msbuildUnescape(trimmed); + + // Visual Studio IDE rejects item wildcards/globs entirely during project loading + if (decoded.find('*') != std::string::npos || decoded.find('?') != std::string::npos) { + addDebug("ClCompile item glob not supported by Visual Studio IDE layout, skipped: '" + decoded + "'"); + return std::make_pair(std::string(), std::string()); + } + + // Return a single token pair mapping directly to one literal path on disk + return std::make_pair(decoded, toAbsoluteExpanded(decoded, projectDir)); + } + + return std::make_pair(std::string(), std::string()); +} + +void ImportProject::applyClCompileUpdate(const tinyxml2::XMLElement *node, + const std::string &baseDir, + const PropertiesMap &properties, + std::list &compileList) +{ + const char *update = node->Attribute("Update"); + if (!update) + return; + + const std::pair updateItem = expandItemSpec(update, baseDir, properties); + + if (updateItem.first.empty()) + return; + + for (ItemGroupClCompile &compile : compileList) { + if (Path::sameFileName(updateItem.second, compile.filename)) { + for (const tinyxml2::XMLElement *child = node->FirstChildElement(); + child; + child = child->NextSiblingElement()) { + applyClCompileChild(child, properties, compile.metadata); } + applyAdditionalOptions(compile.metadata); } } +} + +void ImportProject::applyClCompileRemove(const tinyxml2::XMLElement *node, + const std::string &baseDir, + const PropertiesMap &properties, + std::list &compileList) +{ + const char *remove = node->Attribute("Remove"); + if (!remove) + return; + + const std::pair removeItem = expandItemSpec(remove, baseDir, properties); + + if (removeItem.first.empty()) + return; + + for (auto it = compileList.begin(); it != compileList.end();) { + if (Path::sameFileName(removeItem.second, it->filename)) + it = compileList.erase(it); + else + ++it; + } +} - if (!found) { - errors.emplace_back("no projects found in Visual Studio solution file"); - return false; +ImportProject::ImportResult ImportProject::processCompile(const tinyxml2::XMLElement *node, + const std::string &projectDir, + const PropertiesMap &properties, + const MetadataMap &metadata, + std::list &compileList) +{ + const char *include = node->Attribute("Include"); + if (!include) + return ImportResult::NotFound; + + const std::pair spec = expandItemSpec(include, projectDir, properties); + if (spec.first.empty()) + return ImportResult::NotFound; + + const std::string &toInclude = spec.second; + if (!Path::acceptFile(toInclude)) + return ImportResult::Ok; + + ItemGroupClCompile compile(toInclude); + compile.metadata = metadata; + + const std::string base = stripDirectoryPart(toInclude); + const std::string::size_type dot = base.rfind('.'); + const std::string ext = dot != std::string::npos ? base.substr(dot) : std::string(); + const std::string stem = fileStem(base); + + std::string rootDir; + std::size_t afterRoot = 0; + if (toInclude.size() >= 2 && toInclude[0] == '/' && toInclude[1] == '/') { + const std::size_t srv = toInclude.find('/', 2); + const std::size_t shr = (srv != std::string::npos) ? toInclude.find('/', srv + 1) : std::string::npos; + if (shr != std::string::npos) { + rootDir = toInclude.substr(0, shr + 1); + afterRoot = shr + 1; + } else { + rootDir = toInclude; + } + } else if (toInclude.size() >= 2 && + std::isalpha(static_cast(toInclude[0])) && + toInclude[1] == ':') { + if (toInclude.size() > 2 && toInclude[2] == '/') { + rootDir = toInclude.substr(0, 2) + "/"; + afterRoot = 3; + } else { + afterRoot = 2; + } + } else if (!toInclude.empty() && toInclude[0] == '/') { + rootDir = "/"; + afterRoot = 1; } + const std::size_t lastSlash = toInclude.rfind('/'); + const std::string directory = (lastSlash != std::string::npos && lastSlash >= afterRoot) + ? toInclude.substr(afterRoot, lastSlash - afterRoot + 1) + : std::string(); + + compile.metadata["Identity"] = Path::fromNativeSeparators(spec.first); + compile.metadata["FullPath"] = toInclude; + compile.metadata["RootDir"] = rootDir; + compile.metadata["Filename"] = stem; + compile.metadata["Extension"] = ext; + compile.metadata["Directory"] = directory; + compile.metadata["RecursiveDir"] = std::string(); + + const std::string origNorm = Path::fromNativeSeparators(spec.first); + const std::size_t relSlash = origNorm.rfind('/'); + compile.metadata["RelativeDir"] = (relSlash != std::string::npos) + ? origNorm.substr(0, relSlash + 1) + : std::string(); + + for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) + applyClCompileChild(e1, properties, compile.metadata); + + applyAdditionalOptions(compile.metadata); + + compileList.emplace_back(std::move(compile)); + return ImportResult::Ok; +} - return true; +// Looks up `name` in `properties`, returning its value or an empty string if absent +static std::string propertyOrEmpty(const PropertiesMap &properties, const std::string &name) +{ + const auto it = properties.find(name); + return it != properties.end() ? it->second : std::string(); } -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::processImportProject(const tinyxml2::XMLElement *node, + const std::string &projectDir, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack, + EvalPhase phase) { + // Structural check: is `node` an element at all? Callers + // no longer pre-filter this (see the call sites) -- whether it is actually taken + // is decided/replayed below via importGraphDecision(), which must run for every + // structurally-matching element, including ones whose Condition is false, so the + // decision sequence recorded per file stays positionally aligned with what + // ItemDefs/Items replay. + const char *elementName = node->Name(); + if (!elementName || std::strcmp(elementName, "Import") != 0) + return ImportResult::Ok; + const char *projectAttribute = node->Attribute("Project"); + if (!projectAttribute) + return ImportResult::Ok; + // During the discovery pass, re-run as a Properties-phase import but silently + // discard any errors/debugs it generates -- approximate properties cause many + // spurious failures that would confuse the user. + if (phase == EvalPhase::Discover) { + const auto errSize = errors.size(); + const auto dbgSize = debugs.size(); + const ImportResult r = processImportProject(node, projectDir, properties, metadata, compileList, + projectConfigurationList, importStack, EvalPhase::Properties); + errors.resize(errSize); + debugs.resize(dbgSize); + return r; + } + // Resolve this 's target and decide (Properties pass) or replay + // (ItemDefs/Items pass) whether it is taken. See importGraphDecision() for why + // ItemDefs/Items must reuse the file Properties actually resolved rather than + // recomputing toAbsolute() against properties that may have changed since. + std::string file = toAbsolute(projectAttribute, projectDir, properties); + if (!importGraphDecision(conditionIsTrue(node, properties), file)) + return ImportResult::Ok; + + const std::string extension = Path::getFilenameExtensionInLowerCase(file); + if (extension == ".props" || extension == ".targets" || extension == ".vcxitems") { + const char *sdk = node->Attribute("Sdk"); + if (sdk) { + addDebug("Could not import \"" + file + "\" - " + " (Sdk not supported)"); + return ImportResult::NotResolvable; } - 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)) {} + // Identify well-known system files by their logical filename rather than a + // substring search on the full path. This avoids false positives when a + // user file's path happens to contain "Microsoft.Cpp.props" etc. + const auto filenameIs = [&file](const char *name) { + const std::string::size_type slash = file.rfind('/'); + const std::string part = (slash != std::string::npos) ? file.substr(slash + 1) : file; + const std::string::size_type paren = part.rfind(')'); + const std::string namePart = (paren != std::string::npos) ? part.substr(paren + 1) : part; + return caseInsensitiveStringCompare(namePart, name) == 0; + }; - 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 (filenameIs("Microsoft.Cpp.targets")) { + attemptSyntheticImport(propertyOrEmpty(properties, "ForceImportBeforeCppTargets"), + properties, metadata, compileList, projectConfigurationList, importStack, phase); + + // Microsoft.Common.targets (imported by Microsoft.Cpp.targets) sets + // OutputPath = $(OutDir) when OutputPath is not already defined. + // Emulate this before importing Directory.Build.targets so that files + // in that chain (e.g. bundle output paths) can expand $(OutputPath). + // Property assignment only happens during the Properties pass; ItemDefs/Items + // reuse the properties Properties already finished computing. + if (phase == EvalPhase::Properties && properties.find("OutputPath") == properties.end()) { + const auto outDirIt = properties.find("OutDir"); + if (outDirIt != properties.end()) + properties["OutputPath"] = outDirIt->second; } - } - // 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; + // Emulate key side-effect: Microsoft.Cpp.targets -> Microsoft.Common.targets -> Directory.Build.targets. + // $(ImportDirectoryBuildTargets) defaults to true; an explicit "false" + // suppresses the import entirely. $(DirectoryBuildTargetsPath), when set, + // overrides the upward directory search with an explicit file. See + // Microsoft's "Customize your build by folder or solution" docs. + const auto importTargetsIt = properties.find("ImportDirectoryBuildTargets"); + if (importTargetsIt == properties.end() || caseInsensitiveStringCompare(importTargetsIt->second, "false") != 0) { + const auto targetsPathIt = properties.find("DirectoryBuildTargetsPath"); + const std::string directoryBuildTargets = (targetsPathIt != properties.end() && !targetsPathIt->second.empty()) + ? targetsPathIt->second + : findFileAbove(projectDir, "Directory.Build.targets"); + attemptSyntheticImport(directoryBuildTargets, + properties, metadata, compileList, projectConfigurationList, importStack, phase); } - } - 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); + attemptSyntheticImport(propertyOrEmpty(properties, "ForceImportAfterCppTargets"), + properties, metadata, compileList, projectConfigurationList, importStack, phase); + + 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 (filenameIs("Microsoft.Cpp.Default.props")) { + attemptSyntheticImport(propertyOrEmpty(properties, "ForceImportBeforeCppDefaultProps"), + properties, metadata, compileList, projectConfigurationList, importStack, phase); + + // Emulate key side-effects here, including the Directory.Build.props import that + // Microsoft.Common.props would normally perform. + // Microsoft.Cpp.Default.props -> Microsoft.Common.props -> Directory.Build.props. + // Directory.Build.props is an ordinary import: it is walked in every phase so + // that its ItemDefinitionGroups and ItemGroups are collected as well, exactly + // like Directory.Build.targets in the Microsoft.Cpp.targets emulation above. + // $(ImportDirectoryBuildProps) defaults to true; an explicit "false" suppresses + // the import entirely. $(DirectoryBuildPropsPath), when set, overrides the + // upward directory search with an explicit file. See Microsoft's "Customize + // your build by folder or solution" docs. + const auto importPropsIt = properties.find("ImportDirectoryBuildProps"); + if (importPropsIt == properties.end() || caseInsensitiveStringCompare(importPropsIt->second, "false") != 0) { + const auto propsPathIt = properties.find("DirectoryBuildPropsPath"); + const std::string directoryBuildProps = (propsPathIt != properties.end() && !propsPathIt->second.empty()) + ? propsPathIt->second + : findFileAbove(projectDir, "Directory.Build.props"); + attemptSyntheticImport(directoryBuildProps, + properties, metadata, compileList, projectConfigurationList, importStack, phase); } - // 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(); - } + // Emulate key defaults set by Microsoft.Cpp.Default.props (properties only). + // Derive DefaultPlatformToolset from VisualStudioVersion (already in properties from + // the .sln header or the importVcxproj default). The mapping is: + // VS 10 -> v100, VS 11 -> v110, VS 12 -> v120, + // VS 14 -> v140, VS 15 -> v141, VS 16 -> v142, VS 17 -> v143, VS 18 -> v145 + if (phase == EvalPhase::Properties && properties.find("DefaultPlatformToolset") == properties.end()) { + auto vsIt = properties.find("VisualStudioVersion"); + if (vsIt != properties.end()) { + int major = 0; + try { major = std::stoi(vsIt->second); } catch (...) {} + std::string toolset; + if (major == 18) + toolset = "v145"; + else if (major == 17) + toolset = "v143"; + else if (major == 16) + toolset = "v142"; + else if (major == 15) + toolset = "v141"; + else if (major == 14) + toolset = "v140"; + else if (major == 12) + toolset = "v120"; + else if (major == 11) + toolset = "v110"; + else if (major == 10) + toolset = "v100"; + if (!toolset.empty()) + properties["DefaultPlatformToolset"] = toolset; } - 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("||"); - } + attemptSyntheticImport(propertyOrEmpty(properties, "ForceImportAfterCppDefaultProps"), + properties, metadata, compileList, projectConfigurationList, importStack, phase); - tokenlist.createAst(); + return ImportResult::Ok; + } - // Locate ast top and execute the condition - for (const Token *tok = tokenlist.front(); tok; tok = tok->next()) { - if (tok->astParent()) { - return execute(tok->astTop(), p) == "True"; - } + if (filenameIs("Microsoft.Cpp.props")) { + // ForceImportBeforeCppProps: honour any value set before Microsoft.Cpp.props + // is processed (e.g. by the vcxproj itself or by Directory.Build.props, which + // was already imported via the Microsoft.Cpp.Default.props handler above). + attemptSyntheticImport(propertyOrEmpty(properties, "ForceImportBeforeCppProps"), + properties, metadata, compileList, projectConfigurationList, importStack, phase); + + if (phase == EvalPhase::Properties) { + // Provide the output-directory defaults normally supplied by Cpp.props. + // Use emplace so values already assigned earlier in the project are preserved. + const auto platformIt = properties.find("Platform"); + const bool isWin32 = platformIt != properties.end() && + caseInsensitiveStringCompare(platformIt->second, "Win32") == 0; + std::string intDir = isWin32 ? "$(Configuration)/" : "$(Platform)/$(Configuration)/"; + std::string outDir = isWin32 ? "$(SolutionDir)$(Configuration)/" : "$(SolutionDir)$(Platform)/$(Configuration)/"; + expandMSBuildVariables(intDir, properties); + expandMSBuildVariables(outDir, properties); + properties.emplace("IntDir", intDir); + properties.emplace("OutDir", outDir); + // GeneratedFilesDir defaults to "Generated Files\" (relative to the project + // directory) -- that is what Microsoft.Cpp.Default.props provides, and it is + // the path CppWinRT projects use by convention (e.g. module.g.cpp). + // Do NOT prefix with IntDir here: IntDir is an absolute intermediate path + // specific to the build configuration, whereas GeneratedFilesDir is a + // project-relative folder that is often committed to source control. + properties.emplace("GeneratedFilesDir", "Generated Files/"); } - throw std::runtime_error("Invalid condition: '" + condition + "'"); - } + attemptSyntheticImport(propertyOrEmpty(properties, "ForceImportAfterCppProps"), + properties, metadata, compileList, projectConfigurationList, importStack, phase); - private: + return ImportResult::Ok; + } - static std::string executeOp1(const Token* tok, const ProjectConfiguration &p) { - return execute(tok->astOperand1(), p); + ImportResult result = processImport(file, properties, metadata, compileList, projectConfigurationList, importStack, phase); + if (result > ImportResult::NotResolvable) + addDebug("Could not fully import \"" + file + "\" - " + importResultStr(result) + " (continuing)"); + if (result == ImportResult::NotResolvable) { + addDebug("Could not import \"" + file + "\" - " + importResultStr(result)); } + } else { + addDebug("Could not import \"" + file + "\" unsupported extension " + extension); + } + return ImportResult::Ok; +} - static std::string executeOp2(const Token* tok, const ProjectConfiguration &p) { - return execute(tok->astOperand2(), p); +ImportProject::ImportResult ImportProject::processImportGroup(const tinyxml2::XMLElement *node, + const std::string &baseDir, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack, + EvalPhase phase) { + ImportResult ret = ImportResult::Ok; + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { + // processImportProject() itself checks whether `e` is structurally an + // element and safely no-ops (returns Ok) otherwise, so + // no pre-filter is needed here. + const ImportResult result = processImportProject(e, baseDir, properties, metadata, compileList, projectConfigurationList, importStack, phase); + if (result > ImportResult::NotResolvable) { + if (phase != EvalPhase::Discover) { + const char *proj = e->Attribute("Project"); + addDebug("Could not fully import \"" + std::string(proj ? proj : "") + "\" - " + importResultStr(result) + " (continuing)"); + } + ret = std::max(result, ret); } + } + return ret; +} - 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; - }; +ImportProject::ImportResult ImportProject::processChoose(const tinyxml2::XMLElement *node, + const std::string &baseDir, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack, + EvalPhase phase) { + if (mDiscovering) { + // Discovery cannot know which single a real build would select -- + // that depends on properties (Configuration/Platform above all) that are + // themselves what discovery is trying to find -- so explore every + // and any instead of picking one. mImportGraph is never active + // while mDiscovering is set (discovery runs before the per-configuration + // loop activates it), so there is no decision to record/replay here either. + ImportResult result = ImportResult::Ok; + for (const tinyxml2::XMLElement *child = node->FirstChildElement(); child; child = child->NextSiblingElement()) { + const char *childName = child->Name(); + if (!childName) + continue; + if (std::strcmp(childName, "When") == 0 || std::strcmp(childName, "Otherwise") == 0) { + const ImportResult r = processElementChildren(child, baseDir, properties, metadata, compileList, projectConfigurationList, importStack, phase); + result = std::max(result, r); + } + } + return result; + } - struct ItemDefinitionGroup : Conditional { - explicit ItemDefinitionGroup(const tinyxml2::XMLElement *idg, std::string includePaths) : Conditional(idg), additionalIncludePaths(std::move(includePaths)) { - for (const tinyxml2::XMLElement *e1 = idg->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { - const char* name = e1->Name(); - if (std::strcmp(name, "ClCompile") == 0) { - enhancedInstructionSet = "StreamingSIMDExtensions2"; - for (const tinyxml2::XMLElement *e = e1->FirstChildElement(); e; e = e->NextSiblingElement()) { - const char * const text = e->GetText(); - if (!text) - continue; - const char * const ename = e->Name(); - if (std::strcmp(ename, "PreprocessorDefinitions") == 0) - preprocessorDefinitions = text; - else if (std::strcmp(ename, "AdditionalIncludeDirectories") == 0) { - if (!additionalIncludePaths.empty()) - additionalIncludePaths += ';'; - additionalIncludePaths += text; - } else if (std::strcmp(ename, "LanguageStandard") == 0) { - if (std::strcmp(text, "stdcpp14") == 0) - cppstd = Standards::CPP14; - else if (std::strcmp(text, "stdcpp17") == 0) - cppstd = Standards::CPP17; - else if (std::strcmp(text, "stdcpp20") == 0) - cppstd = Standards::CPP20; - else if (std::strcmp(text, "stdcpplatest") == 0) - cppstd = Standards::CPPLatest; - } else if (std::strcmp(ename, "EnableEnhancedInstructionSet") == 0) { - enhancedInstructionSet = text; - } - } - } - else if (std::strcmp(name, "Link") == 0) { - for (const tinyxml2::XMLElement *e = e1->FirstChildElement(); e; e = e->NextSiblingElement()) { - const char * const text = e->GetText(); - if (!text) - continue; - if (std::strcmp(e->Name(), "EntryPointSymbol") == 0) { - entryPointSymbol = text; - } - } + bool matched = false; + std::size_t branch = 0; + + if (!mImportGraph.replay) { + // Decide (or, outside the three-pass evaluation, simply compute -- + // mDiscovering is handled separately above and never reaches here) + // which branch is taken: the first (document order) whose + // Condition evaluates true, or the if none did. Unlike + // PropertyGroup/ItemGroup/etc., itself carries no Condition -- + // only its children do -- so this does not go through hasName(). + std::size_t index = 0; + std::size_t otherwiseIndex = 0; + bool haveOtherwise = false; + for (const tinyxml2::XMLElement *child = node->FirstChildElement(); child; child = child->NextSiblingElement(), ++index) { + const char *childName = child->Name(); + if (!childName) + continue; + if (std::strcmp(childName, "When") == 0) { + const char *cond = child->Attribute("Condition"); + if (cond && evalCondition(cond, properties)) { + matched = true; + branch = index; + break; } + } else if (std::strcmp(childName, "Otherwise") == 0 && !haveOtherwise) { + haveOtherwise = true; + otherwiseIndex = index; } } + if (!matched && haveOtherwise) { + matched = true; + branch = otherwiseIndex; + } + } - std::string enhancedInstructionSet; - std::string preprocessorDefinitions; - std::string additionalIncludePaths; - std::string entryPointSymbol; // TODO: use this - Standards::cppstd_t cppstd = Standards::CPPLatest; - }; + if (!importGraphChooseDecision(matched, branch)) + return ImportResult::Ok; + + // Re-locate the taken child by index rather than reusing a pointer from the + // scan above: on replay this file's XML may have been reloaded into a fresh + // tinyxml2::XMLDocument since the Properties pass ran (processImport() opens + // a new document per phase for every imported file), so any XMLElement* + // captured earlier would not be valid here. + std::size_t index = 0; + for (const tinyxml2::XMLElement *child = node->FirstChildElement(); child; child = child->NextSiblingElement(), ++index) { + if (index == branch) + return processElementChildren(child, baseDir, properties, metadata, compileList, projectConfigurationList, importStack, phase); + } + return ImportResult::Ok; +} - 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; +ImportProject::ImportResult ImportProject::processElementChildren(const tinyxml2::XMLElement *parent, + const std::string &baseDir, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack, + EvalPhase phase) { + ImportResult result = ImportResult::Ok; + + for (const tinyxml2::XMLElement *node = parent->FirstChildElement(); node; node = node->NextSiblingElement()) { + if (hasName(node, "PropertyGroup", properties)) { + // Properties pass only: by the time ItemDefs/Items run, every property in + // the whole resolved graph is already final in `properties` -- re-running + // PropertyGroup assignment then would be redundant, and evaluating it + // against final rather than as-accumulated properties could even produce a + // different value than what Properties actually computed. + // Also applied directly under EvalPhase::Discover: the discovery bootstrap + // scan in importVcxproj() walks its document through this same function + // (so // are handled identically to the real + // passes), and unlike an imported file reached via processImportProject() + // -- which converts Discover to Properties one level down before recursing + // -- this is the top-level entry point, so Discover itself must unlock + // PropertyGroup here or its properties would never be seeded at all. + if (phase == EvalPhase::Properties || phase == EvalPhase::Discover) { + for (const tinyxml2::XMLElement *child = node->FirstChildElement(); child; child = child->NextSiblingElement()) + addProperty(child, properties); + } + } else if (hasName(node, "ItemDefinitionGroup", properties)) { + // ItemDefs pass only, using the final properties Properties has already + // computed -- matching MSBuild's own item-definition evaluation, which runs + // as a separate pass over the whole graph after property evaluation. + if (phase == EvalPhase::ItemDefs) { + // Evaluate metadata defaults sequentially to capture preceding overrides. + // Structural name check only (hasElementName(), not hasName()): Visual + // Studio does not support Condition on the ClCompile item-definition + // element itself, only on its metadata children -- see hasElementName(). + for (const tinyxml2::XMLElement *item = node->FirstChildElement(); item; item = item->NextSiblingElement()) { + if (!hasElementName(item, "ClCompile")) + continue; + + for (const tinyxml2::XMLElement *child = item->FirstChildElement(); child; child = child->NextSiblingElement()) + addMetadata(child, properties, metadata); } } - } + } else if (hasNameAndLabel(node, "ItemGroup", "ProjectConfigurations", properties)) { + // Idempotent (guarded by alreadyPresent below) and cheap, so it is left + // unconditional rather than restricted to one phase. + for (const tinyxml2::XMLElement *configuration = node->FirstChildElement("ProjectConfiguration"); configuration; configuration = configuration->NextSiblingElement("ProjectConfiguration")) { + const ProjectConfiguration pc(configuration); + if (pc.configuration.empty()) + continue; - bool useOfMfc = false; - bool useUnicode = false; - }; + const bool alreadyPresent = std::any_of(projectConfigurationList.cbegin(), + projectConfigurationList.cend(), + [&pc](const ProjectConfiguration &existing) { + return existing.name == pc.name; + }); - 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) + if (!alreadyPresent) { + projectConfigurationList.emplace_back(pc); + mAllVSConfigs.insert(pc.configuration); + } + } + } else if (hasNameAndNotLabel(node, "ItemGroup", "ProjectConfigurations", properties)) { + // Items pass only, using the final properties and item definitions Properties + // and ItemDefs have already computed -- matching MSBuild's own item + // evaluation, the last of its three passes over the whole graph. + if (phase == EvalPhase::Items) { + // Structural name check only (hasElementName(), not hasName()): Visual + // Studio's C++ project system does not support Condition on a ClCompile + // project item itself -- only on its metadata children (see + // hasElementName()) -- so a Condition here must not hide the item. + for (const tinyxml2::XMLElement *item = node->FirstChildElement(); item; item = item->NextSiblingElement()) { + if (!hasElementName(item, "ClCompile")) continue; - mConditions.emplace_back(condition); + + if (item->Attribute("Include")) + processCompile(item, baseDir, properties, metadata, compileList); + else if (item->Attribute("Update")) + applyClCompileUpdate(item, baseDir, properties, compileList); + else if (item->Attribute("Remove")) + applyClCompileRemove(item, baseDir, properties, compileList); } - // TODO: ForcedIncludeFiles and PrecompiledHeaderFile } - } - 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; + } else if (hasElementName(node, "ImportGroup")) { + // An 's own Condition gates every import inside it, so it is an + // import-graph branch point exactly like an individual and must be + // decided/replayed the same way (see importGraphDecision()) -- ImportGroup + // itself never resolves to a file, so the frozen-file half is unused. + std::string unusedFile; + if (importGraphDecision(conditionIsTrue(node, properties), unusedFile)) { + const ImportResult importResult = processImportGroup(node, baseDir, properties, metadata, compileList, projectConfigurationList, importStack, phase); + result = std::max(result, importResult); } - return false; + } else if (hasElementName(node, "Choose")) { + // 's branch selection is an import-graph branch point exactly + // like / -- see processChoose() and + // importGraphChooseDecision() for why it must be decided once + // (Properties) and replayed (ItemDefs/Items) rather than + // re-evaluated every phase. + const ImportResult chooseResult = processChoose(node, baseDir, properties, metadata, compileList, projectConfigurationList, importStack, phase); + result = std::max(result, chooseResult); + } else { + // processImportProject() itself checks whether `node` is structurally an + // element (safely no-opping otherwise) and, when it + // is, decides/replays whether it is taken (it needs to freeze the resolved + // file, not just the true/false). + const ImportResult importResult = processImportProject(node, baseDir, properties, metadata, compileList, projectConfigurationList, importStack, phase); + result = std::max(result, importResult); } - std::string mFilename; - std::list mConditions; - }; + } + + return result; } -static std::list toStringList(const std::string &s) +// Returns whether `name` matches the '*'/'?' wildcard `pattern` (case-insensitive, +// matching NTFS semantics -- Visual Studio's C++ project system is a Windows-only +// concept). '*' matches any run of characters (including none); '?' matches +// exactly one character. Neither argument is expected to contain a path +// separator -- this matches one filename against one pattern within a single +// directory, not a path. Standard greedy iterative glob match. +static bool matchesWildcardName(const std::string &name, const std::string &pattern) { - 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; + std::size_t n = 0, p = 0; + std::size_t star = std::string::npos, matchPos = 0; + while (n < name.size()) { + if (p < pattern.size() && + (pattern[p] == '?' || + std::tolower(static_cast(pattern[p])) == std::tolower(static_cast(name[n])))) { + ++n; + ++p; + } else if (p < pattern.size() && pattern[p] == '*') { + star = p++; + matchPos = n; + } else if (star != std::string::npos) { + p = star + 1; + n = ++matchPos; + } else { + return false; + } } - if (pos1 < s.size()) - ret.push_back(s.substr(pos1)); - return ret; + while (p < pattern.size() && pattern[p] == '*') + ++p; + return p == pattern.size(); } -static void importPropertyGroup(const tinyxml2::XMLElement *node, std::map &variables, std::string &includePath) +// Lists the regular files (not subdirectories) directly inside `dir` (no +// trailing separator required, '/' separators only). Returns an empty list if +// `dir` does not exist or cannot be opened -- a wildcard matching +// nothing is a silent no-op, exactly like MSBuild's own ImportBefore/ImportAfter +// extensibility folders when nothing has been dropped into them. Not recursive: +// MSBuild's own wildcard Import examples (e.g. "ImportBefore\*") only ever +// reach into one directory, matching the non-recursive '*' used elsewhere in +// this file (see expandItemSpec()'s rejection of '*'/'?' in project items). +#ifdef _WIN32 +static std::vector listDirectoryFiles(const std::string &dir) { - 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; - } - - } 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); - } + std::vector files; + const std::string searchPath = (dir.empty() ? std::string(".") : dir) + "/*"; + WIN32_FIND_DATAA findData; + const HANDLE hFind = FindFirstFileA(searchPath.c_str(), &findData); + if (hFind == INVALID_HANDLE_VALUE) + return files; + do { + if (!(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + files.emplace_back(findData.cFileName); + } while (FindNextFileA(hFind, &findData)); + FindClose(hFind); + return files; +} +#else +static std::vector listDirectoryFiles(const std::string &dir) +{ + std::vector files; + const std::string path = dir.empty() ? std::string(".") : dir; + DIR * const dp = opendir(path.c_str()); + if (!dp) + return files; + while (const struct dirent * const entry = readdir(dp)) { + const std::string name = entry->d_name; + if (name == "." || name == "..") + continue; + if (Path::isDirectory(path + "/" + name)) + continue; + files.push_back(name); } + closedir(dp); + return files; } +#endif -static void loadVisualStudioProperties(const std::string &props, std::map &variables, std::string &includePath, const std::string &additionalIncludeDirectories, std::list &itemDefinitionGroupList) -{ - std::string filename(props); - // variables can't be resolved - if (!simplifyPathWithVariables(filename, variables)) - return; +ImportProject::ImportResult ImportProject::processImport(const std::string &file, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack, + EvalPhase phase) { + std::string filename(file); + // file 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); + // Use classifyPath so that root-relative paths (\foo -> C:\foo) are resolved + // against the base drive, not treated as absolute on Linux. + const PathKind _fkind = classifyPath(Path::fromNativeSeparators(filename)); + if (_fkind != PathKind::UNC && _fkind != PathKind::DriveAbsolute && properties.count("ProjectDir") > 0) + filename = toAbsolute(filename, properties.at("ProjectDir"), properties); + + // MSBuild allows wildcards in an 's Project attribute -- unlike + // project items (see expandItemSpec()'s rejection of '*'/'?' there, a + // *different*, narrower Visual Studio C++ project-system restriction that + // does not apply to imports): "When there are wildcards, all matches found + // are sorted (for reproducibility), and then they are imported in that + // order as if the order had been explicitly set." This is the mechanism + // behind the ImportBefore/ImportAfter extensibility folders Visual + // Studio's own C++ toolchain files use (e.g. + // $(VCTargetsPath)\ImportBefore\Default\*.props), but it applies equally + // to any ordinary user-authored wildcard . + const std::string::size_type lastSlash = filename.find_last_of('/'); + const std::string patternPart = (lastSlash != std::string::npos) ? filename.substr(lastSlash + 1) : filename; + if (patternPart.find('*') != std::string::npos || patternPart.find('?') != std::string::npos) { + const std::string dirPart = (lastSlash != std::string::npos) ? filename.substr(0, lastSlash) : std::string(); + std::vector matches; + for (const std::string &candidate : listDirectoryFiles(dirPart)) { + if (matchesWildcardName(candidate, patternPart)) + // cppcheck-suppress useStlAlgorithm + matches.push_back(candidate); + } + std::sort(matches.begin(), matches.end()); + ImportResult result = ImportResult::Ok; + for (const std::string &match : matches) { + const ImportResult r = processImport(dirPart + "/" + match, properties, metadata, compileList, + projectConfigurationList, importStack, phase); + result = std::max(result, r); + } + return result; + } + + const std::string key = importFileKey(filename); + // Detect circular imports before duplicate-import suppression. + if (!importStack.insert(key).second) + return ImportResult::Cycle; + + ImportStackGuard guard(importStack, key); + // A previously completed import is ignored. + if (mImportGraph.active && !mImportGraph.imported.insert(key).second) + return ImportResult::Ok; tinyxml2::XMLDocument doc; - if (doc.LoadFile(filename.c_str()) != tinyxml2::XML_SUCCESS) - return; + + const tinyxml2::XMLError xmlErr = doc.LoadFile(filename.c_str()); + if (xmlErr != tinyxml2::XML_SUCCESS) { + if (xmlErr == tinyxml2::XML_ERROR_FILE_NOT_FOUND || + xmlErr == tinyxml2::XML_ERROR_FILE_COULD_NOT_BE_OPENED || + xmlErr == tinyxml2::XML_ERROR_FILE_READ_ERROR) + return ImportResult::NotFound; + return ImportResult::NotValid; // file exists but is malformed XML + } + const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement(); if (rootnode == nullptr) - return; - 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); - } - } - } else if (std::strcmp(name,"PropertyGroup")==0) { - importPropertyGroup(node, variables, includePath); - } else if (std::strcmp(name,"ItemDefinitionGroup")==0) { - itemDefinitionGroupList.emplace_back(node, additionalIncludeDirectories); - } - } + return ImportResult::NotValid; + + // Every import-graph branch point encountered while walking this file's children + // (import decisions or replays) is keyed by this file, so ItemDefs/Items replay + // the exact sequence Properties recorded for it. + CurrentFileGuard fileGuard(mImportGraph.currentFile, key); + + MSBuildThis msBuildThis(filename, properties); + std::string propsDir = Path::getPathFromFilename(filename); + + return processElementChildren(rootnode, propsDir, properties, metadata, compileList, + projectConfigurationList, importStack, phase); } 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 +4585,656 @@ bool ImportProject::importVcxproj(const std::string &filename, errors.emplace_back(std::string("Visual Studio project file is not a valid XML - ") + tinyxml2::XMLDocument::ErrorIDToName(error)); return false; } - return importVcxproj(filename, doc, variables, additionalIncludeDirectories, fileFilters, cache); -} -bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::XMLDocument &doc, std::map &variables, const std::string &additionalIncludeDirectories, const std::vector &fileFilters, std::vector &cache) -{ - variables["ProjectDir"] = Path::simplifyPath(Path::getPathFromFilename(filename)); + // 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)); + + // A solution import may already provide VisualStudioVersion. + // Direct .vcxproj imports have no solution header, so use the current + // supported Visual Studio version as the fallback. + properties.emplace("VisualStudioVersion", "18.0"); + + // User-extensible MSVC properties that legitimately default to "" when not + // set by the project. In real MSBuild every undefined property expands to + // the empty string; seed them here so that props files which append to them + // (e.g. ...;$(DisableSpecificWarnings)) + // produce a clean resolved value instead of leaving $(X) unexpanded. + properties.emplace("DisableSpecificWarnings", ""); + + properties["ProjectPath"] = nfilename; + const std::string projFileName = stripDirectoryPart(nfilename); + properties["ProjectFileName"] = projFileName; + const std::string projName = fileStem(projFileName); + properties["ProjectName"] = projName; + properties["ShortProjectName"] = projName.substr(0, std::min(projName.size(), std::size_t(16))); + 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"]; + } + 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"]; + + // TargetName defaults to ProjectName; can be overridden by the XML + properties.emplace("TargetName", properties["ProjectName"]); + + // MSBuildProjectDirectoryNoRoot: like MSBuildThisFileDirectoryNoRoot but for the + // project itself. ProjectDir has a trailing '/' which we strip to match the + // MSBuildProjectDirectory (no trailing separator) convention. + std::string noRoot = stripPathRoot(properties["ProjectDir"]); + if (!noRoot.empty() && noRoot.back() == '/') + noRoot.pop_back(); + properties["MSBuildProjectDirectoryNoRoot"] = noRoot; + + properties["BuildingInsideVisualStudio"] = "true"; + properties["DesignTimeBuild"] = "true"; + + 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; + MetadataMap metadata; const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement(); if (rootnode == nullptr) { errors.emplace_back("Visual Studio project file has no XML root node"); return false; } - 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); - } - } - } - } - } 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); - } - } else if (std::strcmp(name, "ImportGroup") == 0) { - 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 (std::strcmp(e->Name(), "Import") == 0) { - const char *projectAttribute = e->Attribute("Project"); - if (projectAttribute) - loadVisualStudioProperties(projectAttribute, variables, includePath, additionalIncludeDirectories, itemDefinitionGroupList); - } - } - } else if (labelAttribute && std::strcmp(labelAttribute, "Shared") == 0) { - for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (std::strcmp(e->Name(), "Import") == 0) { - const char *projectAttribute = e->Attribute("Project"); - if (projectAttribute) { - // Path to shared items project is relative to current project directory, - // unless the string starts with $(SolutionDir) - std::string pathToSharedItemsFile; - if (std::string(projectAttribute).rfind("$(SolutionDir)", 0) == 0) { - pathToSharedItemsFile = projectAttribute; - } else { - pathToSharedItemsFile = variables["ProjectDir"] + projectAttribute; - } - if (!simplifyPathWithVariables(pathToSharedItemsFile, variables)) { - errors.emplace_back("Could not simplify path to referenced shared items project"); - return false; - } - SharedItemsProject toAdd = importVcxitems(pathToSharedItemsFile, fileFilters, cache); - if (!toAdd.successful) { - errors.emplace_back("Could not load shared items project \"" + pathToSharedItemsFile + "\" from original path \"" + std::string(projectAttribute) + "\"."); - return false; - } - sharedItemsProjects.emplace_back(toAdd); - } - } + // 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()) { + 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); } } } } - // # 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); + // Discovery pass: if no ProjectConfigurations were found inline in the vcxproj, walk + // the whole document through processElementChildren -- the same dispatch the real + // per-configuration passes use -- so every construct that can contain or gate a + // configuration-defining import (PropertyGroup, ImportGroup, a bare Import, and + // Choose/When/Otherwise, at any depth reached through them) is honoured generically, + // no special-casing of individual property names or node kinds required. + // + // While mDiscovering is set (see its doc comment and DiscoveringGuard), + // conditionIsTrue() treats every Condition as satisfied and processChoose() + // explores every branch instead of selecting one: real MSBuild/Visual Studio must + // know the complete configuration set before evaluation with a specific + // Configuration/Platform can even begin, so this scan does not try to predict + // which Condition would hold for values it is itself trying to discover -- it + // simply assumes every path might contribute a configuration and walks all of + // them. Configurations can legitimately be split across multiple imports (e.g. + // one property sheet per configuration, each behind its own Condition, or behind + // a Choose), and both stopping early and guessing a single property value would + // silently lose configurations reachable only a different way. + // + // 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 -- only the + // configurations themselves (accumulated into projectConfigurationList/mAllVSConfigs) + // persist beyond this block. An extra configuration this over-inclusive scan finds + // either genuinely exists or simply picks up no items during the real pass (which + // always evaluates every Condition properly, against that configuration's real + // properties) and costs nothing. + if (projectConfigurationList.empty()) { + PropertiesMap discoverProps = properties; + // Seed properties that would otherwise be unknown at discovery time. This is + // more than cosmetic debug-noise suppression: simplifyPathWithVariables() + // treats any path with a surviving unexpanded $(...) as unresolvable, so an + // import whose PATH ITSELF embeds one of these -- e.g. + // , with no Condition or + // Choose gating it at all -- would otherwise fail to resolve and its + // configuration would be missed entirely, not just logged. Conditions + // themselves no longer need a guessed value to select a branch (mDiscovering + // ignores them, see above), but property-VALUE expansion is unaffected by + // that and still depends on one. These only affect the isolated discovery + // copy -- the real per-config pass uses the unmodified properties map. + discoverProps.emplace("Platform", "x64"); + discoverProps.emplace("Configuration", "Debug"); + // Name-mismatched env vars (same-name ones are auto-resolved by isKnown). + auto discoverSeedEnv = [&](const char *prop, const char *envVar) { + const char *val = std::getenv(envVar); + discoverProps.emplace(prop, val ? val : ""); + }; + discoverSeedEnv("VsInstallRoot", "VSINSTALLDIR"); + discoverSeedEnv("VsInstallDir", "VSINSTALLDIR"); + discoverSeedEnv("VCInstallDir", "VCINSTALLDIR"); + discoverSeedEnv("MSBuildProgramFiles32", "ProgramFiles(x86)"); + discoverSeedEnv("WindowsSdkDir_10", "WindowsSdkDir"); + // MSBuild-internal properties with no env var equivalent. + discoverProps.emplace("VC_LibraryPath_x64", ""); + discoverProps.emplace("WindowsSDK_WindowsMetadata", ""); + discoverProps.emplace("WindowsSDK_LibraryPath_x64", ""); + MetadataMap discoverMeta; + std::list discoverCompile; + std::unordered_set discoverStack; + + DiscoveringGuard discoveringGuard(mDiscovering); + processElementChildren(rootnode, projectDir, discoverProps, discoverMeta, discoverCompile, + projectConfigurationList, discoverStack, EvalPhase::Discover); + } + + PropertiesMap originalVariables = properties; + + // Visual Studio (like MSBuild) evaluates a project in three passes over the + // whole resolved import graph: every PropertyGroup/Import first, so every + // property -- wherever in the graph it is set -- is final before anything + // else runs, then every ItemDefinitionGroup, then every ItemGroup. See the + // EvalPhase doc comment and importGraphDecision() for the full rationale. + const std::string rootKey = importFileKey(nfilename); + + // Prime mConfigInvariantProperties: an isolated Properties-only walk per + // configuration (same idea as the discovery bootstrap above -- scratch + // copies of properties/metadata/import state so nothing here bleeds into + // the real per-configuration passes below), just to learn which property + // names resolve to the same value in every configuration this project + // has. expandItemSpec() (via hasOnlyInvariantItemPathVariables()) treats + // those, in addition to the fixed MSBuild "this file"/"this project" set, + // as safe to expand in a project item path -- see mConfigInvariantProperties's + // doc comment in importproject.h for why that's the right line to draw. + // With zero or one configuration every property trivially qualifies (there + // is nothing for it to vary across), which is correct: a project that + // only ever builds one way can't run into the per-configuration mismatch + // the underlying Visual Studio restriction is about. + mConfigInvariantProperties.clear(); + { + // Run the priming Properties pass once per configuration, keeping + // each configuration's full resulting property map -- needed below + // to tell "this property was never set by any PropertyGroup this + // project has" (fine: hasOnlyInvariantItemPathVariables() falls + // back to checking the OS environment for those, independently of + // mConfigInvariantProperties) apart from "this property is set in + // SOME of this project's configurations but not others" (not fine: + // that is itself a value that varies by configuration -- see below). + std::vector perConfigProperties; + perConfigProperties.reserve(projectConfigurationList.size()); + for (const ProjectConfiguration &primePc : projectConfigurationList) { + PropertiesMap primeProperties = originalVariables; + primeProperties["Configuration"] = primePc.configuration; + primeProperties["Platform"] = primePc.platformStr; + + MetadataMap primeMetadata; + std::list primeCompileList; + std::unordered_set primeImportStack; + + mImportGraph = ImportGraph(); + mImportGraph.active = true; + mImportGraph.currentFile = rootKey; + mImportGraph.imported.insert(rootKey); + + processElementChildren(rootnode, projectDir, primeProperties, primeMetadata, primeCompileList, + projectConfigurationList, primeImportStack, EvalPhase::Properties); + + perConfigProperties.push_back(std::move(primeProperties)); + } + + // Case-insensitive for the same reason mConfigInvariantProperties + // itself is (see its doc comment in importproject.h): each + // perConfigProperties entry above is itself a PropertiesMap, so + // within one configuration "MyRoot" and "myroot" already collapse + // to a single entry, but without this comparator here two DIFFERENT + // configurations spelling the same property differently -- one + // PropertyGroup writing "MyRoot", another "myroot" -- would be + // tracked as two unrelated single-value names instead of one + // property with (potentially) two different values, and each could + // wrongly look config-invariant on its own. + // + // The union of property names across every configuration -- not + // just each configuration's own names -- matters just as much: a + // property some, but not all, configurations set (e.g. a PropertyGroup + // conditioned on Configuration=='Debug' with no Release counterpart) + // is genuinely config-varying, exactly like real MSBuild treats it, + // since $(MyRoot) resolves to that PropertyGroup's value under Debug + // and to "" (PropertyValueExpander::lookup()'s own fallback for a + // name no PropertyGroup and no OS environment variable defines -- + // see hasOnlyInvariantItemPathVariables()'s separate getenv() check + // for the case where an environment variable of the same name DOES + // make it invariant regardless) under every configuration that never + // sets it. Naively collecting only the values each configuration's + // own map happens to contain would miss this entirely: a property + // set in exactly one configuration would have exactly one recorded + // value there -- itself -- and look spuriously invariant. + std::set allPropertyNames; + for (const auto &configProperties : perConfigProperties) { + for (const auto &prop : configProperties) + // cppcheck-suppress useStlAlgorithm + allPropertyNames.insert(prop.first); + } + std::map, cppcheck::stricmp> valuesByProperty; + for (const auto &configProperties : perConfigProperties) { + for (const auto &name : allPropertyNames) { + const auto it = configProperties.find(name); + valuesByProperty[name].insert(it != configProperties.end() ? it->second : std::string()); + } } - for (const auto &p : sharedProject.includePaths) { - std::string path = Path::simplifyPath(Path::getPathFromFilename(sharedProject.pathToProjectFile) + p); - sharedItemsIncludePaths.emplace_back(std::move(path)); + for (const auto &prop : valuesByProperty) { + if (prop.second.size() == 1) + mConfigInvariantProperties.insert(prop.first); } } - // Project files - PathMatch filtermatcher(fileFilters, Path::getCurrentPath()); - for (const ItemGroupClCompile& compile : compileList) { - if (!fileFilters.empty() && !filtermatcher.match(compile.mFilename)) - continue; + bool first = true; + + for (const ProjectConfiguration &pc : projectConfigurationList) { + if (!first) { + compileList.clear(); + properties = originalVariables; + metadata.clear(); + importStack.clear(); + } else + first = false; + + properties["Configuration"] = pc.configuration; + properties["Platform"] = pc.platformStr; + + // Pass 1 (Properties): decide, for every import-graph branch point, whether it + // is taken -- using the properties known at that exact point in the walk -- + // and record the decision sequence per file. + mImportGraph = ImportGraph(); + mImportGraph.active = true; + mImportGraph.currentFile = rootKey; + mImportGraph.imported.insert(rootKey); + + const ImportResult propertiesResult = processElementChildren(rootnode, projectDir, properties, metadata, + compileList, projectConfigurationList, importStack, + EvalPhase::Properties); + if (propertiesResult > ImportResult::NotResolvable) + addDebug("Could not fully evaluate \"" + nfilename + "\" - " + importResultStr(propertiesResult)); + + // Pass 2 (ItemDefs) and pass 3 (Items) walk the identical document structure + // again but *replay* the decisions pass 1 recorded instead of re-evaluating + // Condition -- re-checking against the now-final properties could produce a + // different graph than pass 1 built, which would desync the properties pass 1 + // computed from the item definitions/items passes 2/3 evaluate. Which files + // get (re-)visited is an automatic, consistent consequence of replaying the + // same decisions in the same order, so only 'imported' and 'cursor' -- not + // 'decisions' itself -- reset between passes. + mImportGraph.replay = true; + + mImportGraph.imported.clear(); + mImportGraph.imported.insert(rootKey); + mImportGraph.cursor.clear(); + importStack.clear(); + + processElementChildren(rootnode, projectDir, properties, metadata, + compileList, projectConfigurationList, importStack, + EvalPhase::ItemDefs); + + mImportGraph.imported.clear(); + mImportGraph.imported.insert(rootKey); + mImportGraph.cursor.clear(); + importStack.clear(); + + const ImportResult itemsResult = processElementChildren(rootnode, projectDir, properties, metadata, + compileList, projectConfigurationList, importStack, + EvalPhase::Items); + if (itemsResult > ImportResult::NotResolvable) + addDebug("Could not fully evaluate \"" + nfilename + "\" - " + importResultStr(itemsResult)); + + // # TODO: support signedness of char via /J (and potential XML option for it)? + // we can only set it globally but in this context it needs to be treated per file + + // Project files + PathMatch filtermatcher(fileFilters, Path::getCurrentPath()); + for (const ItemGroupClCompile &compile : compileList) { + if (!fileFilters.empty() && !filtermatcher.match(compile.filename)) + continue; - for (const ProjectConfiguration &p : projectConfigurationList) { + const std::string &excl = compile.get("ExcludedFromBuild"); + if (!excl.empty() && caseInsensitiveStringCompare(excl, "true") == 0) + 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; - // TODO: detect actual MSC version + FileSettings fs{ compile.filename, Standards::Language::None, 0 }; // file will be identified later on + fs.cfg = pc.name; 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) { + // MSVC always defines _M_IX86 for x86 targets; 600 = Pentium Pro / modern default. + fs.defines += ";_M_IX86=600"; + } else if (pc.platform == ProjectConfiguration::x64) { fs.platformType = Platform::Type::Win64; fs.defines += ";_WIN64=1"; + // MSVC defines both _M_X64 and _M_AMD64 (both == 100) for x64 targets. + fs.defines += ";_M_X64=100;_M_AMD64=100"; + } else if (pc.platform == ProjectConfiguration::ARM64) { + fs.platformType = Platform::Type::WinARM64; + fs.defines += ";_M_ARM64=1"; + } else if (pc.platform == ProjectConfiguration::ARM64EC) { + // ARM64EC is an x64-ABI on ARM64 hardware (VS 2022+). + // MSVC defines _M_ARM64EC and the two x64 macros for this target. + fs.platformType = Platform::Type::WinARM64EC; + fs.defines += ";_WIN64=1"; + fs.defines += ";_M_ARM64EC=1;_M_X64=100;_M_AMD64=100"; + } else if (pc.platform == ProjectConfiguration::ARM) { + fs.platformType = Platform::Type::WinARM; + // MSVC defines _M_ARM=7 (Thumb-2 instruction set) for ARM targets. + fs.defines += ";_M_ARM=7"; } - 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; + + // Currently selects C or C++ from the file extension. + bool isCFile = Path::getFilenameExtensionInLowerCase(compile.filename) == ".c"; + const std::string &compileAs = compile.get("CompileAs"); + if (caseInsensitiveStringCompare(compileAs, "CompileAsC") == 0) + isCFile = true; + else if (caseInsensitiveStringCompare(compileAs, "CompileAsCpp") == 0) + isCFile = false; + if (isCFile) { + Standards::cstd_t cstd = Standards::C17; + const std::string &languageStandardC = compile.get("LanguageStandard_C"); + if (caseInsensitiveStringCompare(languageStandardC, "stdc11") == 0) + cstd = Standards::C11; + else if (caseInsensitiveStringCompare(languageStandardC, "stdc17") == 0) + cstd = Standards::C17; + else if (caseInsensitiveStringCompare(languageStandardC, "stdclatest") == 0) + cstd = Standards::CLatest; + fs.standard = Standards::getC(cstd); + } else { + Standards::cppstd_t cppstd = Standards::CPP14; + const std::string &languageStandard = compile.get("LanguageStandard"); + if (caseInsensitiveStringCompare(languageStandard, "stdcpp11") == 0) + cppstd = Standards::CPP11; + else if (caseInsensitiveStringCompare(languageStandard, "stdcpp14") == 0) + cppstd = Standards::CPP14; + else if (caseInsensitiveStringCompare(languageStandard, "stdcpp17") == 0) + cppstd = Standards::CPP17; + else if (caseInsensitiveStringCompare(languageStandard, "stdcpp20") == 0) + cppstd = Standards::CPP20; + else if (caseInsensitiveStringCompare(languageStandard, "stdcpp23") == 0) + cppstd = Standards::CPP23; + else if (caseInsensitiveStringCompare(languageStandard, "stdcpplatest") == 0) + cppstd = Standards::CPPLatest; + fs.standard = Standards::getCPP(cppstd); } - if (useUnicode) { - fs.defines += ";UNICODE=1;_UNICODE=1"; + + // Inject _MSC_VER and _MSC_FULL_VER derived from PlatformToolset. + // Without these, standard-library and Windows SDK headers (, + // ) use generic fallbacks, fail to compile, or misidentify + // the supported language standard. + std::string mscVer = "1950"; // VS 2026 fallback + std::string mscFullVer = "195000000"; + + // Prefer item-level override, then project property, then DefaultPlatformToolset. + std::string toolset = compile.get("PlatformToolset"); + if (toolset.empty()) { + const auto tsIt = properties.find("PlatformToolset"); + if (tsIt != properties.end()) + toolset = tsIt->second; + else { + const auto defIt = properties.find("DefaultPlatformToolset"); + if (defIt != properties.end()) + toolset = defIt->second; + } } - fsSetDefines(fs, fs.defines); - fsSetIncludePaths(fs, Path::getPathFromFilename(compile.mFilename), toStringList(includePath + ';' + additionalIncludePaths), variables); - for (const auto &path : sharedItemsIncludePaths) { - fs.includePaths.emplace_back(path); + + if (caseInsensitiveStringCompare(toolset, "v145") == 0) { // VS 2026 + mscVer = "1950"; + mscFullVer = "195000000"; + } else if (caseInsensitiveStringCompare(toolset, "v144") == 0) { // VS 2025 + mscVer = "1940"; + mscFullVer = "194000000"; + } else if (caseInsensitiveStringCompare(toolset, "v143") == 0) { // VS 2022 + mscVer = "1930"; + mscFullVer = "193000000"; + } else if (caseInsensitiveStringCompare(toolset, "v142") == 0) { // VS 2019 + mscVer = "1920"; + mscFullVer = "192000000"; + } else if (caseInsensitiveStringCompare(toolset, "v141") == 0) { // VS 2017 + mscVer = "1910"; + mscFullVer = "191000000"; + } else if (caseInsensitiveStringCompare(toolset, "v140") == 0) { // VS 2015 + mscVer = "1900"; + mscFullVer = "190000000"; + } else if (caseInsensitiveStringCompare(toolset, "v14") == 0) { + try { + const int sub = std::stoi(toolset.substr(3)); + mscVer = std::to_string(1900 + (sub * 10)); + mscFullVer = mscVer + "00000"; + } catch (...) {} + } else { + // Unknown or absent toolset: derive from VisualStudioVersion. + const auto vsIt = properties.find("VisualStudioVersion"); + if (vsIt != properties.end()) { + try { + const double vsVer = std::stod(vsIt->second); + if (vsVer >= 19.0) { + // future VS: keep the default (1940) as a safe floor + } else if (vsVer >= 18.0) { // VS 2026 + mscVer = "1950"; mscFullVer = "195000000"; + } else if (vsVer >= 17.0) { // VS 2025 + mscVer = "1940"; mscFullVer = "194000000"; + } else if (vsVer >= 16.0) { // VS 2022 + mscVer = "1930"; mscFullVer = "193000000"; + } else if (vsVer >= 15.0) { // VS 2019 + mscVer = "1920"; mscFullVer = "192000000"; + } else if (vsVer >= 14.0) { // VS 2017 + mscVer = "1910"; mscFullVer = "191000000"; + } + } catch (...) {} + } } - fileSettings.push_back(std::move(fs)); - } - } - return true; -} + fs.defines += ";_MSC_VER=" + mscVer + ";_MSC_FULL_VER=" + mscFullVer; + + // _MSVC_LANG mirrors the C++ standard flag. MSVC only defines this for + // C++ translation units; C files do not get it (even with /TC). + // Note: MSVC does NOT set __cplusplus to the standard value unless + // /Zc:__cplusplus is passed; code that needs the standard should test + // _MSVC_LANG, which is always set correctly. + if (!isCFile) { + std::string msvcLang = "201402L"; // MSVC default when no /std: flag is set + const std::string &languageStandard = compile.get("LanguageStandard"); + if (languageStandard == "stdcpp11") + msvcLang = "201103L"; + else if (languageStandard == "stdcpp14") + msvcLang = "201402L"; + else if (languageStandard == "stdcpp17") + msvcLang = "201703L"; + else if (languageStandard == "stdcpp20") + msvcLang = "202002L"; + else if (languageStandard == "stdcpp23") + msvcLang = "202302L"; + else if (languageStandard == "stdcpplatest") + msvcLang = "202604L"; // current C++26 draft baseline + fs.defines += ";_MSVC_LANG=" + msvcLang; + } -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; - } + const std::string enableEnhancedInstructionSet = compile.get("EnableEnhancedInstructionSet"); + if (caseInsensitiveStringCompare(enableEnhancedInstructionSet, "StreamingSIMDExtensions") == 0) { + if (pc.platform == ProjectConfiguration::Win32) + fs.defines += ";_M_IX86_FP=1"; + } else if (caseInsensitiveStringCompare(enableEnhancedInstructionSet, "StreamingSIMDExtensions2") == 0) { + if (pc.platform == ProjectConfiguration::Win32) + fs.defines += ";_M_IX86_FP=2"; + } else if (caseInsensitiveStringCompare(enableEnhancedInstructionSet, "AdvancedVectorExtensions") == 0) + fs.defines += ";__AVX__"; + else if (caseInsensitiveStringCompare(enableEnhancedInstructionSet, "AdvancedVectorExtensions2") == 0) + fs.defines += ";__AVX2__"; + else if (caseInsensitiveStringCompare(enableEnhancedInstructionSet, "AdvancedVectorExtensions512") == 0) + 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 (caseInsensitiveStringCompare(charSet, "Unicode") == 0) + fs.defines += ";UNICODE=1;_UNICODE=1"; + else if (caseInsensitiveStringCompare(charSet, "MultiByte") == 0) + fs.defines += ";_MBCS=1"; - SharedItemsProject result; - result.pathToProjectFile = filename; + const auto configurationTypeIt = properties.find("ConfigurationType"); + if (configurationTypeIt != properties.end() && + caseInsensitiveStringCompare(configurationTypeIt->second, "DynamicLibrary") == 0) { + fs.defines += ";_WINDLL"; + } - PathMatch filtermatcher(fileFilters, Path::getCurrentPath()); + if (useOfMfcIt != properties.end() && + caseInsensitiveStringCompare(useOfMfcIt->second, "Dynamic") == 0) { + fs.defines += ";_AFXDLL"; + } - 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); + const auto useOfAtlIt = properties.find("UseOfAtl"); + if (useOfAtlIt != properties.end() && + caseInsensitiveStringCompare(useOfAtlIt->second, "Dynamic") == 0) { + fs.defines += ";_ATL_DLL"; + } + + std::string runtimeLibrary; + const auto runtimeLibraryIt = properties.find("RuntimeLibrary"); + if (runtimeLibraryIt != properties.end()) + runtimeLibrary = runtimeLibraryIt->second; + + if (runtimeLibrary.empty()) { + const auto useDebugLibrariesIt = properties.find("UseDebugLibraries"); + const bool debug = useDebugLibrariesIt != properties.end() && + caseInsensitiveStringCompare(useDebugLibrariesIt->second, "true") == 0; + runtimeLibrary = debug ? "MultiThreadedDebugDLL" : "MultiThreadedDLL"; + } + + if (caseInsensitiveStringCompare(runtimeLibrary, "MultiThreadedDebugDLL") == 0) + fs.defines += ";_MT;_DLL;_DEBUG"; + else if (caseInsensitiveStringCompare(runtimeLibrary, "MultiThreadedDLL") == 0) + fs.defines += ";_MT;_DLL"; + else if (caseInsensitiveStringCompare(runtimeLibrary, "MultiThreadedDebug") == 0) + fs.defines += ";_MT;_DEBUG"; + else if (caseInsensitiveStringCompare(runtimeLibrary, "MultiThreaded") == 0) + fs.defines += ";_MT"; + + std::string defines = fs.defines; + if (!compile.get("PreprocessorDefinitions").empty()) + defines += (";" + compile.get("PreprocessorDefinitions")); + const std::string &undefStr = compile.get("UndefinePreprocessorDefinitions"); + if (!undefStr.empty()) { + // Build a set of macro names to suppress (skip %(InheritedValues) tokens). + std::set undefs; + std::string seg; + for (std::size_t i = 0; i <= undefStr.size(); ++i) { + const char c = (i < undefStr.size()) ? undefStr[i] : ';'; + if (c == ';') { + if (!seg.empty() && !startsWith(seg, "%(")) + undefs.insert(seg); + seg.clear(); + } else { + seg += c; + } + } + // Remove matching entries from the accumulated defines string. + std::string filtered; + seg.clear(); + for (std::size_t i = 0; i <= defines.size(); ++i) { + const char c = (i < defines.size()) ? defines[i] : ';'; + if (c == ';') { + if (!seg.empty()) { + const std::string name = seg.substr(0, seg.find('=')); + if (undefs.find(name) == undefs.end()) { + if (!filtered.empty()) + filtered += ';'; + filtered += seg; + } + seg.clear(); + } } else { - errors.emplace_back("Could not find shared items source file"); - return result; + seg += c; } } + defines = std::move(filtered); } - } else if (std::strcmp(node->Name(), "ItemDefinitionGroup") == 0) { - ItemDefinitionGroup temp(node, ""); - for (const auto& includePath : toStringList(temp.additionalIncludePaths)) { - if (includePath == "%(AdditionalIncludeDirectories)") + fsSetDefines(fs, defines); + fsSetIncludePaths(fs, projectDir, toStringList(propertyOrEmpty(properties, "IncludePath")), properties); + + std::string rawAdditionalIncludes = compile.get("AdditionalIncludeDirectories"); + expandMSBuildVariables(rawAdditionalIncludes, properties); // Pre-expand so toStringList splits them cleanly! + + fs.systemIncludePaths = std::move(fs.includePaths); + fsSetIncludePaths(fs, projectDir, toStringList(rawAdditionalIncludes), properties); + + std::string rawForcedIncludes = compile.get("ForcedIncludeFiles"); + expandMSBuildVariables(rawForcedIncludes, properties); + + fs.forcedIncludes.clear(); + for (const std::string &forcedInclude : toStringList(rawForcedIncludes)) { + if (forcedInclude.empty()) continue; - std::string toAdd(includePath); - findAndReplace(toAdd, "$(MSBuildThisFileDirectory)", "./"); - result.includePaths.emplace_back(toAdd); + // Standardize native backslashes to uniform forward slashes + const std::string normalized = Path::fromNativeSeparators(forcedInclude); + const bool relative = isRelative(normalized); + std::string resolved; + + if (!relative) { + // Step 2a: For absolute paths, clean up dot notation and simplify structures + resolved = normalized; + simplifyPathWithVariables(resolved, properties); + } else { + // Relative paths - Search prioritized include paths FIRST (Visual Studio Priority) + bool matchFound = false; + bool isDir = false; + + for (const std::string &includePath : fs.includePaths) { + std::string candidate = toAbsolute(normalized, includePath, properties); + + // Pass the address of isDir to explicitly verify the path is a FILE, never a directory + if (Path::exists(candidate, &isDir) && !isDir) { + resolved = std::move(candidate); + matchFound = true; + break; + } + } + + // Step 3: Fall back to checking the Project Root Folder only if no include paths matched + if (!matchFound) { + resolved = toAbsolute(normalized, projectDir, properties); + } + } + + // Append the safely validated and prioritized file path + fs.forcedIncludes.push_back(std::move(resolved)); } + + fileSettings.push_back(std::move(fs)); } } - result.successful = true; - cache.emplace_back(result); - return result; + mImportGraph = ImportGraph(); // deactivate: later imports evaluate conditions live + + return true; } bool ImportProject::importBcb6Prj(const std::string &projectFilename) @@ -1427,17 +5471,17 @@ 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 std::string cppDefines = cppPredefines + ";" + defines; const bool forceCppMode = (cflags.find("-P") != cflags.end()); for (const std::string &c : compileList) { @@ -1452,8 +5496,11 @@ bool ImportProject::importBcb6Prj(const std::string &projectFilename) // We can also force C++ compilation for all files using the -P command line switch. 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); + // Use classifyPath so that root-relative source paths (\src\foo.cpp) are + // resolved against projectDir's drive rather than passed through as-is. + const PathKind _ck = classifyPath(Path::fromNativeSeparators(c)); + FileSettings fs{Path::simplifyPath((_ck == PathKind::UNC || _ck == PathKind::DriveAbsolute) ? c : projectDir + c), Standards::Language::None, 0}; // file will be identified later on + fsSetIncludePaths(fs, projectDir, toStringList(includePath), properties); fsSetDefines(fs, cppMode ? cppDefines : defines); fileSettings.push_back(std::move(fs)); } @@ -1463,8 +5510,20 @@ bool ImportProject::importBcb6Prj(const std::string &projectFilename) static std::string joinRelativePath(const std::string &path1, const std::string &path2) { - if (!path1.empty() && !Path::isAbsolute(path2)) - return path1 + path2; + if (!path1.empty()) { + // Classify path2 to decide whether to prepend path1. + // Use the original string (before fromNativeSeparators) so we can tell a + // Windows root-relative path "\foo" (starts with backslash) from a + // genuine Unix absolute path "/foo" (starts with forward slash): + // - UNC (\\server or //server) and DriveAbsolute (C:\...) are always absolute. + // - A native forward-slash start means Unix-absolute ("/foo") or UNC ("//"). + // - A native backslash start means Windows root-relative ("\foo"), which on + // Linux after fromNativeSeparators would look like "/foo" but is NOT absolute. + const bool nativelyAbsolute = !path2.empty() && path2[0] == '/'; + const PathKind pk = classifyPath(Path::fromNativeSeparators(path2)); + if (!nativelyAbsolute && pk != PathKind::UNC && pk != PathKind::DriveAbsolute) + return path1 + path2; + } return path2; } @@ -1723,11 +5782,23 @@ 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::WinARM64EC && fs.platformType != Platform::Type::WinARM64EC) + remove = true; + 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) + else if ((platform == Platform::Type::Win32A || platform == Platform::Type::Win32W) && + (fs.platformType == Platform::Type::Win64 || + fs.platformType == Platform::Type::WinARM64 || + fs.platformType == Platform::Type::WinARM64EC || + fs.platformType == Platform::Type::WinARM)) remove = true; else if (filenames.find(fs.filename()) != filenames.end()) remove = true; @@ -1752,9 +5823,19 @@ 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::Win32A || platform == Platform::Type::Win32W) && fs.platformType == Platform::Type::Win64) + else if (platform == Platform::Type::WinARM64 && fs.platformType != Platform::Type::WinARM64) + remove = true; + else if (platform == Platform::Type::WinARM64EC && fs.platformType != Platform::Type::WinARM64EC) + remove = true; + 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::WinARM64EC || + fs.platformType == Platform::Type::WinARM)) remove = true; if (remove) { it = fileSettings.erase(it); @@ -1780,16 +5861,72 @@ 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); + ImportProject project; + 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). + ImportProject::ConditionParser parser(project, condition, properties); + return parser.parse(); +} + +// cppcheck-suppress unusedFunction +std::string cppcheck::testing::expandMSBuildExpression(const std::string& expr) +{ + ImportProject project; + PropertiesMap properties; + std::string s = expr; + project.expandMSBuildVariables(s, properties); + return s; +} + +// cppcheck-suppress unusedFunction +std::string cppcheck::testing::expandMSBuildProperties(const std::string& expr, + const std::string& configuration, + const std::string& platform) +{ + ImportProject project; + PropertiesMap properties; + properties["Configuration"] = configuration; + properties["Platform"] = platform; + std::string s = expr; + project.expandMSBuildVariables(s, properties); + return s; +} + +// only used by tests (testimportproject.cpp::testVcxitemsPathResolution): +// cppcheck-suppress unusedFunction +std::string cppcheck::testing::resolveVcxitemsFilename(const std::string& items, const std::string& projectDir) +{ + ImportProject project; + PropertiesMap properties; + if (!projectDir.empty()) + properties["ProjectDir"] = projectDir; + std::string filename(items); + if (!project.simplifyPathWithVariables(filename, properties)) + return {}; + // Use classifyPath so that root-relative paths (\foo -> C:\foo) are resolved + // against the base drive, not treated as absolute on Linux. + { + const PathKind _fkind = classifyPath(Path::fromNativeSeparators(filename)); + if (_fkind != PathKind::UNC && _fkind != PathKind::DriveAbsolute && properties.count("ProjectDir") > 0) + filename = project.toAbsolute(filename, properties.at("ProjectDir"), properties); + } + return filename; } diff --git a/lib/importproject.h b/lib/importproject.h index b8bbbed3fa3..a258336a551 100644 --- a/lib/importproject.h +++ b/lib/importproject.h @@ -26,18 +26,22 @@ #include "platform.h" #include "utils.h" +#include #include #include #include #include #include #include +#include +#include #include class Settings; struct Suppressions; + namespace tinyxml2 { - class XMLDocument; + class XMLElement; } /// @addtogroup Core @@ -53,14 +57,25 @@ namespace cppcheck { namespace testing { CPPCHECKLIB bool evaluateVcxprojCondition(const std::string& condition, const std::string& configuration, const std::string& platform); + CPPCHECKLIB std::string expandMSBuildExpression(const std::string& expr); + CPPCHECKLIB std::string expandMSBuildProperties(const std::string& expr, const std::string& configuration, const std::string& platform); + CPPCHECKLIB std::string resolveVcxitemsFilename(const std::string& items, const std::string& projectDir); } } +using PropertiesMap = std::map; +using MetadataMap = std::map; + /** * @brief Importing project settings. */ class CPPCHECKLIB WARN_UNUSED ImportProject { public: + friend CPPCHECKLIB bool cppcheck::testing::evaluateVcxprojCondition(const std::string &condition, const std::string &configuration, const std::string &platform); + friend CPPCHECKLIB std::string cppcheck::testing::expandMSBuildExpression(const std::string &expr); + friend CPPCHECKLIB std::string cppcheck::testing::expandMSBuildProperties(const std::string &expr, const std::string &configuration, const std::string &platform); + friend CPPCHECKLIB std::string cppcheck::testing::resolveVcxitemsFilename(const std::string &items, const std::string &projectDir); + enum class Type : std::uint8_t { NONE, UNKNOWN, @@ -73,14 +88,46 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { BORLAND, CPPCHECK_GUI }; + enum class ImportResult : std::uint8_t { + Ok, + Cycle, // Visual Studio/MSBuild reports a circular import; continue safely + NotResolvable, + NotFound, + NotValid, + }; + + /// Visual Studio (like MSBuild) evaluates a project in separate passes over the + /// whole resolved import graph: every PropertyGroup/Import first (so every + /// property, wherever in the graph it is set, is final before anything else + /// runs), then every ItemDefinitionGroup, then every ItemGroup. A PropertyGroup + /// positioned after an ItemDefinitionGroup -- or reached through a file imported + /// later in the document -- is still visible to that earlier ItemDefinitionGroup. + enum class EvalPhase : std::uint8_t { + Properties, ///< Pass 1: PropertyGroup + Import/ImportGroup only; decides and records the import graph + ItemDefs, ///< Pass 2: ItemDefinitionGroup only; replays the recorded import graph + Items, ///< Pass 3: ItemGroup only; replays the recorded import graph + Discover, ///< Structural discovery bootstrap scan: PropertyGroup/ImportGroup/ + ///< Import/Choose are walked like Properties, but every Condition is + ///< treated as satisfied and Choose explores every branch instead of + ///< selecting one (see mDiscovering), and any errors/debugs generated + ///< are discarded rather than surfaced. + }; 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, const PropertiesMap &properties); + /** Set the project path prefix used to resolve relative paths in the project file. + * Normally set automatically by import(); exposed here so unit tests can exercise + * path-joining without needing a real file on disk. */ + // cppcheck-suppress unusedFunction + void setProjectPath(const std::string& p) { + mPath = p; + } public: std::list fileSettings; std::vector errors; + std::vector debugs; ImportProject() = default; virtual ~ImportProject() = default; @@ -106,32 +153,257 @@ 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 processCompileCommands(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: + struct PropertyValueExpander; + class ConditionParser; + 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, ARM64EC, 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 importVcxproj(const std::string &filename, PropertiesMap &properties, const std::vector &fileFilters); + + ImportResult processImport(const std::string &file, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack, + EvalPhase phase); + ImportResult processImportProject(const tinyxml2::XMLElement *node, + const std::string &projectDir, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack, + EvalPhase phase); + ImportResult processImportGroup(const tinyxml2::XMLElement *node, + const std::string &baseDir, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack, + EvalPhase phase); + // Decide (Properties pass) or replay (ItemDefs/Items pass) which single + // / child of a is taken, then process that child's + // own children (PropertyGroup/ItemDefinitionGroup/ItemGroup/ImportGroup/nested + // Choose) exactly as processElementChildren() would process them inline. + // During the discovery bootstrap scan (mDiscovering set) this selection is + // skipped entirely: every and any is processed instead of + // choosing one -- see the mDiscovering branch at the top of the definition. + ImportResult processChoose(const tinyxml2::XMLElement *node, + const std::string &baseDir, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack, + EvalPhase phase); + ImportResult processCompile(const tinyxml2::XMLElement *node, + const std::string &projectDir, + const PropertiesMap &properties, + const MetadataMap &metadata, + std::list &compileList); + ImportResult processElementChildren(const tinyxml2::XMLElement *parent, + const std::string &baseDir, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack, + EvalPhase phase); + void applyClCompileUpdate(const tinyxml2::XMLElement *node, + const std::string &baseDir, + const PropertiesMap &properties, + std::list &compileList); + void applyClCompileRemove(const tinyxml2::XMLElement *node, + const std::string &baseDir, + const PropertiesMap &properties, + std::list &compileList); + // Returns (original-segment, absolute-path) pairs. The original segment is + // the spec after property expansion but before toAbsolute(), preserving the + // relative form needed to compute %(RelativeDir) in processCompile(). + std::pair expandItemSpec(const std::string &spec, + const std::string &projectDir, + const PropertiesMap &properties); + std::string applyMSBuildStaticFunction(const std::string &className, + const std::string &member, + const std::vector &args, + const PropertiesMap *properties = nullptr); + void applyClCompileChild(const tinyxml2::XMLElement *e1, + const PropertiesMap &properties, + MetadataMap &metadata); + void expandMSBuildVariables(std::string &s, const PropertiesMap &properties); + bool evalCondition(const std::string &condition, const PropertiesMap &properties); + bool conditionIsTrue(const tinyxml2::XMLElement *node, const PropertiesMap &properties); + bool hasName(const tinyxml2::XMLElement *node, const char *nodeName, const PropertiesMap &properties); + bool hasNameAndLabel(const tinyxml2::XMLElement *node, const char *nodeName, const char *nodeAttr, const PropertiesMap &properties); + bool hasNameAndNotLabel(const tinyxml2::XMLElement * node, const char *nodeName, const char *nodeAttr, const PropertiesMap & properties); + // Decide (Properties pass) or replay (ItemDefs/Items pass) whether one import-graph + // branch point -- an , an , or a synthetic ForceImportXxx / + // Directory.Build.* import attempt performed by attemptSyntheticImport() -- is + // taken. During the Properties pass `conditionHolds` (already evaluated by the + // caller) is recorded and returned as-is, and `file` (the target path the caller + // has already resolved, if any) is recorded alongside it. During ItemDefs/Items, + // both the condition and the file the caller just (re)computed are ignored, and + // the recorded pair from the Properties pass is replayed instead: the resolved + // file's own $(...) references may include a property that was still unset (or + // held a different value) at the point Properties visited this branch, so + // recomputing it against the now-final properties could resolve to a different + // file than Properties actually walked, which would desync the properties + // Properties built from the items/definitions ItemDefs/Items evaluate. Outside + // the three-pass evaluation (mImportGraph inactive) this simply returns + // `conditionHolds` unchanged. + bool importGraphDecision(bool conditionHolds, std::string &file); + // Same decide/replay discipline as importGraphDecision(), for a 's + // branch selection instead of an 's target file. During the + // Properties pass `matched` (already computed by the caller: did any + // match, or is there an to fall back to) and `branch` + // (the 0-based index, among the Choose's / children in + // document order, of the one that matched) are recorded and returned/left + // as-is. During ItemDefs/Items, both are ignored and the recorded pair is + // replayed instead: a 's Condition may reference a property that was + // still unset (or held a different value) when the Properties pass reached + // this Choose, so recomputing it against the now-final properties could + // select a different branch than Properties actually walked -- desyncing + // the properties Properties built from the items/definitions ItemDefs/Items + // evaluate, exactly like an Import resolving to a different file would. + // Outside the three-pass evaluation (mImportGraph inactive) this simply + // returns `matched` unchanged. + bool importGraphChooseDecision(bool matched, std::size_t &branch); + // Attempt one of the synthetic (non-XML) imports the Microsoft.Cpp.Default.props / + // .props / .targets emulation performs implicitly: the ForceImportBeforeXxx / + // ForceImportAfterXxx hook properties, and the Directory.Build.props/.targets + // auto-import. `file` is the already-resolved target path, or empty if there is + // nothing to import. Participates in the same decide/replay discipline as a + // regular element via importGraphDecision(). + ImportResult attemptSyntheticImport(std::string file, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack, + EvalPhase phase); + void checkUnexpandedExpressions(const std::string &text, const char *context); + bool simplifyPathWithVariables(std::string &s, const PropertiesMap &properties); + void addProperty(const tinyxml2::XMLElement *node, PropertiesMap &properties); + void addMetadata(const tinyxml2::XMLElement *node, const PropertiesMap &properties, MetadataMap &metadata); + std::string getMetadata(const tinyxml2::XMLElement *node, const PropertiesMap &properties, const MetadataMap &metadata, const std::string &original); + std::string toAbsolute(const std::string &filename, const std::string &baseDir, const PropertiesMap &properties); + static std::string toAbsoluteExpanded(const std::string &filename, const std::string &baseDir); + static std::string toAbsolute(const std::string &path); + static void setSolution(const std::string &filename, PropertiesMap &properties); + void addDebug(const std::string &msg); + + /// Tracks the state of the current three-pass (Properties/ItemDefs/Items) + /// evaluation for one project configuration. During the Properties pass every + /// import-graph branch point is decided and its outcome recorded; during the + /// ItemDefs and Items passes those decisions are replayed in the same order + /// instead of being re-evaluated, so all three passes walk the identical + /// resolved graph -- exactly what real MSBuild/Visual Studio evaluation does by + /// resolving imports once, during property evaluation, and reusing that graph + /// for item definitions and items. + struct ImportGraph { + /// One import-graph branch point's outcome, as decided during the + /// Properties pass: whether it was taken and, if so, either the target + /// file it resolved to at that time (Import/ImportGroup) or the index of + /// the / child it selected (Choose) -- whichever applies + /// to this branch point's kind -- frozen so later passes reuse it + /// verbatim instead of recomputing it against possibly-different state. + struct Decision { + bool taken = false; + std::string file; ///< Import/ImportGroup only; unused for Choose. + std::size_t branch = 0; ///< Choose only; unused for Import/ImportGroup. + }; + /// Files already imported during the current pass. An imported file is + /// processed at most once per pass; a repeated of it is ignored + /// (MSB4011). Reset at the start of each pass; which files end up in it is + /// an automatic consequence of replaying the same decisions in the same + /// order, so it does not itself need to be recorded/replayed. + std::unordered_set imported; + /// Per container file (by import-file-key): the decision for each + /// import-graph branch point encountered while walking that file's + /// children, in traversal order. Populated during the Properties pass; + /// read-only afterwards. + std::map> decisions; + /// Per container file: how far ItemDefs/Items replay has consumed that + /// file's decisions vector. Reset at the start of each replay pass. + std::map cursor; + /// Import-file-key of the file whose children are currently being walked; + /// keys 'decisions'/'cursor'. Maintained by CurrentFileGuard in processImport(). + std::string currentFile; + bool active = false; ///< true only inside importVcxproj's per-configuration loop + bool replay = false; ///< true during ItemDefs/Items: replay recorded decisions, don't decide + }; + std::string mPath; std::set mAllVSConfigs; + /// Names of properties that resolve to the SAME value in every one of this + /// project's configurations, computed once per project file by a priming + /// walk over projectConfigurationList (see importVcxproj()) before the + /// real per-configuration passes run. A macro is unsafe in a project item + /// path only if its value could differ by configuration -- see + /// expandItemSpec()'s use of this alongside invariantItemPathProperties() + /// -- so this set, not just that fixed name list, is what a macro's name + /// is checked against. Cleared and repopulated at the top of each + /// importVcxproj() call; empty (and therefore inert) before the first one. + /// MSBuild property names are case-insensitive (PropertyGroup and + /// PropertiesMap itself, above, agree on this), so this uses the same + /// cppcheck::stricmp comparator PropertiesMap does -- otherwise "MyRoot" + /// set in one configuration's PropertyGroup and "myroot" set in another's + /// would be tracked as two unrelated names instead of one property, and + /// each could wrongly look config-invariant on its own even when the + /// property's real value differs by configuration. The priming walk + /// (see its comment in importVcxproj()) also tracks a property that only + /// SOME configurations set at all -- a value in one configuration and no + /// PropertyGroup for it whatsoever in another is itself a difference, + /// exactly like real MSBuild resolving it to "" wherever nothing sets + /// it -- rather than considering the property only where it happens to + /// be present. + std::set mConfigInvariantProperties; + ImportGraph mImportGraph; + /// True only while importVcxproj()'s discovery bootstrap scan (used when a + /// project has no inline ProjectConfigurations) is walking the document. While + /// set, conditionIsTrue() treats every Condition as satisfied and processChoose() + /// explores every / instead of selecting one: discovery's only + /// goal is to enumerate every ProjectConfiguration reachable through ANY + /// combination of property values, and it cannot correctly predict which single + /// branch a real build would take for values -- Configuration/Platform above + /// all -- that are themselves what is being discovered. See DiscoveringGuard. + bool mDiscovering = false; }; @@ -201,10 +473,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..b956b687419 100644 --- a/lib/platform.cpp +++ b/lib/platform.cpp @@ -97,6 +97,62 @@ 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::WinARM64EC: + // ARM64EC is an x64-ABI on ARM64 hardware (VS 2022+). + // Pointer width and size_t are 64-bit; all other layout matches 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 +206,12 @@ 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 == "winARM64EC") + set(Type::WinARM64EC); + 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..f630df9cbd7 100644 --- a/lib/platform.h +++ b/lib/platform.h @@ -140,6 +140,9 @@ class CPPCHECKLIB Platform { Win32A, Win32W, Win64, + WinARM64, + WinARM64EC, + WinARM, Unix32, Unix64, File @@ -188,6 +191,12 @@ class CPPCHECKLIB Platform { return "win32W"; case Type::Win64: return "win64"; + case Type::WinARM64: + return "winARM64"; + case Type::WinARM64EC: + return "winARM64EC"; + 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/project_test.py b/test/cli/project_test.py index 64bb406a709..86d528f074f 100644 --- a/test/cli/project_test.py +++ b/test/cli/project_test.py @@ -20,9 +20,9 @@ def test_missing_project(project_ext): def __test_project_error(tmpdir, ext, content, expected): project_file = os.path.join(tmpdir, "file.{}".format(ext)) - with open(project_file, 'w') as f: + with open(project_file, 'wb') as f: if content is not None: - f.write(content) + f.write(content.encode('utf-8')) ret, stdout, stderr = cppcheck(['--project=' + str(project_file)]) assert 1 == ret @@ -102,7 +102,7 @@ def test_sln_invalid_file(tmpdir): def test_sln_no_header(tmpdir): - content = "\xEF\xBB\xBF\r\n" \ + content = "\r\n" \ "some header" expected = "Visual Studio solution file header not found" @@ -111,7 +111,7 @@ def test_sln_no_header(tmpdir): def test_sln_no_projects(tmpdir): - content = "\xEF\xBB\xBF\r\n" \ + content = "\r\n" \ "Microsoft Visual Studio Solution File, Format Version 12.00\r\n" expected = "no projects found in Visual Studio solution file" @@ -120,7 +120,7 @@ def test_sln_no_projects(tmpdir): def test_sln_project_file_not_found(tmpdir): - content = "\xEF\xBB\xBF\r\n" \ + content = "\r\n" \ "Microsoft Visual Studio Solution File, Format Version 12.00\r\n" \ "# Visual Studio Version 16\r\n" \ "VisualStudioVersion = 16.0.29020.237\r\n" \ 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..9ce10068a8b --- /dev/null +++ b/test/cli/props-dirs/ProjA/ProjA.vcxproj @@ -0,0 +1,27 @@ + + + + {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..aab1237748c --- /dev/null +++ b/test/cli/props-dirs/ProjA/a.cpp @@ -0,0 +1,15 @@ +#include "common.h" +#include "common2.h" + +#ifndef COMMON_H_INCLUDED_MARKER +#error "common.h was not found - AdditionalIncludeDirectories from common.props did not resolve" +#endif +#ifndef COMMON2_H_INCLUDED_MARKER +#error "common2.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..3daf8907924 --- /dev/null +++ b/test/cli/props-dirs/ProjB/ProjB.vcxproj @@ -0,0 +1,24 @@ + + + + {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..8c3e4016529 --- /dev/null +++ b/test/cli/props-dirs/ProjB/b.cpp @@ -0,0 +1,15 @@ +#include "common.h" +#include "common2.h" + +#ifndef COMMON_H_INCLUDED_MARKER +#error "common.h was not found - AdditionalIncludeDirectories from common.props did not resolve" +#endif +#ifndef COMMON2_H_INCLUDED_MARKER +#error "common2.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/common/common2.h b/test/cli/props-dirs/common/common2.h new file mode 100644 index 00000000000..320e68a96fa --- /dev/null +++ b/test/cli/props-dirs/common/common2.h @@ -0,0 +1,3 @@ +#ifndef COMMON2_H_INCLUDED_MARKER +#define COMMON2_H_INCLUDED_MARKER +#endif diff --git a/test/cli/props-dirs/common/common2.props b/test/cli/props-dirs/common/common2.props new file mode 100644 index 00000000000..cf945d49ffa --- /dev/null +++ b/test/cli/props-dirs/common/common2.props @@ -0,0 +1,13 @@ + + + + + + COMMON2_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/shared/shared2.props b/test/cli/props-dirs/shared/shared2.props new file mode 100644 index 00000000000..745a5d45a21 --- /dev/null +++ b/test/cli/props-dirs/shared/shared2.props @@ -0,0 +1,14 @@ + + + + + + + + + SHARED2_DEFINE;%(PreprocessorDefinitions) + + + diff --git a/test/cli/props_dirs_test.py b/test/cli/props_dirs_test.py new file mode 100644 index 00000000000..bccfe3cf2bc --- /dev/null +++ b/test/cli/props_dirs_test.py @@ -0,0 +1,93 @@ + +# 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:14:14: error: Division by zero. [zerodiv]\n' + ' return x / 0;\n' + ' ^\n') % os.path.join('props-dirs', 'ProjA', 'a.cpp') +__ERR_B = ('%s:14: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 (-> common/common.props) and shared/shared2.props + # (-> common/common2.props), and sets its own PROJA_DEFINE. + # v143 toolset -> _MSC_VER=1930/_MSC_FULL_VER=193000000; common.props sets stdcpp17 + # -> _MSVC_LANG=201703L. + assert '_WIN32=1' in dump_a_content + assert '_WIN64=1' in dump_a_content + assert '_M_X64=100' in dump_a_content + assert '_MSC_VER=1930' in dump_a_content + assert '_MSC_FULL_VER=193000000' in dump_a_content + assert '_MSVC_LANG=201703L' in dump_a_content + assert 'PROJA_DEFINE=1' in dump_a_content + assert 'SHARED_DEFINE=1' in dump_a_content + assert 'COMMON_DEFINE=1' in dump_a_content + assert '' in dump_a_content + + # ProjB imports common/common.props and common/common2.props directly - it must see + # COMMON2_DEFINE and COMMON_DEFINE, but none of ProjA's or shared's defines. + assert '_MSC_VER=1930' in dump_b_content + assert '_MSC_FULL_VER=193000000' in dump_b_content + assert '_MSVC_LANG=201703L' in dump_b_content + assert 'COMMON2_DEFINE=1' in dump_b_content + assert 'COMMON_DEFINE=1' 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 + assert 'SHARED2_DEFINE' not in dump_b_content diff --git a/test/cli/slnx-folders/slnx-folders.slnx b/test/cli/slnx-folders/slnx-folders.slnx index ce6b9014431..fd50c3a054c 100644 --- a/test/cli/slnx-folders/slnx-folders.slnx +++ b/test/cli/slnx-folders/slnx-folders.slnx @@ -2,11 +2,13 @@ - - - - - - + + + + + + + + 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..cc7549a0e06 --- /dev/null +++ b/test/cli/vcxproj-unicode/vcxproj_unicode.vcxproj @@ -0,0 +1,43 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + MultiByte + Win32 + + + + + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + NotSet + Static + + + Application + false + v143 + MultiByte + + + + + diff --git a/test/cli/vcxproj_choose/main.cpp b/test/cli/vcxproj_choose/main.cpp new file mode 100644 index 00000000000..76e8197013a --- /dev/null +++ b/test/cli/vcxproj_choose/main.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/test/cli/vcxproj_choose/vcxproj_choose.vcxproj b/test/cli/vcxproj_choose/vcxproj_choose.vcxproj new file mode 100644 index 00000000000..f83db51b5fb --- /dev/null +++ b/test/cli/vcxproj_choose/vcxproj_choose.vcxproj @@ -0,0 +1,61 @@ + + + + + Debug + x64 + + + + {33333333-3333-3333-3333-333333333333} + choosetest + 10.0 + + + + Application + v143 + + + + + + + + + CHOSEN_BRANCH=When;%(PreprocessorDefinitions) + + + + true + + + + + + CHOSEN_BRANCH=Otherwise;%(PreprocessorDefinitions) + + + + true + + + + + + + + + + + true + + + + diff --git a/test/cli/vcxproj_choose_test.py b/test/cli/vcxproj_choose_test.py new file mode 100644 index 00000000000..a261dfada7b --- /dev/null +++ b/test/cli/vcxproj_choose_test.py @@ -0,0 +1,44 @@ + +# python -m pytest vcxproj_choose_test.py +# +# Regression coverage for MSBuild/Visual Studio // +# support: Visual Studio's project evaluator (the same Microsoft.Build engine +# msbuild.exe uses) selects a Choose's branch once, using properties as +# accumulated at that point in the document, and reuses that same branch for +# item-definition and item evaluation -- it does not re-evaluate the +# Condition a second time against final properties. This fixture's +# depends on a property that is only assigned by a PropertyGroup positioned +# AFTER the Choose; if cppcheck dropped Choose/When/Otherwise entirely (as it +# used to), the ItemDefinitionGroup inside it would never be evaluated at all, +# and if it instead re-evaluated the When Condition against final properties +# during item-definition/item evaluation, it would pick the wrong branch +# ("When" instead of the "Otherwise" branch Properties actually selected). + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_choose(): + args = [ + '--project=vcxproj_choose/vcxproj_choose.vcxproj', + '--no-cppcheck-build-dir', + '--dump' + ] + ret, stdout, _ = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + dump_file = os.path.join(__script_dir, 'vcxproj_choose', 'main.cpp.dump') + assert os.path.exists(dump_file), "Dump file not found at %s" % dump_file + + with open(dump_file, 'rt') as f: + dump_content = f.read() + + # UseSpecialDefine is unset at the point Properties reaches the Choose (it is + # only assigned afterwards), so the Properties pass selects -- + # and ItemDefs/Items must replay that same selection rather than + # re-evaluating the When Condition against the now-final property value. + assert 'CHOSEN_BRANCH=Otherwise' in dump_content, dump_content + assert 'CHOSEN_BRANCH=When' not in dump_content, dump_content diff --git a/test/cli/vcxproj_directory_build_disabled/Directory.Build.props b/test/cli/vcxproj_directory_build_disabled/Directory.Build.props new file mode 100644 index 00000000000..747f1613e36 --- /dev/null +++ b/test/cli/vcxproj_directory_build_disabled/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + PROPS_DEFINE=1;%(PreprocessorDefinitions) + + + diff --git a/test/cli/vcxproj_directory_build_disabled/Directory.Build.targets b/test/cli/vcxproj_directory_build_disabled/Directory.Build.targets new file mode 100644 index 00000000000..fac7a9f273f --- /dev/null +++ b/test/cli/vcxproj_directory_build_disabled/Directory.Build.targets @@ -0,0 +1,8 @@ + + + + + TARGETS_DEFINE=1;%(PreprocessorDefinitions) + + + diff --git a/test/cli/vcxproj_directory_build_disabled/main.cpp b/test/cli/vcxproj_directory_build_disabled/main.cpp new file mode 100644 index 00000000000..61931dfffa0 --- /dev/null +++ b/test/cli/vcxproj_directory_build_disabled/main.cpp @@ -0,0 +1,15 @@ +// Each check below fails with #error if Directory.Build.props/.targets was +// imported despite ImportDirectoryBuildProps/ImportDirectoryBuildTargets being +// set to false; see vcxproj_directory_build_disabled_test.py. +#if defined(PROPS_DEFINE) +#error Directory.Build.props was imported despite ImportDirectoryBuildProps=false +#endif +#if defined(TARGETS_DEFINE) +#error Directory.Build.targets was imported despite ImportDirectoryBuildTargets=false +#endif + +int main() +{ + int x = 3 / 0; // ERROR + return x; +} diff --git a/test/cli/vcxproj_directory_build_disabled/vcxproj_directory_build_disabled.vcxproj b/test/cli/vcxproj_directory_build_disabled/vcxproj_directory_build_disabled.vcxproj new file mode 100644 index 00000000000..bddcfb9bc8a --- /dev/null +++ b/test/cli/vcxproj_directory_build_disabled/vcxproj_directory_build_disabled.vcxproj @@ -0,0 +1,31 @@ + + + + + Debug + x64 + + + + + {88888888-8888-8888-8888-888888888888} + dirbuilddisabledtest + 10.0 + false + false + + + + Application + v143 + + + + + + + diff --git a/test/cli/vcxproj_directory_build_disabled_test.py b/test/cli/vcxproj_directory_build_disabled_test.py new file mode 100644 index 00000000000..cf07274016f --- /dev/null +++ b/test/cli/vcxproj_directory_build_disabled_test.py @@ -0,0 +1,33 @@ + +# python -m pytest vcxproj_directory_build_disabled_test.py +# +# Regression coverage for $(ImportDirectoryBuildProps) / $(ImportDirectoryBuildTargets): +# attemptSyntheticImport()'s emulation of Microsoft.Cpp.Default.props (which normally +# imports Directory.Build.props) and Microsoft.Cpp.targets (which normally imports +# Directory.Build.targets) must honor these controls, which Microsoft documents in +# "Customize your build by folder or solution". Both default to true, but this +# fixture's Globals PropertyGroup -- processed before either import point is +# reached -- sets both to false, so neither Directory.Build.props nor +# Directory.Build.targets (both sitting right next to the project, exactly where +# the automatic upward search would otherwise find them) may be imported. +# +# main.cpp turns either file's ItemDefinitionGroup contribution into a #error, so +# the only expected diagnostic is the deliberate division by zero. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_directory_build_disabled(): + args = [ + '--template=cppcheck1', + '--project=vcxproj_directory_build_disabled/vcxproj_directory_build_disabled.vcxproj', + '--no-cppcheck-build-dir' + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + filename = os.path.join('vcxproj_directory_build_disabled', 'main.cpp') + assert stderr == '[%s:13]: (error) Division by zero.\n' % filename diff --git a/test/cli/vcxproj_directory_build_path/CustomBuild.props b/test/cli/vcxproj_directory_build_path/CustomBuild.props new file mode 100644 index 00000000000..c4591915f91 --- /dev/null +++ b/test/cli/vcxproj_directory_build_path/CustomBuild.props @@ -0,0 +1,8 @@ + + + + + RIGHT_PROPS_DEFINE=1;%(PreprocessorDefinitions) + + + diff --git a/test/cli/vcxproj_directory_build_path/CustomBuild.targets b/test/cli/vcxproj_directory_build_path/CustomBuild.targets new file mode 100644 index 00000000000..3a11e53f679 --- /dev/null +++ b/test/cli/vcxproj_directory_build_path/CustomBuild.targets @@ -0,0 +1,8 @@ + + + + + RIGHT_TARGETS_DEFINE=1;%(PreprocessorDefinitions) + + + diff --git a/test/cli/vcxproj_directory_build_path/Directory.Build.props b/test/cli/vcxproj_directory_build_path/Directory.Build.props new file mode 100644 index 00000000000..b15b0db8926 --- /dev/null +++ b/test/cli/vcxproj_directory_build_path/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + WRONG_PROPS_DEFINE=1;%(PreprocessorDefinitions) + + + diff --git a/test/cli/vcxproj_directory_build_path/Directory.Build.targets b/test/cli/vcxproj_directory_build_path/Directory.Build.targets new file mode 100644 index 00000000000..9a18ed7fe4e --- /dev/null +++ b/test/cli/vcxproj_directory_build_path/Directory.Build.targets @@ -0,0 +1,8 @@ + + + + + WRONG_TARGETS_DEFINE=1;%(PreprocessorDefinitions) + + + diff --git a/test/cli/vcxproj_directory_build_path/main.cpp b/test/cli/vcxproj_directory_build_path/main.cpp new file mode 100644 index 00000000000..7dc10ed6c2b --- /dev/null +++ b/test/cli/vcxproj_directory_build_path/main.cpp @@ -0,0 +1,21 @@ +// Each check below fails with #error if DirectoryBuildPropsPath/ +// DirectoryBuildTargetsPath did not override the upward directory search with +// the explicit file they name; see vcxproj_directory_build_path_test.py. +#if !defined(RIGHT_PROPS_DEFINE) +#error DirectoryBuildPropsPath override (CustomBuild.props) was not imported +#endif +#if defined(WRONG_PROPS_DEFINE) +#error Directory.Build.props was imported even though DirectoryBuildPropsPath overrides the search +#endif +#if !defined(RIGHT_TARGETS_DEFINE) +#error DirectoryBuildTargetsPath override (CustomBuild.targets) was not imported +#endif +#if defined(WRONG_TARGETS_DEFINE) +#error Directory.Build.targets was imported even though DirectoryBuildTargetsPath overrides the search +#endif + +int main() +{ + int x = 3 / 0; // ERROR + return x; +} diff --git a/test/cli/vcxproj_directory_build_path/vcxproj_directory_build_path.vcxproj b/test/cli/vcxproj_directory_build_path/vcxproj_directory_build_path.vcxproj new file mode 100644 index 00000000000..bd596b95cde --- /dev/null +++ b/test/cli/vcxproj_directory_build_path/vcxproj_directory_build_path.vcxproj @@ -0,0 +1,31 @@ + + + + + Debug + x64 + + + + + {99999999-9999-9999-9999-999999999999} + dirbuildpathtest + 10.0 + CustomBuild.props + CustomBuild.targets + + + + Application + v143 + + + + + + + diff --git a/test/cli/vcxproj_directory_build_path_test.py b/test/cli/vcxproj_directory_build_path_test.py new file mode 100644 index 00000000000..721e6ba9b8d --- /dev/null +++ b/test/cli/vcxproj_directory_build_path_test.py @@ -0,0 +1,32 @@ + +# python -m pytest vcxproj_directory_build_path_test.py +# +# Regression coverage for $(DirectoryBuildPropsPath) / $(DirectoryBuildTargetsPath): +# when set, Microsoft documents that these override attemptSyntheticImport()'s +# normal upward-directory search (see "Customize your build by folder or +# solution") with an explicit file. This fixture's Globals PropertyGroup sets +# both to "CustomBuild.props"/"CustomBuild.targets", while Directory.Build.props +# and Directory.Build.targets -- which the upward search would otherwise find +# first, since they sit right next to the project -- are also present, each +# defining a macro the *other* file does not. main.cpp turns any mismatch (the +# override not imported, or the plain Directory.Build.* file imported anyway) +# into a #error, so the only expected diagnostic is the deliberate division by +# zero. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_directory_build_path(): + args = [ + '--template=cppcheck1', + '--project=vcxproj_directory_build_path/vcxproj_directory_build_path.vcxproj', + '--no-cppcheck-build-dir' + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + filename = os.path.join('vcxproj_directory_build_path', 'main.cpp') + assert stderr == '[%s:19]: (error) Division by zero.\n' % filename diff --git a/test/cli/vcxproj_discover_choose/Configs.props b/test/cli/vcxproj_discover_choose/Configs.props new file mode 100644 index 00000000000..aeebadde55d --- /dev/null +++ b/test/cli/vcxproj_discover_choose/Configs.props @@ -0,0 +1,9 @@ + + + + + Debug + x64 + + + diff --git a/test/cli/vcxproj_discover_choose/main.cpp b/test/cli/vcxproj_discover_choose/main.cpp new file mode 100644 index 00000000000..76e8197013a --- /dev/null +++ b/test/cli/vcxproj_discover_choose/main.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/test/cli/vcxproj_discover_choose/vcxproj_discover_choose.vcxproj b/test/cli/vcxproj_discover_choose/vcxproj_discover_choose.vcxproj new file mode 100644 index 00000000000..982b085258c --- /dev/null +++ b/test/cli/vcxproj_discover_choose/vcxproj_discover_choose.vcxproj @@ -0,0 +1,34 @@ + + + + {dddddddd-dddd-dddd-dddd-dddddddddddd} + discoverchoosetest + 10.0 + + + + + + + + + + + + Application + v143 + + + + + + + diff --git a/test/cli/vcxproj_discover_choose_test.py b/test/cli/vcxproj_discover_choose_test.py new file mode 100644 index 00000000000..c56d058c8f2 --- /dev/null +++ b/test/cli/vcxproj_discover_choose_test.py @@ -0,0 +1,43 @@ + +# python -m pytest vcxproj_discover_choose_test.py +# +# Regression coverage for the vcxproj configuration-discovery pass (used only +# when a project has no inline and +# must find its configurations by walking imports): it must also look inside +# a top-level , not just top-level PropertyGroup/ImportGroup/Import. +# +# This fixture's only configuration (Debug|x64) is defined in Configs.props, +# which is reachable only through an nested inside a top-level +# . A discovery pass that walks +# PropertyGroup/ImportGroup/Import at the top level but never inspects +# at all never finds Configs.props, so no configuration is discovered and +# cppcheck fails outright ("no C or C++ source files found") even though the +# project is well-formed -- this is a different bug than the discovery pass +# stopping early (covered by vcxproj_split_configs_test.py): here the node type +# is never even recognized, so it doesn't matter whether it's found first, +# last, or is the only import in the project. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_discover_choose(): + args = [ + '--project=vcxproj_discover_choose/vcxproj_discover_choose.vcxproj', + '--no-cppcheck-build-dir', + '--dump' + ] + ret, stdout, _ = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + # Windows prints native '\' path separators ("Checking foo\main.cpp ..."); + # normalize before matching so this passes on every platform (same idiom + # used by test_log() in clang-import_test.py). + normalized_stdout = stdout.replace('\\', '/') + + # The configuration reachable only through the top-level Choose must be + # discovered and checked, not silently dropped. + assert 'Checking vcxproj_discover_choose/main.cpp Debug|x64' in normalized_stdout, stdout diff --git a/test/cli/vcxproj_discover_property_path/Debug/Configs.props b/test/cli/vcxproj_discover_property_path/Debug/Configs.props new file mode 100644 index 00000000000..aeebadde55d --- /dev/null +++ b/test/cli/vcxproj_discover_property_path/Debug/Configs.props @@ -0,0 +1,9 @@ + + + + + Debug + x64 + + + diff --git a/test/cli/vcxproj_discover_property_path/main.cpp b/test/cli/vcxproj_discover_property_path/main.cpp new file mode 100644 index 00000000000..76e8197013a --- /dev/null +++ b/test/cli/vcxproj_discover_property_path/main.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/test/cli/vcxproj_discover_property_path/vcxproj_discover_property_path.vcxproj b/test/cli/vcxproj_discover_property_path/vcxproj_discover_property_path.vcxproj new file mode 100644 index 00000000000..71677194e2b --- /dev/null +++ b/test/cli/vcxproj_discover_property_path/vcxproj_discover_property_path.vcxproj @@ -0,0 +1,28 @@ + + + + {11111111-2222-3333-4444-555555555555} + discoverpropertypathtest + 10.0 + + + + + + Application + v143 + + + + + + + diff --git a/test/cli/vcxproj_discover_property_path_test.py b/test/cli/vcxproj_discover_property_path_test.py new file mode 100644 index 00000000000..8007b969471 --- /dev/null +++ b/test/cli/vcxproj_discover_property_path_test.py @@ -0,0 +1,44 @@ + +# python -m pytest vcxproj_discover_property_path_test.py +# +# Regression coverage for the vcxproj configuration-discovery pass (used only +# when a project has no inline and +# must find its configurations by walking imports): it must still seed a +# guessed Configuration/Platform, even though it now ignores every Condition +# (see vcxproj_discover_choose_test.py / mDiscovering). +# +# This fixture's only has no Condition and is not behind a -- +# there is nothing for "ignore every Condition" to help with here. Instead, the +# import's PATH ITSELF embeds $(Configuration): . Resolving that path at all requires a +# concrete value for $(Configuration); without a seeded guess it is left as a +# literal, unexpanded "$(Configuration)" segment, which +# simplifyPathWithVariables() treats as unresolvable, so the import -- and the +# only configuration it defines -- is silently skipped and cppcheck fails +# outright ("no C or C++ source files found") despite the project being +# well-formed. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_discover_property_path(): + args = [ + '--project=vcxproj_discover_property_path/vcxproj_discover_property_path.vcxproj', + '--no-cppcheck-build-dir', + '--dump' + ] + ret, stdout, _ = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + # Windows prints native '\' path separators ("Checking foo\main.cpp ..."); + # normalize before matching so this passes on every platform (same idiom + # used by test_log() in clang-import_test.py). + normalized_stdout = stdout.replace('\\', '/') + + # The configuration reachable only through the unconditional, path-embedded + # $(Configuration) import must be discovered and checked. + assert 'Checking vcxproj_discover_property_path/main.cpp Debug|x64' in normalized_stdout, stdout diff --git a/test/cli/vcxproj_duplicate_import/Common.props b/test/cli/vcxproj_duplicate_import/Common.props new file mode 100644 index 00000000000..b3058925ff6 --- /dev/null +++ b/test/cli/vcxproj_duplicate_import/Common.props @@ -0,0 +1,6 @@ + + + + $(Counter)X + + diff --git a/test/cli/vcxproj_duplicate_import/main.cpp b/test/cli/vcxproj_duplicate_import/main.cpp new file mode 100644 index 00000000000..76e8197013a --- /dev/null +++ b/test/cli/vcxproj_duplicate_import/main.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/test/cli/vcxproj_duplicate_import/vcxproj_duplicate_import.vcxproj b/test/cli/vcxproj_duplicate_import/vcxproj_duplicate_import.vcxproj new file mode 100644 index 00000000000..6fe09159cd9 --- /dev/null +++ b/test/cli/vcxproj_duplicate_import/vcxproj_duplicate_import.vcxproj @@ -0,0 +1,41 @@ + + + + + Debug + x64 + + + + {66666666-6666-6666-6666-666666666666} + duptest + 10.0 + + + + Application + v143 + + + + + + + + + + COUNTER_VALUE=$(Counter);%(PreprocessorDefinitions) + + + + + + + + + diff --git a/test/cli/vcxproj_duplicate_import_test.py b/test/cli/vcxproj_duplicate_import_test.py new file mode 100644 index 00000000000..d9fb3583d4d --- /dev/null +++ b/test/cli/vcxproj_duplicate_import_test.py @@ -0,0 +1,40 @@ + +# python -m pytest vcxproj_duplicate_import_test.py +# +# Regression coverage for MSBuild/Visual Studio duplicate-import handling +# (MSB4011): once a project file has been imported, a later of the +# same file (by resolved, case-insensitive path) is a no-op -- its content is +# not evaluated a second time. This fixture imports Common.props via two +# separate elements; Common.props appends to a property +# (Counter = $(Counter)X) non-idempotently, so double-processing is directly +# observable: processed once, Counter ends up "X"; processed twice, "XX". +# Checked in all three evaluation passes (Properties/ItemDefs/Items), not just +# Properties, since each pass has its own 'imported' dedup state that must +# independently reproduce the same "process once" result. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_duplicate_import(): + args = [ + '--project=vcxproj_duplicate_import/vcxproj_duplicate_import.vcxproj', + '--no-cppcheck-build-dir', + '--dump' + ] + ret, stdout, _ = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + dump_file = os.path.join(__script_dir, 'vcxproj_duplicate_import', 'main.cpp.dump') + assert os.path.exists(dump_file), "Dump file not found at %s" % dump_file + + with open(dump_file, 'rt') as f: + dump_content = f.read() + + # Common.props must be evaluated exactly once despite being referenced by + # two separate elements -- Counter must be "X", not "XX". + assert 'COUNTER_VALUE=X;' in dump_content, dump_content + assert 'COUNTER_VALUE=XX' not in dump_content, dump_content 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_import_condition/conditional.props b/test/cli/vcxproj_import_condition/conditional.props new file mode 100644 index 00000000000..8f93474463e --- /dev/null +++ b/test/cli/vcxproj_import_condition/conditional.props @@ -0,0 +1,8 @@ + + + + + SHOULD_NOT_APPEAR=1;%(PreprocessorDefinitions) + + + diff --git a/test/cli/vcxproj_import_condition/main.cpp b/test/cli/vcxproj_import_condition/main.cpp new file mode 100644 index 00000000000..76e8197013a --- /dev/null +++ b/test/cli/vcxproj_import_condition/main.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/test/cli/vcxproj_import_condition/vcxproj_import_condition.vcxproj b/test/cli/vcxproj_import_condition/vcxproj_import_condition.vcxproj new file mode 100644 index 00000000000..b4352581e87 --- /dev/null +++ b/test/cli/vcxproj_import_condition/vcxproj_import_condition.vcxproj @@ -0,0 +1,37 @@ + + + + + Debug + x64 + + + + {55555555-5555-5555-5555-555555555555} + condtest + 10.0 + + + + Application + v143 + + + + + + + + + + + + true + + + + diff --git a/test/cli/vcxproj_import_condition_test.py b/test/cli/vcxproj_import_condition_test.py new file mode 100644 index 00000000000..bd1cf4e6df7 --- /dev/null +++ b/test/cli/vcxproj_import_condition_test.py @@ -0,0 +1,43 @@ + +# python -m pytest vcxproj_import_condition_test.py +# +# Regression coverage for MSBuild/Visual Studio's three-pass evaluation order: +# an 's Condition is decided exactly once, during the Properties pass, +# using properties as accumulated at the point Properties reaches it -- not +# re-evaluated during the ItemDefs/Items passes against end-of-Properties +# (final) property state. This fixture's +# has a Condition that depends on a property (LateFlag) which is only assigned +# by a PropertyGroup positioned AFTER the Import -- so the Properties pass +# sees it unset and does not take the import. If ItemDefs/Items instead +# re-evaluated the same Condition against the now-final properties (where +# LateFlag=true), they would incorrectly take the import there and pick up +# conditional.props's ItemDefinitionGroup, leaking SHOULD_NOT_APPEAR into the +# dump even though the import was never actually part of the resolved graph +# Properties built. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_import_condition(): + args = [ + '--project=vcxproj_import_condition/vcxproj_import_condition.vcxproj', + '--no-cppcheck-build-dir', + '--dump' + ] + ret, stdout, _ = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + dump_file = os.path.join(__script_dir, 'vcxproj_import_condition', 'main.cpp.dump') + assert os.path.exists(dump_file), "Dump file not found at %s" % dump_file + + with open(dump_file, 'rt') as f: + dump_content = f.read() + + # conditional.props must stay un-imported in every phase -- its Condition + # was false when Properties decided it, and that decision must be replayed + # (not re-evaluated) by ItemDefs/Items. + assert 'SHOULD_NOT_APPEAR' not in dump_content, dump_content diff --git a/test/cli/vcxproj_import_graph/Directory.Build.props b/test/cli/vcxproj_import_graph/Directory.Build.props new file mode 100644 index 00000000000..de26fe1ecac --- /dev/null +++ b/test/cli/vcxproj_import_graph/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + DIRBUILD_DEFINE=1;%(PreprocessorDefinitions) + + + diff --git a/test/cli/vcxproj_import_graph/dup.props b/test/cli/vcxproj_import_graph/dup.props new file mode 100644 index 00000000000..ab74367157d --- /dev/null +++ b/test/cli/vcxproj_import_graph/dup.props @@ -0,0 +1,12 @@ + + + + + $(DupImportCount)1 + + + + DUP_IMPORT_COUNT=$(DupImportCount);%(PreprocessorDefinitions) + + + diff --git a/test/cli/vcxproj_import_graph/guarded.props b/test/cli/vcxproj_import_graph/guarded.props new file mode 100644 index 00000000000..a0c33fbbf98 --- /dev/null +++ b/test/cli/vcxproj_import_graph/guarded.props @@ -0,0 +1,11 @@ + + + + true + + + + GUARDED_DEFINE=1;%(PreprocessorDefinitions) + + + diff --git a/test/cli/vcxproj_import_graph/main.cpp b/test/cli/vcxproj_import_graph/main.cpp new file mode 100644 index 00000000000..bd828b0e725 --- /dev/null +++ b/test/cli/vcxproj_import_graph/main.cpp @@ -0,0 +1,20 @@ +// Each check below fails with #error when the import graph is not walked the way +// MSBuild / Visual Studio walk it; see vcxproj_import_graph_test.py. +#if !defined(GUARDED_DEFINE) +#error ItemDefinitionGroup of guarded.props (import guard) was not applied +#endif +#if DUP_IMPORT_COUNT != 1 +#error dup.props was imported more than once +#endif +#if !defined(DIRBUILD_DEFINE) +#error ItemDefinitionGroup of Directory.Build.props was not applied +#endif +#if !defined(PROJ_DEFINE) +#error ItemDefinitionGroup of the project was not applied +#endif + +int main() +{ + int x = 3 / 0; // ERROR + return x; +} diff --git a/test/cli/vcxproj_import_graph/vcxproj_import_graph.slnx b/test/cli/vcxproj_import_graph/vcxproj_import_graph.slnx new file mode 100644 index 00000000000..c740f7a1631 --- /dev/null +++ b/test/cli/vcxproj_import_graph/vcxproj_import_graph.slnx @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/cli/vcxproj_import_graph/vcxproj_import_graph.vcxproj b/test/cli/vcxproj_import_graph/vcxproj_import_graph.vcxproj new file mode 100644 index 00000000000..1a8d4720cb9 --- /dev/null +++ b/test/cli/vcxproj_import_graph/vcxproj_import_graph.vcxproj @@ -0,0 +1,55 @@ + + + + + Debug + x64 + + + + 17.0 + Win32Proj + {4f0c2b5e-1a7d-4c3e-9b8a-2d6e7f1c0a91} + vcxprojimportgraph + 10.0 + + + + Application + true + v143 + Unicode + + + + + + + + + + + + + + + + + + Level3 + _DEBUG;_CONSOLE;PROJ_DEFINE=1;%(PreprocessorDefinitions) + stdcpp17 + + + Console + + + + + + + + + diff --git a/test/cli/vcxproj_import_graph_test.py b/test/cli/vcxproj_import_graph_test.py new file mode 100644 index 00000000000..f72b8d7bd99 --- /dev/null +++ b/test/cli/vcxproj_import_graph_test.py @@ -0,0 +1,32 @@ +# python -m pytest vcxproj_import_graph_test.py +# +# MSBuild (and therefore Visual Studio) resolves the import graph exactly once, while +# evaluating properties, and then walks that same graph for item definitions and items. +# This fixture checks that cppcheck's three-phase evaluation does the same: +# - an guarded by a property that the imported file itself sets +# () +# still contributes its ItemDefinitionGroup +# - a file that is imported twice is processed only once (MSB4011) +# - Directory.Build.props, imported through the Microsoft.Cpp.Default.props emulation, +# contributes its ItemDefinitionGroup as well +# main.cpp turns every one of these into an #error, so the only expected diagnostic is +# the deliberate division by zero. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_import_graph(): + args = [ + '--template=cppcheck1', + '--project=vcxproj_import_graph/vcxproj_import_graph.slnx', + '--project-configuration=Debug|x64', + '--no-cppcheck-build-dir' + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + filename = os.path.join('vcxproj_import_graph', 'main.cpp') + assert stderr == '[%s:18]: (error) Division by zero.\n' % filename diff --git a/test/cli/vcxproj_import_wildcard/ImportBefore/a.props b/test/cli/vcxproj_import_wildcard/ImportBefore/a.props new file mode 100644 index 00000000000..33aea328de2 --- /dev/null +++ b/test/cli/vcxproj_import_wildcard/ImportBefore/a.props @@ -0,0 +1,8 @@ + + + + + WILDCARD_A_DEFINE=1;%(PreprocessorDefinitions) + + + diff --git a/test/cli/vcxproj_import_wildcard/ImportBefore/b.props b/test/cli/vcxproj_import_wildcard/ImportBefore/b.props new file mode 100644 index 00000000000..193b35a1fc7 --- /dev/null +++ b/test/cli/vcxproj_import_wildcard/ImportBefore/b.props @@ -0,0 +1,8 @@ + + + + + WILDCARD_B_DEFINE=1;%(PreprocessorDefinitions) + + + diff --git a/test/cli/vcxproj_import_wildcard/ImportBefore/decoy.txt b/test/cli/vcxproj_import_wildcard/ImportBefore/decoy.txt new file mode 100644 index 00000000000..a0ba20a47fb --- /dev/null +++ b/test/cli/vcxproj_import_wildcard/ImportBefore/decoy.txt @@ -0,0 +1,2 @@ +Not a .props file -- the "*.props" wildcard pattern must not attempt to load +this as XML. If it did, cppcheck would fail since this isn't valid XML. diff --git a/test/cli/vcxproj_import_wildcard/main.cpp b/test/cli/vcxproj_import_wildcard/main.cpp new file mode 100644 index 00000000000..79c249d89d3 --- /dev/null +++ b/test/cli/vcxproj_import_wildcard/main.cpp @@ -0,0 +1,18 @@ +// Each check below fails with #error unless the wildcard correctly expanded to import BOTH a.props and +// b.props (sorted, per MSBuild's own documented behavior), while skipping +// decoy.txt (extension doesn't match "*.props") and the nonexistent +// ImportAfter\*.props wildcard resolved to nothing without error. See +// vcxproj_import_wildcard_test.py. +#if !defined(WILDCARD_A_DEFINE) +#error ImportBefore/a.props (matched by wildcard Import) was not imported +#endif +#if !defined(WILDCARD_B_DEFINE) +#error ImportBefore/b.props (matched by wildcard Import) was not imported +#endif + +int main() +{ + int x = 3 / 0; // ERROR + return x; +} diff --git a/test/cli/vcxproj_import_wildcard/vcxproj_import_wildcard.vcxproj b/test/cli/vcxproj_import_wildcard/vcxproj_import_wildcard.vcxproj new file mode 100644 index 00000000000..938fe0931d2 --- /dev/null +++ b/test/cli/vcxproj_import_wildcard/vcxproj_import_wildcard.vcxproj @@ -0,0 +1,36 @@ + + + + + Debug + x64 + + + + {aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee} + importwildcardtest + 10.0 + + + + Application + v143 + + + + + + + + + + + + + diff --git a/test/cli/vcxproj_import_wildcard_test.py b/test/cli/vcxproj_import_wildcard_test.py new file mode 100644 index 00000000000..c9d06096607 --- /dev/null +++ b/test/cli/vcxproj_import_wildcard_test.py @@ -0,0 +1,43 @@ + +# python -m pytest vcxproj_import_wildcard_test.py +# +# Regression coverage for wildcard support: MSBuild allows wildcards in +# an 's Project attribute -- a *different*, general mechanism from the +# Visual Studio C++ project-system restriction against wildcards in project +# items (see vcxproj_split_configs_test.py and expandItemSpec()'s rejection of +# '*'/'?' there). This is the mechanism behind the ImportBefore/ImportAfter +# extensibility folders Visual Studio's own C++ toolchain files use, e.g. +# $(VCTargetsPath)\ImportBefore\Default\*.props -- but it applies equally to +# any ordinary user-authored wildcard , which is what this fixture +# exercises directly (rather than depending on the synthetic, hand-emulated +# Microsoft.Cpp.*.props/targets handlers, which do not read the real files and +# so never reach a wildcard import inside them). +# +# This fixture's must expand to +# BOTH a.props and b.props (sorted, "as if the order had been explicitly +# set" per Microsoft's own docs), while skipping decoy.txt (its extension +# doesn't match "*.props", so it must never even be attempted as XML -- it +# isn't valid XML, so a match would make cppcheck fail outright). A second +# pointing at a directory that does +# not exist at all must be a silent no-op, not an error. +# +# main.cpp turns any of these going wrong into a #error, so the only expected +# diagnostic is the deliberate division by zero. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_import_wildcard(): + args = [ + '--template=cppcheck1', + '--project=vcxproj_import_wildcard/vcxproj_import_wildcard.vcxproj', + '--no-cppcheck-build-dir' + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + filename = os.path.join('vcxproj_import_wildcard', 'main.cpp') + assert stderr == '[%s:16]: (error) Division by zero.\n' % filename diff --git a/test/cli/vcxproj_include_dir_macro/RealInc/myheader.h b/test/cli/vcxproj_include_dir_macro/RealInc/myheader.h new file mode 100644 index 00000000000..133cdac53de --- /dev/null +++ b/test/cli/vcxproj_include_dir_macro/RealInc/myheader.h @@ -0,0 +1 @@ +#define FROM_REAL_HEADER 1 diff --git a/test/cli/vcxproj_include_dir_macro/main.cpp b/test/cli/vcxproj_include_dir_macro/main.cpp new file mode 100644 index 00000000000..c8a0ea73fa6 --- /dev/null +++ b/test/cli/vcxproj_include_dir_macro/main.cpp @@ -0,0 +1,5 @@ +#include "myheader.h" +#ifndef FROM_REAL_HEADER +#error myheader.h (under RealInc, reached only via the unresolved AdditionalIncludeDirectories macro) was not found +#endif +int main() { return 0; } diff --git a/test/cli/vcxproj_include_dir_macro/vcxproj_include_dir_macro.vcxproj b/test/cli/vcxproj_include_dir_macro/vcxproj_include_dir_macro.vcxproj new file mode 100644 index 00000000000..f13d99fcf45 --- /dev/null +++ b/test/cli/vcxproj_include_dir_macro/vcxproj_include_dir_macro.vcxproj @@ -0,0 +1,39 @@ + + + + + Debug + x64 + + + + {dddddddd-dddd-dddd-dddd-dddddddddddd} + incdirmacrotest + 10.0 + + + + Application + v143 + + + + + + + $(CppcheckTestIncDirMacro)\RealInc;%(AdditionalIncludeDirectories) + + + + + + + + + diff --git a/test/cli/vcxproj_include_dir_macro_test.py b/test/cli/vcxproj_include_dir_macro_test.py new file mode 100644 index 00000000000..535ec94cd23 --- /dev/null +++ b/test/cli/vcxproj_include_dir_macro_test.py @@ -0,0 +1,59 @@ + +# python -m pytest vcxproj_include_dir_macro_test.py +# +# Regression coverage for an unresolvable macro in AdditionalIncludeDirectories +# (fsSetIncludePaths(), lib/importproject.cpp). Unlike a ClCompile item's own +# Include/Update/Remove path (see vcxproj_item_macro_path_test.py and friends), +# an include-search-path entry isn't restricted to config-invariant macros -- +# AdditionalIncludeDirectories is ordinary ItemDefinitionGroup metadata, +# expanded unconditionally like any other (see vcxproj_property_order_test.py). +# The problem here is different: when a macro in one entry genuinely can't be +# resolved at all (not a project property, not an environment variable), +# fsSetIncludePaths() used to silently drop that whole entry from the search +# path list. Any header that lived under it would then simply go unfound, with +# nothing in cppcheck's normal output pointing back at the real cause -- the +# only trace was an addDebug() call, and addDebug()/debugs is not surfaced by +# the CLI under any flag (it's accumulated but never read anywhere outside +# ImportProject itself). +# +# The fix keeps the entry (as its literal, unexpanded text -- a directory that +# can never exist is harmless to search) and records a normal, always-visible +# message in ImportProject::errors, printed unconditionally by the CLI exactly +# like every other project-import error -- not gated behind --debug like +# addDebug() traces are. +# +# This fixture's only item is main.cpp, which #includes +# "myheader.h" -- present only under RealInc/, reachable exclusively via +# $(CppcheckTestIncDirMacro)\RealInc, where CppcheckTestIncDirMacro is not +# defined anywhere (no PropertyGroup, and not expected to be a real +# environment variable). main.cpp's #error fires if and only if that header +# was not found, independently confirming the entry was really dropped from +# the search path rather than merely failing to warn. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_include_dir_macro(): + args = [ + '--project=vcxproj_include_dir_macro/vcxproj_include_dir_macro.vcxproj', + '--no-cppcheck-build-dir', + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + # A normal, always-visible message naming the exact unresolved macro path -- + # not silently dropped, and not hidden behind --debug. + assert "cppcheck: error: AdditionalIncludeDirectories entry has an unresolved macro, " \ + "include path will not be found: '$(CppcheckTestIncDirMacro)/RealInc'" in stdout, stdout + + # myheader.h under RealInc/ must NOT have been found -- the unresolved + # entry stays inert (literal, matching no real directory), it is not + # somehow resolved anyway. main.cpp's own #error is the independent proof. + filename = 'vcxproj_include_dir_macro/main.cpp' + normalized_stderr = stderr.replace('\\', '/') + assert ('%s:3:2: error: #error myheader.h (under RealInc, reached only via the unresolved ' + 'AdditionalIncludeDirectories macro) was not found [preprocessorErrorDirective]' % filename) in normalized_stderr, stderr diff --git a/test/cli/vcxproj_item_absolute_drive_path/vcxproj_item_absolute_drive_path.vcxproj b/test/cli/vcxproj_item_absolute_drive_path/vcxproj_item_absolute_drive_path.vcxproj new file mode 100644 index 00000000000..26a6097d11f --- /dev/null +++ b/test/cli/vcxproj_item_absolute_drive_path/vcxproj_item_absolute_drive_path.vcxproj @@ -0,0 +1,36 @@ + + + + + Debug + x64 + + + + {dddddddd-eeee-ffff-0000-111111111111} + itemabsolutedrivepathtest + 10.0 + + + + Application + v143 + + + + + + + + + + diff --git a/test/cli/vcxproj_item_absolute_drive_path_test.py b/test/cli/vcxproj_item_absolute_drive_path_test.py new file mode 100644 index 00000000000..de22d09de1e --- /dev/null +++ b/test/cli/vcxproj_item_absolute_drive_path_test.py @@ -0,0 +1,49 @@ + +# python -m pytest vcxproj_item_absolute_drive_path_test.py +# +# Regression coverage for ImportProject::toAbsoluteExpanded() using the +# host-dependent Path::isAbsolute() to decide whether a ClCompile item path +# is already fully rooted (lib/importproject.cpp). Path::isAbsolute() only +# recognizes a leading '/' on a non-Windows host, so a Windows +# drive-absolute path straight out of a .vcxproj -- e.g. "C:/nonexistent/foo.cpp", +# with no macro involved at all -- would be misclassified as relative and +# joined onto the project directory instead of used as-is, producing +# something like "/C:/nonexistent/foo.cpp" -- not what real +# Visual Studio does (and not what toAbsoluteExpanded() itself would do +# running natively on Windows, where Path::isAbsolute() DOES recognize +# "C:\..." as absolute). The fix uses classifyPath() -- the same +# host-independent Windows/Unix path classification already used throughout +# this file (see pathCombineAppend(), toAbsolute(), etc.) -- instead. +# +# This fixture's only item is Include="C:\nonexistent\foo.cpp", +# a plain drive-absolute path with nothing to macro-expand, so +# expandItemSpec() passes it straight through to toAbsoluteExpanded(). + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_item_absolute_drive_path(): + args = [ + '--project=vcxproj_item_absolute_drive_path/vcxproj_item_absolute_drive_path.vcxproj', + '--no-cppcheck-build-dir', + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + normalized_stdout = stdout.replace('\\', '/') + normalized_stderr = stderr.replace('\\', '/') + + # The drive-absolute path must be used exactly as-is -- not joined onto + # the project directory. + assert 'Checking C:/nonexistent/foo.cpp Debug|x64...' in normalized_stdout, stdout + assert normalized_stderr.count('C:/nonexistent/foo.cpp:0:0: error: File is missing: C:/nonexistent/foo.cpp [missingFile]') == 1, stderr + + # It must NOT have been nested under the project directory -- that would + # mean toAbsoluteExpanded() wrongly treated the already-absolute path as + # relative and joined it onto projectDir. + assert 'vcxproj_item_absolute_drive_path/C:' not in normalized_stdout, stdout + assert 'vcxproj_item_absolute_drive_path/C:' not in normalized_stderr, stderr diff --git a/test/cli/vcxproj_item_condition/foo.cpp b/test/cli/vcxproj_item_condition/foo.cpp new file mode 100644 index 00000000000..76e8197013a --- /dev/null +++ b/test/cli/vcxproj_item_condition/foo.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/test/cli/vcxproj_item_condition/vcxproj_item_condition.vcxproj b/test/cli/vcxproj_item_condition/vcxproj_item_condition.vcxproj new file mode 100644 index 00000000000..2c293865e2b --- /dev/null +++ b/test/cli/vcxproj_item_condition/vcxproj_item_condition.vcxproj @@ -0,0 +1,39 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {66666666-6666-6666-6666-666666666666} + itemcondtest + 10.0 + + + + Application + v143 + + + Application + v143 + + + + + + + + + + diff --git a/test/cli/vcxproj_item_condition_test.py b/test/cli/vcxproj_item_condition_test.py new file mode 100644 index 00000000000..5788df861fd --- /dev/null +++ b/test/cli/vcxproj_item_condition_test.py @@ -0,0 +1,47 @@ + +# python -m pytest vcxproj_item_condition_test.py +# +# Regression coverage for Condition on a ClCompile *project item* itself +# (as opposed to Condition on the item's metadata children, which is a +# different, fully-supported feature -- see applyClCompileChild()). +# +# Microsoft documents that the Visual Studio C++ project system does not +# support Condition on project items: "Conditions aren't supported for +# Project items (that is, item types that are treated as project items by +# rules definitions)." -- see +# https://learn.microsoft.com/en-us/cpp/build/reference/vcxproj-file-structure +# +# This fixture's only item is Include="foo.cpp" +# Condition="'$(Configuration)'=='Debug'". A discovery/evaluation pass that +# honors that Condition (i.e. treats the item like any other conditioned +# element) drops foo.cpp entirely when evaluating Release|x64 -- but real +# Visual Studio ignores the Condition on the item itself and always includes +# foo.cpp, in both Debug and Release. So foo.cpp must be checked under both +# configurations. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_item_condition(): + args = [ + '--project=vcxproj_item_condition/vcxproj_item_condition.vcxproj', + '--no-cppcheck-build-dir', + '--dump' + ] + ret, stdout, _ = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + # Windows prints native '\' path separators ("Checking foo\main.cpp ..."); + # normalize before matching so this passes on every platform (same idiom + # used by test_log() in clang-import_test.py). + normalized_stdout = stdout.replace('\\', '/') + + # foo.cpp's Condition is gated on Configuration=='Debug', but Visual + # Studio does not evaluate Condition on project items -- so foo.cpp must + # be checked under BOTH configurations, not just Debug. + assert 'Checking vcxproj_item_condition/foo.cpp Debug|x64' in normalized_stdout, stdout + assert 'Checking vcxproj_item_condition/foo.cpp Release|x64' in normalized_stdout, stdout diff --git a/test/cli/vcxproj_item_macro_path/SomeDir/foo.cpp b/test/cli/vcxproj_item_macro_path/SomeDir/foo.cpp new file mode 100644 index 00000000000..76e8197013a --- /dev/null +++ b/test/cli/vcxproj_item_macro_path/SomeDir/foo.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/test/cli/vcxproj_item_macro_path/vcxproj_item_macro_path.vcxproj b/test/cli/vcxproj_item_macro_path/vcxproj_item_macro_path.vcxproj new file mode 100644 index 00000000000..639f8d759e2 --- /dev/null +++ b/test/cli/vcxproj_item_macro_path/vcxproj_item_macro_path.vcxproj @@ -0,0 +1,52 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {77777777-7777-7777-7777-777777777777} + itemmacropathtest + 10.0 + + + + Application + v143 + + + Application + v143 + + + + + + SomeDir + + + SomeOtherDir + + + + + + + diff --git a/test/cli/vcxproj_item_macro_path_case_builtin/foo.cpp b/test/cli/vcxproj_item_macro_path_case_builtin/foo.cpp new file mode 100644 index 00000000000..c5e4ff6a07d --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_case_builtin/foo.cpp @@ -0,0 +1,8 @@ +// Reached only if $(msbuildprojectdirectory)\foo.cpp was correctly +// recognized as the (case-insensitive) built-in MSBuildProjectDirectory +// property and expanded -- see vcxproj_item_macro_path_case_test.py. +int main() +{ + int x = 3 / 0; // ERROR + return x; +} diff --git a/test/cli/vcxproj_item_macro_path_case_builtin/vcxproj_item_macro_path_case_builtin.vcxproj b/test/cli/vcxproj_item_macro_path_case_builtin/vcxproj_item_macro_path_case_builtin.vcxproj new file mode 100644 index 00000000000..b8babcfcd02 --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_case_builtin/vcxproj_item_macro_path_case_builtin.vcxproj @@ -0,0 +1,35 @@ + + + + + Debug + x64 + + + + {bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb} + itemmacropathcasebuiltintest + 10.0 + + + + Application + v143 + + + + + + + + + + diff --git a/test/cli/vcxproj_item_macro_path_case_invariant/SameCaseDir/mathplot.cpp b/test/cli/vcxproj_item_macro_path_case_invariant/SameCaseDir/mathplot.cpp new file mode 100644 index 00000000000..f35ae700ca5 --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_case_invariant/SameCaseDir/mathplot.cpp @@ -0,0 +1,8 @@ +// Reached only if $(MYROOT)\mathplot.cpp was correctly recognized as the +// same config-invariant property as MyRoot/myroot and expanded -- see +// vcxproj_item_macro_path_case_test.py. +int main() +{ + int x = 3 / 0; // ERROR + return x; +} diff --git a/test/cli/vcxproj_item_macro_path_case_invariant/vcxproj_item_macro_path_case_invariant.vcxproj b/test/cli/vcxproj_item_macro_path_case_invariant/vcxproj_item_macro_path_case_invariant.vcxproj new file mode 100644 index 00000000000..29810cf054e --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_case_invariant/vcxproj_item_macro_path_case_invariant.vcxproj @@ -0,0 +1,49 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa} + itemmacropathcaseinvarianttest + 10.0 + + + + Application + v143 + + + Application + v143 + + + + + + SameCaseDir + + + SameCaseDir + + + + + + + diff --git a/test/cli/vcxproj_item_macro_path_case_test.py b/test/cli/vcxproj_item_macro_path_case_test.py new file mode 100644 index 00000000000..d4e80820ed5 --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_case_test.py @@ -0,0 +1,107 @@ + +# python -m pytest vcxproj_item_macro_path_case_test.py +# +# Regression coverage for MSBuild property-name case-insensitivity in the +# ClCompile item-path macro analysis (expandItemSpec() / +# hasOnlyInvariantItemPathVariables(), lib/importproject.cpp). Real MSBuild +# property names are case-insensitive -- PropertiesMap itself already uses +# cppcheck::stricmp for exactly this reason -- but the config-invariance +# analysis this feature added (mConfigInvariantProperties, the priming-pass +# valuesByProperty map, and the fixed invariantItemPathProperties() set) used +# plain, case-sensitive std::set/std::map. That mismatch has two distinct, +# independently regressed failure modes: +# +# 1. A property genuinely set to DIFFERENT values in different +# configurations, but spelled with different casing in each +# PropertyGroup (e.g. for Debug, for Release), gets +# tracked as two unrelated single-valued names instead of one +# multi-valued one -- each looks (wrongly) invariant on its own, so +# cppcheck would expand and check two different real files instead of +# correctly leaving the item unexpanded (matching real Visual Studio's +# documented restriction -- see vcxproj_item_macro_path_test.py). +# test_vcxproj_item_macro_path_case_variant covers this. +# +# 2. A property that genuinely IS config-invariant (same value everywhere), +# or one of the fixed MSBuildThisFile.../MSBuildProject... properties, +# fails to be recognized as such if it's spelled with different casing +# across the PropertyGroups that set it and/or the item path that +# references it -- real Visual Studio resolves it regardless, but +# cppcheck would wrongly reject it and report a real, checkable file as +# missing. test_vcxproj_item_macro_path_case_invariant (a user-defined +# property spelled three different ways) and +# test_vcxproj_item_macro_path_case_builtin (a fixed MSBuild property +# referenced in lowercase) cover this. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_item_macro_path_case_variant(): + args = [ + '--project=vcxproj_item_macro_path_case_variant/vcxproj_item_macro_path_case_variant.vcxproj', + '--no-cppcheck-build-dir', + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + normalized_stdout = stdout.replace('\\', '/') + normalized_stderr = stderr.replace('\\', '/') + literal_path = 'vcxproj_item_macro_path_case_variant/$(CaseRoot)/foo.cpp' + + # $(CaseRoot) genuinely varies by configuration -- "CaseRoot" (Debug) and + # "caseroot" (Release) are the same MSBuild property, just spelled + # differently -- so it must NOT be expanded in either configuration, + # exactly like vcxproj_item_macro_path_test.py's same-casing case. + assert ('Checking %s Debug|x64...' % literal_path) in normalized_stdout, stdout + assert ('Checking %s Release|x64...' % literal_path) in normalized_stdout, stdout + assert normalized_stderr.count('%s:0:0: error: File is missing: %s [missingFile]' % (literal_path, literal_path)) == 1, stderr + + # The real files under DebugCaseDir/ and ReleaseCaseDir/ must never be + # reached -- if they were, their #error would surface here instead, and + # that would mean the case-sensitivity bug let a genuinely-varying + # property slip through as "invariant". + assert 'DebugCaseDir/foo.cpp' not in normalized_stdout, stdout + assert 'ReleaseCaseDir/foo.cpp' not in normalized_stdout, stdout + + +def test_vcxproj_item_macro_path_case_invariant(): + args = [ + '--project=vcxproj_item_macro_path_case_invariant/vcxproj_item_macro_path_case_invariant.vcxproj', + '--no-cppcheck-build-dir', + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + normalized_stdout = stdout.replace('\\', '/') + filename = 'vcxproj_item_macro_path_case_invariant/SameCaseDir/mathplot.cpp' + + # $(MYROOT) must resolve in both configurations to the one property real + # Visual Studio sees, however it's spelled at each definition/reference + # site (MyRoot / myroot / MYROOT) -- not be rejected as unsupported. + assert ('Checking %s Debug|x64...' % filename) in normalized_stdout, stdout + assert ('Checking %s Release|x64...' % filename) in normalized_stdout, stdout + + normalized_stderr = stderr.replace('\\', '/') + assert normalized_stderr.count('%s:6:15: error: Division by zero.' % filename) == 1, stderr + + +def test_vcxproj_item_macro_path_case_builtin(): + args = [ + '--project=vcxproj_item_macro_path_case_builtin/vcxproj_item_macro_path_case_builtin.vcxproj', + '--no-cppcheck-build-dir', + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + normalized_stdout = stdout.replace('\\', '/') + filename = 'vcxproj_item_macro_path_case_builtin/foo.cpp' + + # $(msbuildprojectdirectory), all lowercase, must still be recognized as + # the fixed MSBuildProjectDirectory property and expanded. + assert ('Checking %s Debug|x64...' % filename) in normalized_stdout, stdout + + normalized_stderr = stderr.replace('\\', '/') + assert normalized_stderr.count('%s:6:15: error: Division by zero.' % filename) == 1, stderr diff --git a/test/cli/vcxproj_item_macro_path_case_variant/DebugCaseDir/foo.cpp b/test/cli/vcxproj_item_macro_path_case_variant/DebugCaseDir/foo.cpp new file mode 100644 index 00000000000..9a9e81d27c6 --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_case_variant/DebugCaseDir/foo.cpp @@ -0,0 +1,2 @@ +#error DebugCaseDir/foo.cpp must never be checked -- $(CaseRoot) genuinely varies by configuration (case-insensitively) and must not be expanded +int main() { return 0; } diff --git a/test/cli/vcxproj_item_macro_path_case_variant/ReleaseCaseDir/foo.cpp b/test/cli/vcxproj_item_macro_path_case_variant/ReleaseCaseDir/foo.cpp new file mode 100644 index 00000000000..5d4ef0c337c --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_case_variant/ReleaseCaseDir/foo.cpp @@ -0,0 +1,2 @@ +#error ReleaseCaseDir/foo.cpp must never be checked -- $(CaseRoot) genuinely varies by configuration (case-insensitively) and must not be expanded +int main() { return 0; } diff --git a/test/cli/vcxproj_item_macro_path_case_variant/vcxproj_item_macro_path_case_variant.vcxproj b/test/cli/vcxproj_item_macro_path_case_variant/vcxproj_item_macro_path_case_variant.vcxproj new file mode 100644 index 00000000000..cd79684f61f --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_case_variant/vcxproj_item_macro_path_case_variant.vcxproj @@ -0,0 +1,53 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {99999999-9999-9999-9999-999999999999} + itemmacropathcasevarianttest + 10.0 + + + + Application + v143 + + + Application + v143 + + + + + + DebugCaseDir + + + ReleaseCaseDir + + + + + + + diff --git a/test/cli/vcxproj_item_macro_path_env/EnvLibRoot/mathplot.cpp b/test/cli/vcxproj_item_macro_path_env/EnvLibRoot/mathplot.cpp new file mode 100644 index 00000000000..6300635aa46 --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_env/EnvLibRoot/mathplot.cpp @@ -0,0 +1,8 @@ +// Reached only if $(CppcheckTestEnvLibRoot)\mathplot.cpp was correctly +// resolved via the CppcheckTestEnvLibRoot environment variable -- see +// vcxproj_item_macro_path_env_test.py. +int main() +{ + int x = 3 / 0; // ERROR + return x; +} diff --git a/test/cli/vcxproj_item_macro_path_env/vcxproj_item_macro_path_env.vcxproj b/test/cli/vcxproj_item_macro_path_env/vcxproj_item_macro_path_env.vcxproj new file mode 100644 index 00000000000..304c471589d --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_env/vcxproj_item_macro_path_env.vcxproj @@ -0,0 +1,45 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {99999999-9999-9999-9999-999999999999} + itemmacropathenvtest + 10.0 + + + + Application + v143 + + + Application + v143 + + + + + + + + + + diff --git a/test/cli/vcxproj_item_macro_path_env_test.py b/test/cli/vcxproj_item_macro_path_env_test.py new file mode 100644 index 00000000000..ff0e7a99b66 --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_env_test.py @@ -0,0 +1,74 @@ + +# python -m pytest vcxproj_item_macro_path_env_test.py +# +# Regression coverage for the environment-variable fallback in +# expandItemSpec()'s ClCompile item-path macro check (see +# vcxproj_item_macro_path_test.py / vcxproj_item_macro_path_invariant_test.py +# for the general rule this is a special case of): a $(...) reference in an +# Include/Update/Remove path to a property no PropertyGroup or property +# sheet in the project ever sets, but that resolves via a real OS +# environment variable of the same name, is config-invariant exactly like a +# property-sheet constant is -- an environment variable has exactly one +# value for the whole cppcheck invocation, it cannot differ between +# Debug|x64 and Release|x64 -- and real MSBuild property evaluation itself +# falls back to the environment for anything nothing else defines. So real +# Visual Studio resolves it, and cppcheck must too. +# +# This fixture's only item is +# Include="$(CppcheckTestEnvLibRoot)\mathplot.cpp", where +# CppcheckTestEnvLibRoot is not defined anywhere in the project itself. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_item_macro_path_env_var_set(): + env = os.environ.copy() + env['CppcheckTestEnvLibRoot'] = 'EnvLibRoot' + + args = [ + '--project=vcxproj_item_macro_path_env/vcxproj_item_macro_path_env.vcxproj', + '--no-cppcheck-build-dir', + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir, env=env) + assert ret == 0, stdout + + normalized_stdout = stdout.replace('\\', '/') + filename = 'vcxproj_item_macro_path_env/EnvLibRoot/mathplot.cpp' + + # Both configurations must resolve $(CppcheckTestEnvLibRoot)\mathplot.cpp + # via the environment variable to the same real file and actually check + # it -- not treat the macro as unsupported just because no PropertyGroup + # in the project itself ever set it. + assert ('Checking %s Debug|x64...' % filename) in normalized_stdout, stdout + assert ('Checking %s Release|x64...' % filename) in normalized_stdout, stdout + + normalized_stderr = stderr.replace('\\', '/') + assert normalized_stderr.count('%s:6:15: error: Division by zero.' % filename) == 1, stderr + + +def test_vcxproj_item_macro_path_env_var_unset(): + env = os.environ.copy() + env.pop('CppcheckTestEnvLibRoot', None) + + args = [ + '--project=vcxproj_item_macro_path_env/vcxproj_item_macro_path_env.vcxproj', + '--no-cppcheck-build-dir', + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir, env=env) + assert ret == 0, stdout + + normalized_stdout = stdout.replace('\\', '/') + normalized_stderr = stderr.replace('\\', '/') + literal_path = 'vcxproj_item_macro_path_env/$(CppcheckTestEnvLibRoot)/mathplot.cpp' + + # With no environment variable and no project property defining it + # either, $(CppcheckTestEnvLibRoot) is genuinely unresolvable -- the item + # must still be checked (and reported missing) under its literal path in + # both configurations, not silently dropped. + assert ('Checking %s Debug|x64...' % literal_path) in normalized_stdout, stdout + assert ('Checking %s Release|x64...' % literal_path) in normalized_stdout, stdout + assert normalized_stderr.count('%s:0:0: error: File is missing: %s [missingFile]' % (literal_path, literal_path)) == 1, stderr diff --git a/test/cli/vcxproj_item_macro_path_hyphen/HyphenDir/foo.cpp b/test/cli/vcxproj_item_macro_path_hyphen/HyphenDir/foo.cpp new file mode 100644 index 00000000000..7bb863299e2 --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_hyphen/HyphenDir/foo.cpp @@ -0,0 +1,10 @@ +// Reached only if $(My-Root)\foo.cpp was correctly recognized as a +// config-invariant property reference and expanded -- which requires the +// hyphen in "My-Root" to be accepted as part of the property name by +// hasOnlyInvariantItemPathVariables()'s inline scanner. See +// vcxproj_item_macro_path_hyphen_test.py. +int main() +{ + int x = 3 / 0; // ERROR + return x; +} diff --git a/test/cli/vcxproj_item_macro_path_hyphen/vcxproj_item_macro_path_hyphen.vcxproj b/test/cli/vcxproj_item_macro_path_hyphen/vcxproj_item_macro_path_hyphen.vcxproj new file mode 100644 index 00000000000..bab49b16519 --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_hyphen/vcxproj_item_macro_path_hyphen.vcxproj @@ -0,0 +1,45 @@ + + + + + Debug + x64 + + + + {bbbbbbbb-cccc-dddd-eeee-ffffffffffff} + itemmacropathhyphentest + 10.0 + + + + Application + v143 + + + + + + HyphenDir + + + + + + + diff --git a/test/cli/vcxproj_item_macro_path_hyphen_test.py b/test/cli/vcxproj_item_macro_path_hyphen_test.py new file mode 100644 index 00000000000..3218e751f1f --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_hyphen_test.py @@ -0,0 +1,49 @@ + +# python -m pytest vcxproj_item_macro_path_hyphen_test.py +# +# Regression coverage for hasOnlyInvariantItemPathVariables()'s inline +# property-name scanner rejecting '-' (lib/importproject.cpp). Real MSBuild +# property names are [A-Za-z_][A-Za-z0-9_-]* -- '-' IS valid after the +# first character, e.g. HyphenDir referenced as +# $(My-Root) -- but the scanner used to decide whether a ClCompile item +# path's macro references are all safely config-invariant only accepted +# [A-Za-z0-9_], so it stopped at the first '-' and captured a truncated +# name. Since the character right after that truncated name was '-' (not +# the required closing ')'), the whole reference was rejected as "not +# provably invariant" and left unexpanded, even though this property is +# only ever set once, to one value -- genuinely invariant, exactly like +# vcxproj_item_macro_path_invariant_test.py's ExternalLibRoot. See +# vcxproj_item_macro_path_hyphen/vcxproj_item_macro_path_hyphen.vcxproj. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_item_macro_path_hyphen(): + args = [ + '--project=vcxproj_item_macro_path_hyphen/vcxproj_item_macro_path_hyphen.vcxproj', + '--no-cppcheck-build-dir', + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + normalized_stdout = stdout.replace('\\', '/') + filename = 'vcxproj_item_macro_path_hyphen/HyphenDir/foo.cpp' + + # $(My-Root) must resolve to "HyphenDir" and be expanded -- reaching and + # checking the real file, not being left as the literal, un-expandable + # "$(My-Root)\foo.cpp" text. + assert ('Checking %s Debug|x64...' % filename) in normalized_stdout, stdout + + normalized_stderr = stderr.replace('\\', '/') + assert normalized_stderr.count('%s:8:15: error: Division by zero.' % filename) == 1, stderr + + # It must never be reported as a missing file under the literal, + # unexpanded macro text -- that would mean the hyphen truncated the + # scanned property name and the reference was wrongly rejected as + # "not provably invariant". + assert '$(My-Root)' not in normalized_stdout, stdout + assert '$(My-Root)' not in normalized_stderr, stderr diff --git a/test/cli/vcxproj_item_macro_path_invariant/ExternalLibRoot/mathplot.cpp b/test/cli/vcxproj_item_macro_path_invariant/ExternalLibRoot/mathplot.cpp new file mode 100644 index 00000000000..7c43e7f9d47 --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_invariant/ExternalLibRoot/mathplot.cpp @@ -0,0 +1,7 @@ +// Reached only if $(ExternalLibRoot)\mathplot.cpp was correctly expanded and +// checked -- see vcxproj_item_macro_path_invariant_test.py. +int main() +{ + int x = 3 / 0; // ERROR + return x; +} diff --git a/test/cli/vcxproj_item_macro_path_invariant/vcxproj_item_macro_path_invariant.vcxproj b/test/cli/vcxproj_item_macro_path_invariant/vcxproj_item_macro_path_invariant.vcxproj new file mode 100644 index 00000000000..cc812ac6072 --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_invariant/vcxproj_item_macro_path_invariant.vcxproj @@ -0,0 +1,51 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {88888888-8888-8888-8888-888888888888} + itemmacropathinvarianttest + 10.0 + + + + Application + v143 + + + Application + v143 + + + + + + ExternalLibRoot + + + + + + + diff --git a/test/cli/vcxproj_item_macro_path_invariant_test.py b/test/cli/vcxproj_item_macro_path_invariant_test.py new file mode 100644 index 00000000000..a7928ad5c46 --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_invariant_test.py @@ -0,0 +1,52 @@ + +# python -m pytest vcxproj_item_macro_path_invariant_test.py +# +# Regression coverage for the flip side of vcxproj_item_macro_path_test.py: +# a macro in a ClCompile project item's Include/Update/Remove path whose +# value is the SAME in every one of the project's configurations. Real Visual +# Studio has no trouble resolving this -- it's the well-known pattern of a +# property sheet (or, as here, an unconditioned PropertyGroup) defining a +# fixed third-party library location, e.g. $(wxMathPlot) or $(BoostRoot), and +# referencing it directly in an Include path. Microsoft's documented +# restriction is specifically about macros whose value COULD differ by +# configuration -- see vcxproj_item_macro_path_test.py and +# https://learn.microsoft.com/en-us/cpp/build/reference/vcxproj-file-structure +# -- so a config-invariant macro like this one is not the case that +# restriction describes, and cppcheck must expand it just like real Visual +# Studio does, not treat it the same as a genuinely varying one. +# +# This fixture has two configurations (Debug|x64, Release|x64) and one +# , where +# ExternalLibRoot is set once, unconditionally, to the same value regardless +# of configuration. main.cpp's deliberate division by zero proves the file +# was actually found and checked (not silently skipped as unresolved) in +# every configuration. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_item_macro_path_invariant(): + args = [ + '--project=vcxproj_item_macro_path_invariant/vcxproj_item_macro_path_invariant.vcxproj', + '--no-cppcheck-build-dir', + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + normalized_stdout = stdout.replace('\\', '/') + filename = 'vcxproj_item_macro_path_invariant/ExternalLibRoot/mathplot.cpp' + + # Both configurations must resolve $(ExternalLibRoot)\mathplot.cpp to the + # same real file and actually check it -- not treat the macro as + # unsupported and end up with no valid source files. + assert ('Checking %s Debug|x64...' % filename) in normalized_stdout, stdout + assert ('Checking %s Release|x64...' % filename) in normalized_stdout, stdout + + # Same underlying file checked under both configurations -- cppcheck + # dedups the identical diagnostic, so it's reported exactly once. + normalized_stderr = stderr.replace('\\', '/') + assert normalized_stderr.count('%s:5:15: error: Division by zero.' % filename) == 1, stderr diff --git a/test/cli/vcxproj_item_macro_path_missing_config/DebugDir/foo.cpp b/test/cli/vcxproj_item_macro_path_missing_config/DebugDir/foo.cpp new file mode 100644 index 00000000000..0e5ed632320 --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_missing_config/DebugDir/foo.cpp @@ -0,0 +1,2 @@ +#error DebugDir/foo.cpp must never be checked -- $(MyRoot) is unset in Release|x64, so it genuinely varies by configuration and must not be expanded +int main() { return 0; } diff --git a/test/cli/vcxproj_item_macro_path_missing_config/vcxproj_item_macro_path_missing_config.vcxproj b/test/cli/vcxproj_item_macro_path_missing_config/vcxproj_item_macro_path_missing_config.vcxproj new file mode 100644 index 00000000000..628207b23b5 --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_missing_config/vcxproj_item_macro_path_missing_config.vcxproj @@ -0,0 +1,50 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {cccccccc-cccc-cccc-cccc-cccccccccccc} + itemmacropathmissingconfigtest + 10.0 + + + + Application + v143 + + + Application + v143 + + + + + + DebugDir + + + + + + + diff --git a/test/cli/vcxproj_item_macro_path_missing_config_test.py b/test/cli/vcxproj_item_macro_path_missing_config_test.py new file mode 100644 index 00000000000..2100e0dc02f --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_missing_config_test.py @@ -0,0 +1,55 @@ + +# python -m pytest vcxproj_item_macro_path_missing_config_test.py +# +# Regression coverage for mConfigInvariantProperties's priming pass only +# recording a property's value in configurations where it is actually SET +# (lib/importproject.cpp). A property a PropertyGroup sets in only some of a +# project's configurations -- with no PropertyGroup for it at all in the +# others -- still genuinely varies by configuration: real MSBuild resolves +# $(MyRoot) to the PropertyGroup's value under the configuration that sets +# it, and to "" under every configuration that doesn't (no PropertyGroup, no +# matching OS environment variable -- see +# vcxproj_item_macro_path_env_test.py for the case where one does exist). +# Naively collecting only the values each configuration's own property map +# happens to contain misses this: a property set in exactly one +# configuration has exactly one recorded value there and looks spuriously +# invariant, when the real, config-varying picture is "one real value in one +# configuration, empty in the rest" -- two distinct values, not one. +# +# This fixture's only item is Include="$(MyRoot)\foo.cpp", where +# MyRoot is set only by Debug|x64's PropertyGroup; Release|x64 has none. +# DebugDir/foo.cpp's #error independently proves the item was never expanded +# and checked under Debug -- only that it consistently stays literal (and +# reported missing) in both configurations, exactly like +# vcxproj_item_macro_path_test.py's same-two-explicit-values case. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_item_macro_path_missing_config(): + args = [ + '--project=vcxproj_item_macro_path_missing_config/vcxproj_item_macro_path_missing_config.vcxproj', + '--no-cppcheck-build-dir', + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + normalized_stdout = stdout.replace('\\', '/') + normalized_stderr = stderr.replace('\\', '/') + literal_path = 'vcxproj_item_macro_path_missing_config/$(MyRoot)/foo.cpp' + + # $(MyRoot) is unset in Release|x64 -- it genuinely varies by + # configuration -- so it must NOT be expanded in EITHER configuration, + # not just the one where it happens to be unset. + assert ('Checking %s Debug|x64...' % literal_path) in normalized_stdout, stdout + assert ('Checking %s Release|x64...' % literal_path) in normalized_stdout, stdout + assert normalized_stderr.count('%s:0:0: error: File is missing: %s [missingFile]' % (literal_path, literal_path)) == 1, stderr + + # DebugDir/foo.cpp must never be reached -- if it were, its #error would + # surface here instead, meaning MyRoot's being unset in Release wrongly + # let Debug's value be treated as config-invariant. + assert 'DebugDir/foo.cpp' not in normalized_stdout, stdout diff --git a/test/cli/vcxproj_item_macro_path_test.py b/test/cli/vcxproj_item_macro_path_test.py new file mode 100644 index 00000000000..1cb35164492 --- /dev/null +++ b/test/cli/vcxproj_item_macro_path_test.py @@ -0,0 +1,69 @@ + +# python -m pytest vcxproj_item_macro_path_test.py +# +# Regression coverage for macro ($(...)) expansion in a ClCompile project +# item's Include/Update/Remove path (as opposed to expansion elsewhere, e.g. +# in metadata values or Condition attributes, which is unaffected). +# +# Microsoft documents that the Visual Studio C++ project system does not +# reliably resolve a macro in a project item path if that macro's value could +# differ by configuration -- "The IDE doesn't expect project item paths to be +# different for different project configurations" -- see +# https://learn.microsoft.com/en-us/cpp/build/reference/vcxproj-file-structure +# That is specifically about macros whose value VARIES by configuration: the +# fixed set of MSBuild "this file"/"this project" location properties +# (MSBuildThisFileDirectory and friends) are always exempt, since they can't +# vary by construction -- see test/cli/shared-items-project, covered +# separately by test_shared_items_project() in more-projects_test.py -- and so +# is any other property THIS project happens to resolve to the same value in +# every one of its configurations, e.g. one a property sheet sets once, +# unconditionally (see vcxproj_item_macro_path_invariant_test.py). +# +# This fixture's only item is Include="$(SomeDir)\foo.cpp", where +# SomeDir is an ordinary user-defined property that genuinely differs between +# the project's two configurations (SomeDir for Debug|x64, SomeOtherDir for +# Release|x64) -- exactly the case Visual Studio can't reliably resolve, since +# a single Solution Explorer file list can't show two different files +# depending on which configuration happens to be active. Expanding $(SomeDir) +# here would resolve to a real file that genuinely exists on disk in either +# configuration -- but real Visual Studio does not expand it, so cppcheck +# must not either. +# +# Critically, "must not expand it" does not mean the item silently vanishes: +# a user whose real project has a file like this would have no way to know +# it went unchecked. So the item is still kept, under its literal, +# unexpanded path ("vcxproj_item_macro_path/$(SomeDir)/foo.cpp") -- which +# can never exist on disk -- and cppcheck reports it exactly like any other +# missing source file, a normal, visible "File is missing: ..." error that +# also names the exact unresolved macro path, not just an addDebug() trace +# nobody but a --debug run would ever see. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_item_macro_path(): + args = [ + '--project=vcxproj_item_macro_path/vcxproj_item_macro_path.vcxproj', + '--no-cppcheck-build-dir', + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + normalized_stdout = stdout.replace('\\', '/') + normalized_stderr = stderr.replace('\\', '/') + literal_path = 'vcxproj_item_macro_path/$(SomeDir)/foo.cpp' + + # $(SomeDir) must NOT be expanded -- Visual Studio can't reliably resolve + # it either, since it genuinely differs between configurations -- but the + # item must still be checked (and fail to be found) under its literal, + # unexpanded path, in both configurations, rather than disappearing. + assert ('Checking %s Debug|x64...' % literal_path) in normalized_stdout, stdout + assert ('Checking %s Release|x64...' % literal_path) in normalized_stdout, stdout + + # Same literal (nonexistent) path in both configurations -- cppcheck + # dedups the identical diagnostic, so it's reported exactly once. + assert normalized_stderr.count('%s:0:0: error: File is missing: %s [missingFile]' % (literal_path, literal_path)) == 1, stderr diff --git a/test/cli/vcxproj_metadata_self_reference_case/foo.cpp b/test/cli/vcxproj_metadata_self_reference_case/foo.cpp new file mode 100644 index 00000000000..449a9a7862f --- /dev/null +++ b/test/cli/vcxproj_metadata_self_reference_case/foo.cpp @@ -0,0 +1,21 @@ +// Reached (division-by-zero branch) only if BOTH BASE_DEFINE and +// EXTRA_DEFINE ended up defined -- which requires addMetadata()'s +// case-mismatched "$(preprocessordefinitions)" self-reference (see the +// second ItemDefinitionGroup in the .vcxproj) to have correctly resolved to +// "BASE_DEFINE", accumulated from the first ItemDefinitionGroup. Before the +// fix, that self-reference was left as literal, unresolved macro text +// instead -- so BASE_DEFINE was never actually defined, and this file would +// take the "not defined" branch below instead, hiding the division by zero. +// See vcxproj_metadata_self_reference_case_test.py. +#if defined(BASE_DEFINE) && defined(EXTRA_DEFINE) +int main() +{ + int x = 3 / 0; // ERROR + return x; +} +#else +int main() +{ + return 0; +} +#endif diff --git a/test/cli/vcxproj_metadata_self_reference_case/vcxproj_metadata_self_reference_case.vcxproj b/test/cli/vcxproj_metadata_self_reference_case/vcxproj_metadata_self_reference_case.vcxproj new file mode 100644 index 00000000000..4dc9d515f78 --- /dev/null +++ b/test/cli/vcxproj_metadata_self_reference_case/vcxproj_metadata_self_reference_case.vcxproj @@ -0,0 +1,48 @@ + + + + + Debug + x64 + + + + {cccccccc-dddd-eeee-ffff-000011112222} + metadataselfreferencecasetest + 10.0 + + + + Application + v143 + + + + + + + BASE_DEFINE + + + + + $(preprocessordefinitions);EXTRA_DEFINE + + + + + + + + diff --git a/test/cli/vcxproj_metadata_self_reference_case_test.py b/test/cli/vcxproj_metadata_self_reference_case_test.py new file mode 100644 index 00000000000..5881bb052b6 --- /dev/null +++ b/test/cli/vcxproj_metadata_self_reference_case_test.py @@ -0,0 +1,42 @@ + +# python -m pytest vcxproj_metadata_self_reference_case_test.py +# +# Regression coverage for addMetadata()'s "$(eName) self-reference" +# accumulation idiom (lib/importproject.cpp) using plain, case-sensitive +# findAndReplace() instead of findAndReplaceCaseInsensitive() -- the same +# function already used everywhere else in this file for property/metadata +# name comparisons, and MetadataMap itself is already case-insensitive +# (cppcheck::stricmp). Real MSBuild item metadata names are case-insensitive, +# so a self-reference like $(preprocessordefinitions) inside a +# element must resolve regardless of casing -- see +# vcxproj_metadata_self_reference_case/vcxproj_metadata_self_reference_case.vcxproj, +# which accumulates PreprocessorDefinitions across two ItemDefinitionGroup +# blocks, the second referencing the first back via a differently-cased +# $(preprocessordefinitions). + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_metadata_self_reference_case(): + args = [ + '--project=vcxproj_metadata_self_reference_case/vcxproj_metadata_self_reference_case.vcxproj', + '--no-cppcheck-build-dir', + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + normalized_stdout = stdout.replace('\\', '/') + filename = 'vcxproj_metadata_self_reference_case/foo.cpp' + + assert ('Checking %s Debug|x64...' % filename) in normalized_stdout, stdout + + normalized_stderr = stderr.replace('\\', '/') + + # Reached only if BOTH BASE_DEFINE (accumulated from the first + # ItemDefinitionGroup, via the case-mismatched self-reference) and + # EXTRA_DEFINE ended up defined. + assert normalized_stderr.count('%s:13:15: error: Division by zero.' % filename) == 1, stderr diff --git a/test/cli/vcxproj_property_order/main.cpp b/test/cli/vcxproj_property_order/main.cpp new file mode 100644 index 00000000000..905869dfa38 --- /dev/null +++ b/test/cli/vcxproj_property_order/main.cpp @@ -0,0 +1,4 @@ +int main() +{ + return 0; +} diff --git a/test/cli/vcxproj_property_order/vcxproj_property_order.vcxproj b/test/cli/vcxproj_property_order/vcxproj_property_order.vcxproj new file mode 100644 index 00000000000..54d921985c9 --- /dev/null +++ b/test/cli/vcxproj_property_order/vcxproj_property_order.vcxproj @@ -0,0 +1,53 @@ + + + + + Debug + x64 + + + + 17.0 + Win32Proj + {7a1f4e2c-9b3d-4a6e-8c1f-3e5d7a9b2c4f} + vcxprojpropertyorder + 10.0 + + + + Application + true + v143 + Unicode + + + + + + + + + + + + + + Level3 + LATE_DEFINE=$(LateDefinedProp);_CONSOLE;%(PreprocessorDefinitions) + stdcpp17 + + + + + + + + 42 + + + + + diff --git a/test/cli/vcxproj_property_order_test.py b/test/cli/vcxproj_property_order_test.py new file mode 100644 index 00000000000..818ce4b6a86 --- /dev/null +++ b/test/cli/vcxproj_property_order_test.py @@ -0,0 +1,39 @@ + +# python -m pytest vcxproj_property_order_test.py +# +# Regression coverage for MSBuild/Visual Studio property-before-items evaluation +# order: a project fully resolves every property across the whole file before +# evaluating any ItemDefinitionGroup or ItemGroup, regardless of where in the +# document that property happens to be set. This fixture's ItemDefinitionGroup +# references a property that is only assigned by a PropertyGroup positioned AFTER +# it (and after the ItemGroup) -- if cppcheck evaluated the project as one +# document-order pass instead of a real Properties-then-ItemDefs-then-Items +# sequence, that property would still be unset (and therefore unexpanded) at the +# point the ItemDefinitionGroup is evaluated. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_property_order(): + args = [ + '--project=vcxproj_property_order/vcxproj_property_order.vcxproj', + '--no-cppcheck-build-dir', + '--dump' + ] + ret, stdout, _ = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + dump_file = os.path.join(__script_dir, 'vcxproj_property_order', 'main.cpp.dump') + assert os.path.exists(dump_file), "Dump file not found at %s" % dump_file + + with open(dump_file, 'rt') as f: + dump_content = f.read() + + # LateDefinedProp is set by a PropertyGroup below the ItemDefinitionGroup that + # uses it; the ItemDefinitionGroup must still see its final value (42), not an + # unexpanded '$(LateDefinedProp)' or an empty expansion. + assert 'LATE_DEFINE=42' in dump_content, dump_content diff --git a/test/cli/vcxproj_split_configs/ConfigsDebug.props b/test/cli/vcxproj_split_configs/ConfigsDebug.props new file mode 100644 index 00000000000..aeebadde55d --- /dev/null +++ b/test/cli/vcxproj_split_configs/ConfigsDebug.props @@ -0,0 +1,9 @@ + + + + + Debug + x64 + + + diff --git a/test/cli/vcxproj_split_configs/ConfigsRelease.props b/test/cli/vcxproj_split_configs/ConfigsRelease.props new file mode 100644 index 00000000000..94921237e83 --- /dev/null +++ b/test/cli/vcxproj_split_configs/ConfigsRelease.props @@ -0,0 +1,9 @@ + + + + + Release + x64 + + + diff --git a/test/cli/vcxproj_split_configs/main.cpp b/test/cli/vcxproj_split_configs/main.cpp new file mode 100644 index 00000000000..76e8197013a --- /dev/null +++ b/test/cli/vcxproj_split_configs/main.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/test/cli/vcxproj_split_configs/vcxproj_split_configs.vcxproj b/test/cli/vcxproj_split_configs/vcxproj_split_configs.vcxproj new file mode 100644 index 00000000000..cf87ec77dc8 --- /dev/null +++ b/test/cli/vcxproj_split_configs/vcxproj_split_configs.vcxproj @@ -0,0 +1,36 @@ + + + + {aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa} + splitconfigtest3 + 10.0 + + + + + + + + + Application + v143 + + + Application + v143 + + + + + + + diff --git a/test/cli/vcxproj_split_configs_test.py b/test/cli/vcxproj_split_configs_test.py new file mode 100644 index 00000000000..8196add7edf --- /dev/null +++ b/test/cli/vcxproj_split_configs_test.py @@ -0,0 +1,49 @@ + +# python -m pytest vcxproj_split_configs_test.py +# +# Regression coverage for the vcxproj configuration-discovery pass (used only +# when a project has no inline and +# must find its configurations by walking imports): it must not stop scanning +# as soon as it finds the first configuration. Real MSBuild/Visual Studio has +# to know the complete configuration set before evaluation with a specific +# Configuration/Platform pair is even possible, so its own preliminary parse +# does not stop early either -- configurations can legitimately be split +# across multiple imports. +# +# This fixture is deliberately ordered so a naive "stop at the first +# configuration found" discovery pass fails it: ConfigsRelease.props is +# unconditional and comes first, so it is found regardless; ConfigsDebug.props +# is reachable only through an ImportGroup gated on Configuration=='Debug', +# positioned AFTER it. A discovery pass that stops once Release|x64 is found +# never reaches that second import at all -- and simply re-running the real +# per-configuration evaluation pass for Release|x64 does not rescue it either, +# since that pass evaluates with Configuration=Release, so the Debug-gated +# ImportGroup's Condition is false during it too. Only a discovery pass that +# keeps scanning (using its own guessed Configuration, seeded before the scan +# starts) reaches and evaluates the second import and finds Debug|x64 as well. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + + +def test_vcxproj_split_configs(): + args = [ + '--project=vcxproj_split_configs/vcxproj_split_configs.vcxproj', + '--no-cppcheck-build-dir', + '--dump' + ] + ret, stdout, _ = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + # Windows prints native '\' path separators ("Checking foo\main.cpp ..."); + # normalize before matching so this passes on every platform (same idiom + # used by test_log() in clang-import_test.py). + normalized_stdout = stdout.replace('\\', '/') + + # Both configurations must be discovered and checked, not just whichever + # one the (possibly early-stopping) discovery pass happens to find first. + assert 'Checking vcxproj_split_configs/main.cpp Release|x64' in normalized_stdout, stdout + assert 'Checking vcxproj_split_configs/main.cpp Debug|x64' in normalized_stdout, stdout diff --git a/test/cli/vcxproj_unicode_test.py b/test/cli/vcxproj_unicode_test.py new file mode 100644 index 00000000000..b59a4c4ad4e --- /dev/null +++ b/test/cli/vcxproj_unicode_test.py @@ -0,0 +1,63 @@ + +# 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') + + # v143 toolset -> _MSC_VER=1930, _MSC_FULL_VER=193000000 + assert '_WIN32=1' in dump_content + assert '_M_IX86=600' in dump_content + assert '_MSC_VER=1930' in dump_content + assert '_MSC_FULL_VER=193000000' in dump_content + assert '_MSVC_LANG=201402L' in dump_content # default C++14, no LanguageStandard set + assert 'UNICODE=1' in dump_content + assert '_UNICODE=1' in dump_content + assert '_MBCS' not in dump_content + +def test_vcxproj_unicode_release(tmp_path): + dump_content = _get_dump_for_configuration(tmp_path, 'Release|Win32') + + # v143 toolset, CharacterSet=NotSet, UseOfMfc=Static + assert '_WIN32=1' in dump_content + assert '_M_IX86=600' in dump_content + assert '_MSC_VER=1930' in dump_content + assert '_MSC_FULL_VER=193000000' in dump_content + assert '_MSVC_LANG=201402L' in dump_content + assert 'UNICODE' not in dump_content + assert '_MBCS' not in dump_content + +def test_vcxproj_multibyte(tmp_path): + dump_content = _get_dump_for_configuration(tmp_path, 'MultiByte|Win32') + + # MultiByte projects must define _MBCS and must NOT define UNICODE or _UNICODE (M-1) + assert '_MBCS=1' in dump_content + assert 'UNICODE' not in dump_content diff --git a/test/testimportproject.cpp b/test/testimportproject.cpp index 873272030f6..c7cd3000fc2 100644 --- a/test/testimportproject.cpp +++ b/test/testimportproject.cpp @@ -18,15 +18,16 @@ #include "filesettings.h" #include "fixture.h" +#include "helpers.h" #include "importproject.h" #include "redirect.h" #include "settings.h" #include "standards.h" #include "suppressions.h" -#include "xml.h" +#include +#include #include -#include #include #include #include @@ -35,14 +36,13 @@ class TestImporter final : public ImportProject { public: - using ImportProject::importCompileCommands; + using ImportProject::processCompileCommands; using ImportProject::importCppcheckGuiProject; - using ImportProject::importVcxproj; - using ImportProject::SharedItemsProject; using ImportProject::collectArgs; using ImportProject::fsSetDefines; using ImportProject::fsSetIncludePaths; using ImportProject::setRelativePaths; + using ImportProject::setProjectPath; }; @@ -58,31 +58,32 @@ class TestImportProject : public TestFixture { TEST_CASE(setIncludePaths2); TEST_CASE(setIncludePaths3); // macro names are case insensitive TEST_CASE(setRelativePathsInclude); // #14746 - TEST_CASE(importCompileCommands1); - TEST_CASE(importCompileCommands2); // #8563, #9567 - TEST_CASE(importCompileCommands3); // check with existing trailing / in directory - TEST_CASE(importCompileCommands4); // only accept certain file types - TEST_CASE(importCompileCommands5); // Windows/CMake/Ninja generated compile_commands.json - TEST_CASE(importCompileCommands6); // Windows/CMake/Ninja generated compile_commands.json with spaces - TEST_CASE(importCompileCommands7); // linux: "/home/danielm/cppcheck 2" - TEST_CASE(importCompileCommands8); // Windows: "C:\Users\danielm\cppcheck" - TEST_CASE(importCompileCommands9); - TEST_CASE(importCompileCommands10); // #10887: include path with space - TEST_CASE(importCompileCommands11); // include path order - TEST_CASE(importCompileCommands12); // #13040: "directory" is parent directory, relative include paths - TEST_CASE(importCompileCommands13); // #13333: duplicate file entries - TEST_CASE(importCompileCommands14); // #14156 - TEST_CASE(importCompileCommands15); // #14306 - TEST_CASE(importCompileCommandsForcedInclude); // -include / /FI force-include - TEST_CASE(importCompileCommandsArgumentsSection); // Handle arguments section - TEST_CASE(importCompileCommandsNoCommandSection); // gracefully handles malformed json - TEST_CASE(importCompileCommandsDirectoryMissing); // 'directory' field missing - TEST_CASE(importCompileCommandsDirectoryInvalid); // 'directory' field not a string + TEST_CASE(processCompileCommands1); + TEST_CASE(processCompileCommands2); // #8563, #9567 + TEST_CASE(processCompileCommands3); // check with existing trailing / in directory + TEST_CASE(processCompileCommands4); // only accept certain file types + TEST_CASE(processCompileCommands5); // Windows/CMake/Ninja generated compile_commands.json + TEST_CASE(processCompileCommands6); // Windows/CMake/Ninja generated compile_commands.json with spaces + TEST_CASE(processCompileCommands7); // linux: "/home/danielm/cppcheck 2" + TEST_CASE(processCompileCommands8); // Windows: "C:\Users\danielm\cppcheck" + TEST_CASE(processCompileCommands9); + TEST_CASE(processCompileCommands10); // #10887: include path with space + TEST_CASE(processCompileCommands11); // include path order + TEST_CASE(processCompileCommands12); // #13040: "directory" is parent directory, relative include paths + TEST_CASE(processCompileCommands13); // #13333: duplicate file entries + TEST_CASE(processCompileCommands14); // #14156 + TEST_CASE(processCompileCommands15); // #14306 + TEST_CASE(processCompileCommandsForcedInclude); // -include / /FI force-include + TEST_CASE(processCompileCommandsArgumentsSection); // Handle arguments section + TEST_CASE(processCompileCommandsNoCommandSection); // gracefully handles malformed json + TEST_CASE(processCompileCommandsDirectoryMissing); // 'directory' field missing + TEST_CASE(processCompileCommandsDirectoryInvalid); // 'directory' field not a string TEST_CASE(importCppcheckGuiProject); TEST_CASE(importCppcheckGuiProjectDuplicateSuppressions); TEST_CASE(importCppcheckGuiProjectPremiumMisra); + TEST_CASE(importCppcheckGuiProjectAbsPath); // absolute Unix path must not be prepended with mPath + TEST_CASE(importCppcheckGuiProjectRelPathWithBase); // relative path must be prepended with mPath TEST_CASE(ignorePaths); - TEST_CASE(testVcxprojUnicode); TEST_CASE(testCollectArgs1); TEST_CASE(testCollectArgs2); TEST_CASE(testCollectArgs3); @@ -91,6 +92,13 @@ class TestImportProject : public TestFixture { TEST_CASE(testCollectArgs6); TEST_CASE(testCollectArgs7); TEST_CASE(testVcxprojConditions); + TEST_CASE(testVcxprojConditionEqualityIsNotVersionComparison); + TEST_CASE(testPropertyNameAllowsHyphen); + TEST_CASE(testMSBuildStaticFunctions); + TEST_CASE(testVcxitemsPathResolution); + TEST_CASE(testMissingChildImportNonFatal); // missing child imports must not abort the project + TEST_CASE(testCurrentToolsVersionFromProps); // "Current" keyword uses VisualStudioVersion property + TEST_CASE(testMetadataSelfReferenceCaseInsensitive); } void setDefines() const { @@ -107,13 +115,28 @@ class TestImportProject : public TestFixture { TestImporter::fsSetDefines(fs, "A;;B"); ASSERT_EQUALS("A=1;B=1", fs.defines); + + // M-2: %(metadata) tokens at the start of the string must be stripped + TestImporter::fsSetDefines(fs, "%(PreprocessorDefinitions)"); + ASSERT_EQUALS("", fs.defines); + + TestImporter::fsSetDefines(fs, "%(PreprocessorDefinitions);A"); + ASSERT_EQUALS("A=1", fs.defines); + + TestImporter::fsSetDefines(fs, "%(PreprocessorDefinitions);A;B"); + ASSERT_EQUALS("A=1;B=1", fs.defines); + + // Leading %(…) followed by a mid-string %(…) — both must be stripped + TestImporter::fsSetDefines(fs, "%(PreprocessorDefinitions);A;%(OtherMeta);B"); + ASSERT_EQUALS("A=1;B=1", fs.defines); } 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 +144,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 +155,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()); } @@ -150,7 +175,7 @@ class TestImportProject : public TestFixture { ASSERT_EQUALS("sub/a.c", fs.filename()); } - void importCompileCommands1() const { + void processCompileCommands1() const { REDIRECT; constexpr char json[] = R"([{ "directory": "/tmp", @@ -159,12 +184,12 @@ class TestImportProject : public TestFixture { }])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); ASSERT_EQUALS(1, importer.fileSettings.size()); ASSERT_EQUALS("TEST1=1;TEST2=2", importer.fileSettings.cbegin()->defines); } - void importCompileCommands2() const { + void processCompileCommands2() const { REDIRECT; // Absolute file path #ifdef _WIN32 @@ -175,7 +200,7 @@ class TestImportProject : public TestFixture { }])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); ASSERT_EQUALS(1, importer.fileSettings.size()); ASSERT_EQUALS("C:/bar.c", importer.fileSettings.cbegin()->filename()); #else @@ -186,13 +211,13 @@ class TestImportProject : public TestFixture { }])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); ASSERT_EQUALS(1, importer.fileSettings.size()); ASSERT_EQUALS("/bar.c", importer.fileSettings.cbegin()->filename()); #endif } - void importCompileCommands3() const { + void processCompileCommands3() const { REDIRECT; const char json[] = R"([{ "directory": "/tmp/", @@ -201,12 +226,12 @@ class TestImportProject : public TestFixture { }])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); ASSERT_EQUALS(1, importer.fileSettings.size()); ASSERT_EQUALS("/tmp/src.c", importer.fileSettings.cbegin()->filename()); } - void importCompileCommands4() const { + void processCompileCommands4() const { REDIRECT; constexpr char json[] = R"([{ "directory": "/tmp/", @@ -215,11 +240,11 @@ class TestImportProject : public TestFixture { }])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); ASSERT_EQUALS(0, importer.fileSettings.size()); } - void importCompileCommands5() const { + void processCompileCommands5() const { REDIRECT; constexpr char json[] = R"([{ @@ -234,12 +259,12 @@ class TestImportProject : public TestFixture { }])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); ASSERT_EQUALS(2, importer.fileSettings.size()); ASSERT_EQUALS("C:/Users/dan/git/test-cppcheck/mylib/src/", importer.fileSettings.cbegin()->includePaths.front()); } - void importCompileCommands6() const { + void processCompileCommands6() const { REDIRECT; constexpr char json[] = R"([{ @@ -254,14 +279,14 @@ class TestImportProject : public TestFixture { }])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); ASSERT_EQUALS(2, importer.fileSettings.size()); ASSERT_EQUALS("C:/Users/dan/git/test-cppcheck/mylib/src/", importer.fileSettings.cbegin()->includePaths.front()); ASSERT_EQUALS("C:/Users/dan/git/test-cppcheck/mylib/second src/", importer.fileSettings.cbegin()->includePaths.back()); } - void importCompileCommands7() const { + void processCompileCommands7() const { REDIRECT; // cmake -DFILESDIR="/some/path" .. constexpr char json[] = @@ -272,7 +297,7 @@ class TestImportProject : public TestFixture { }])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); ASSERT_EQUALS(1, importer.fileSettings.size()); ASSERT_EQUALS("FILESDIR=\"/some/path\"", importer.fileSettings.cbegin()->defines); ASSERT_EQUALS(1, importer.fileSettings.cbegin()->includePaths.size()); @@ -282,7 +307,7 @@ class TestImportProject : public TestFixture { importer.fileSettings.cbegin()->includePaths.back()); } - void importCompileCommands8() const { + void processCompileCommands8() const { REDIRECT; // cmake -DFILESDIR="C:\Program Files\Cppcheck" -G"NMake Makefiles" .. constexpr char json[] = @@ -293,10 +318,10 @@ class TestImportProject : public TestFixture { }])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); // Do not crash + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); // Do not crash } - void importCompileCommands9() const { + void processCompileCommands9() const { REDIRECT; // IAR output (https://sourceforge.net/p/cppcheck/discussion/general/thread/608af51e0a/) constexpr char json[] = @@ -310,10 +335,10 @@ class TestImportProject : public TestFixture { }])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); } - void importCompileCommands10() const { // #10887 + void processCompileCommands10() const { // #10887 REDIRECT; constexpr char json[] = R"([{ @@ -327,13 +352,13 @@ class TestImportProject : public TestFixture { }])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); ASSERT_EQUALS(1, importer.fileSettings.size()); const FileSettings &fs = importer.fileSettings.front(); ASSERT_EQUALS("/home/danielm/cppcheck/test folder/", fs.includePaths.front()); } - void importCompileCommands11() const { // include path order + void processCompileCommands11() const { // include path order REDIRECT; constexpr char json[] = R"([{ @@ -349,14 +374,14 @@ class TestImportProject : public TestFixture { }])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); ASSERT_EQUALS(1, importer.fileSettings.size()); const FileSettings &fs = importer.fileSettings.front(); ASSERT_EQUALS("/x/def/", fs.includePaths.front()); ASSERT_EQUALS("/x/abc/", fs.includePaths.back()); } - void importCompileCommands12() const { // #13040 + void processCompileCommands12() const { // #13040 REDIRECT; constexpr char json[] = R"([{ @@ -366,14 +391,14 @@ class TestImportProject : public TestFixture { }])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); ASSERT_EQUALS(1, importer.fileSettings.size()); const FileSettings &fs = importer.fileSettings.front(); ASSERT_EQUALS(1, fs.includePaths.size()); ASSERT_EQUALS("/x/", fs.includePaths.front()); } - void importCompileCommands13() const { // #13333 + void processCompileCommands13() const { // #13333 REDIRECT; constexpr char json[] = R"([{ @@ -387,7 +412,7 @@ class TestImportProject : public TestFixture { }])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); ASSERT_EQUALS(2, importer.fileSettings.size()); const FileSettings &fs1 = importer.fileSettings.front(); const FileSettings &fs2 = importer.fileSettings.back(); @@ -395,7 +420,7 @@ class TestImportProject : public TestFixture { ASSERT_EQUALS(1, fs2.file.fsFileId()); } - void importCompileCommands14() const { // #14156 + void processCompileCommands14() const { // #14156 REDIRECT; constexpr char json[] = R"([{ @@ -412,13 +437,13 @@ class TestImportProject : public TestFixture { }])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); ASSERT_EQUALS(1, importer.fileSettings.size()); const FileSettings &fs = importer.fileSettings.front(); ASSERT_EQUALS("TFS_LINUX_MODULE_NAME=\"tfs_linux\"", fs.defines); } - void importCompileCommands15() const { // #14306 + void processCompileCommands15() const { // #14306 REDIRECT; constexpr char json[] = R"([ @@ -431,14 +456,14 @@ class TestImportProject : public TestFixture { ])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); ASSERT_EQUALS(1, importer.fileSettings.size()); const FileSettings &fs = importer.fileSettings.front(); ASSERT_EQUALS(1, fs.includePaths.size()); ASSERT_EQUALS("C:/Users/abcd/efg/hijk/path/123/", fs.includePaths.front()); } - void importCompileCommandsForcedInclude() const { // -include / /FI force-include + void processCompileCommandsForcedInclude() const { // -include / /FI force-include REDIRECT; constexpr char json[] = R"([{ @@ -448,7 +473,7 @@ class TestImportProject : public TestFixture { }])"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); ASSERT_EQUALS(1, importer.fileSettings.size()); const FileSettings &fs = importer.fileSettings.front(); ASSERT_EQUALS(2, fs.forcedIncludes.size()); @@ -456,48 +481,48 @@ class TestImportProject : public TestFixture { ASSERT_EQUALS("platform.h", fs.forcedIncludes.back()); // MSVC/clang-cl /FI } - void importCompileCommandsArgumentsSection() const { + void processCompileCommandsArgumentsSection() const { REDIRECT; constexpr char json[] = "[ { \"directory\": \"/tmp/\"," "\"arguments\": [\"gcc\", \"-c\", \"src.c\"]," "\"file\": \"src.c\" } ]"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(true, importer.importCompileCommands(istr)); + ASSERT_EQUALS(true, importer.processCompileCommands(istr)); ASSERT_EQUALS(1, importer.fileSettings.size()); ASSERT_EQUALS("/tmp/src.c", importer.fileSettings.cbegin()->filename()); } - void importCompileCommandsNoCommandSection() const { + void processCompileCommandsNoCommandSection() const { REDIRECT; constexpr char json[] = "[ { \"directory\": \"/tmp/\"," "\"file\": \"src.mm\" } ]"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(false, importer.importCompileCommands(istr)); + ASSERT_EQUALS(false, importer.processCompileCommands(istr)); ASSERT_EQUALS(0, importer.fileSettings.size()); ASSERT_EQUALS(1, importer.errors.size()); ASSERT_EQUALS("no 'arguments' or 'command' field found in compilation database entry", importer.errors[0]); } - void importCompileCommandsDirectoryMissing() const { + void processCompileCommandsDirectoryMissing() const { REDIRECT; constexpr char json[] = "[ { \"file\": \"src.mm\" } ]"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(false, importer.importCompileCommands(istr)); + ASSERT_EQUALS(false, importer.processCompileCommands(istr)); ASSERT_EQUALS(0, importer.fileSettings.size()); ASSERT_EQUALS(1, importer.errors.size()); ASSERT_EQUALS("'directory' field in compilation database entry missing", importer.errors[0]); } - void importCompileCommandsDirectoryInvalid() const { + void processCompileCommandsDirectoryInvalid() const { REDIRECT; constexpr char json[] = "[ { \"directory\": 123," "\"file\": \"src.mm\" } ]"; std::istringstream istr(json); TestImporter importer; - ASSERT_EQUALS(false, importer.importCompileCommands(istr)); + ASSERT_EQUALS(false, importer.processCompileCommands(istr)); ASSERT_EQUALS(0, importer.fileSettings.size()); ASSERT_EQUALS(1, importer.errors.size()); ASSERT_EQUALS("'directory' field in compilation database entry is not a string", importer.errors[0]); @@ -578,6 +603,51 @@ class TestImportProject : public TestFixture { ASSERT(s.addons.empty()); } + void importCppcheckGuiProjectAbsPath() const { + // Regression test: absolute Unix paths in must not be prepended with the + // project directory (mPath). Before the fix, joinRelativePath() classified "/foo" + // as RootRelative (no drive letter) and prepended mPath, yielding "/proj//foo". + REDIRECT; + constexpr char xml[] = "\n" + "\n" + " \n" + " \n" + " \n" + "\n"; + std::istringstream istr(xml); + Settings s; + Suppressions supprs; + TestImporter project; + project.setProjectPath("/project/dir/"); + ASSERT_EQUALS(true, project.importCppcheckGuiProject(istr, s, supprs)); + ASSERT_EQUALS(1, project.guiProject.pathNames.size()); + ASSERT_EQUALS("/abs/path/file.cpp", project.guiProject.pathNames[0]); + } + + void importCppcheckGuiProjectRelPathWithBase() const { + // Relative paths in must be prepended with the project directory (mPath), + // matching MSBuild/GUI behaviour where relative entries are project-dir-relative. + // Also covers the mixed-CWD scenario: when --project=../foo.cppcheck is used, + // mPath is "../", so a relative entry like "src/a.cpp" becomes "../src/a.cpp". + REDIRECT; + constexpr char xml[] = "\n" + "\n" + " \n" + " \n" + " \n" + " \n" + "\n"; + std::istringstream istr(xml); + Settings s; + Suppressions supprs; + TestImporter project; + project.setProjectPath("../"); + ASSERT_EQUALS(true, project.importCppcheckGuiProject(istr, s, supprs)); + ASSERT_EQUALS(2, project.guiProject.pathNames.size()); + ASSERT_EQUALS("/abs/file.cpp", project.guiProject.pathNames[0]); + ASSERT_EQUALS("../src/rel.cpp", project.guiProject.pathNames[1]); + } + void ignorePaths() const { FileSettings fs1{"foo/bar", Standards::Language::CPP, 0}; FileSettings fs2{"qwe/rty", Standards::Language::CPP, 0}; @@ -595,59 +665,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 +770,97 @@ 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'", "", "")); + // == and != are NOT relational operators: real MSBuild documents them + // as ordinary equality/inequality (numeric if both sides are plain + // numbers, case-insensitive string otherwise), never the zero-padded, + // component-wise equality $([MSBuild]::VersionEquals(...)) implements + // (see testMSBuildStaticFunctions() for that function's own, + // deliberately different, semantics). '1.2.3.4.0' and '1.2.3.4' are + // different strings and neither parses as a plain number, so == and + // != see them as unequal -- even though relationally (see + // '17'/'17.0.0.0' below) MSBuildVersion's own comparator would treat + // a shorter version as simply less than a longer one, not equal to it + // either. + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4.0' == '1.2.3.4'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4.0' != '1.2.3.4'", "", "")); + // Relational operators (<, >, <=, >=) DO use MSBuildVersion's + // comparator, which treats a version's omitted trailing components as + // -1 (see MSBuildVersion::cmp()'s doc comment) -- so a shorter + // version string sorts strictly before a longer one, '17' < '17.0.0.0', + // rather than comparing equal to it under either == or a relational op. + 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'", "", "")); + // Same reasoning as '17'/'17.0.0.0' above: == is plain (numeric- or + // string-, never version-) comparison, so none of these are equal -- + // '17.0', '17.0.0.0' and '17.0.0' are all different strings from each + // other and from '17', and none of them parses as a plain integer + // (they contain '.'). + 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.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 +869,625 @@ 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")); + + // Length property access (no parentheses) + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Length) == '5'", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Length) == '0'", "", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Platform.Length) == '5'", "Debug", "Win32")); + // Length after a method call in a chain + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.ToUpper().Length) == '5'", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Replace('-', '').Length) == '9'", "Debug-Test", "Win32")); + // Length in PropertyValueExpander (non-condition) path + ASSERT_EQUALS("5", cppcheck::testing::expandMSBuildProperties("$(Configuration.Length)", "Debug", "Win32")); + ASSERT_EQUALS("0", cppcheck::testing::expandMSBuildProperties("$(Configuration.Length)", "", "Win32")); + ASSERT_EQUALS("5", cppcheck::testing::expandMSBuildProperties("$(Platform.Length)", "Debug", "Win32")); + ASSERT_EQUALS("5", cppcheck::testing::expandMSBuildProperties("$(Configuration.ToUpper().Length)", "Debug", "Win32")); + + // IndexOf / LastIndexOf + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.IndexOf('-')) == '5'", "Debug-Test", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.IndexOf('x')) == '-1'", "Debug-Test", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.IndexOf('e')) == '1'", "Debug-Test", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.IndexOf('e', '3')) == '7'", "Debug-Test", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.LastIndexOf('e')) == '7'", "Debug-Test", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.LastIndexOf('x')) == '-1'", "Debug-Test", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.LastIndexOf('e', '3')) == '1'", "Debug-Test", "Win32")); + ASSERT_EQUALS("5", cppcheck::testing::expandMSBuildProperties("$(Configuration.IndexOf('-'))", "Debug-Test", "Win32")); + ASSERT_EQUALS("-1", cppcheck::testing::expandMSBuildProperties("$(Configuration.IndexOf('x'))", "Debug-Test", "Win32")); + ASSERT_EQUALS("7", cppcheck::testing::expandMSBuildProperties("$(Configuration.LastIndexOf('e'))", "Debug-Test", "Win32")); + + // PadLeft / PadRight + ASSERT_EQUALS(" hi", cppcheck::testing::expandMSBuildProperties("$(Configuration.PadLeft('4'))", "hi", "Win32")); + ASSERT_EQUALS("hi ", cppcheck::testing::expandMSBuildProperties("$(Configuration.PadRight('4'))", "hi", "Win32")); + ASSERT_EQUALS("00hi", cppcheck::testing::expandMSBuildProperties("$(Configuration.PadLeft('4', '0'))", "hi", "Win32")); + ASSERT_EQUALS("hi00", cppcheck::testing::expandMSBuildProperties("$(Configuration.PadRight('4', '0'))", "hi", "Win32")); + // no padding needed when value already long enough + ASSERT_EQUALS("hello", cppcheck::testing::expandMSBuildProperties("$(Configuration.PadLeft('3'))", "hello", "Win32")); + ASSERT_EQUALS("hello", cppcheck::testing::expandMSBuildProperties("$(Configuration.PadRight('3'))", "hello", "Win32")); + // width exactly equal to value length — no padding + ASSERT_EQUALS("hi", cppcheck::testing::expandMSBuildProperties("$(Configuration.PadLeft('2'))", "hi", "Win32")); + ASSERT_EQUALS("hi", cppcheck::testing::expandMSBuildProperties("$(Configuration.PadRight('2'))", "hi", "Win32")); + // width 0 — no padding + ASSERT_EQUALS("hi", cppcheck::testing::expandMSBuildProperties("$(Configuration.PadLeft('0'))", "hi", "Win32")); + // chaining: pad then measure length + ASSERT_EQUALS("6", cppcheck::testing::expandMSBuildProperties("$(Configuration.PadLeft('6', '0').Length)", "hi", "Win32")); + ASSERT_EQUALS("6", cppcheck::testing::expandMSBuildProperties("$(Configuration.PadRight('6', '0').Length)", "hi", "Win32")); + // condition evaluation path + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.PadLeft('4', '0')) == '00hi'", "hi", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.PadRight('4', '0')) == 'hi00'", "hi", "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'", "", "")); + // Equality comparison: numeric, Boolean, then string fallback -- NOT + // MSBuildVersion's zero-padded comparison (see the '17'/'17.0.0.0' + // block above in testVcxprojConditions() for why): '0x10' and '16' + // are both plain (hex/decimal) integers, so == compares them + // numerically and they're equal; '1.0' is not a plain integer (it + // contains '.') and is a different string from '1', so == falls back + // to string comparison and they're NOT equal. + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'0x10' == '16'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1.0' == '1'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'true' == 'TRUE'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'Alpha' == 'alpha'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'Alpha' == 'Beta'", "", "")); + // Boolean literals and quoted boolean strings are case-insensitive (H-2) + // Unquoted keywords (matchWord is already case-insensitive) + ASSERT(cppcheck::testing::evaluateVcxprojCondition("true", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("false", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("True", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("False", "", "")); + // Quoted boolean strings from property expansion (e.g. true) + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'true'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'false'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'TRUE'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'FALSE'", "", "")); + // Composition with mixed-case booleans + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'true' And 'True'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'true' And 'false'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'false' Or 'TRUE'", "", "")); + // Equality comparison: numeric, Boolean, then string fallback -- NOT + // MSBuildVersion's zero-padded comparison (see the '17'/'17.0.0.0' + // block above in testVcxprojConditions() for why): '0x10' and '16' + // are both plain (hex/decimal) integers, so == compares them + // numerically and they're equal; '1.0' is not a plain integer (it + // contains '.') and is a different string from '1', so == falls back + // to string comparison and they're NOT equal. + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'0x10' == '16'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1.0' == '1'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'true' == 'TRUE'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'Alpha' == 'alpha'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'Alpha' == 'Beta'", "", "")); + } + + // Regression coverage for compare()'s == / != previously sharing the + // same numeric -> Boolean -> Version -> string cascade as the relational + // operators <, >, <=, >=. Real MSBuild documents <, >, <=, >= as usable + // only with numeric values -- a dotted, multi-component string isn't a + // valid number there, which is exactly why the dedicated + // $([MSBuild]::VersionGreaterThan(...)) etc. functions exist -- while == + // and != are documented as ordinary equality/inequality: numeric if both + // sides are plain numbers, case-insensitive string otherwise. Before the + // fix, == and != additionally fell through to MSBuildVersion's own + // comparator when both sides parsed as a dotted version string, and that + // comparator's own "==" implementation deliberately treats an omitted + // trailing component as zero (1 == 1.0 == 1.0.0) -- correct for the + // dedicated VersionEquals() function this codebase also exposes (see + // testMSBuildStaticFunctions()'s $([MSBuild]::VersionEquals(...)) + // coverage), but not something a plain == in an ordinary + // Condition="..." attribute should ever do. This meant e.g. + // Condition="'$(SomeVersion)' == '1.1.0'" could spuriously match + // '$(SomeVersion)'=='1.1', a real vcxproj-authoring hazard given how + // often toolset/SDK version properties look exactly like this. + void testVcxprojConditionEqualityIsNotVersionComparison() const { + // The exact shape of bug report: a shorter and a longer dotted + // version string must NOT compare equal under == or != , even though + // MSBuildVersion's own zero-padded equality (used by + // $([MSBuild]::VersionEquals(...)) -- and, before this fix, leaked + // into plain ==) would treat them as the same value. + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1.1' == '1.1.0'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.1' != '1.1.0'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1' == '1.0'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1' != '1.0'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1' == '1.0.0'", "", "")); + + // Relationally (<, >, <=, >=), MSBuildVersion's comparator treats an + // omitted trailing component as -1, not 0 (matching .NET's + // System.Version semantics), so a shorter version sorts strictly + // BEFORE a longer one -- neither "equal" under == nor under the + // relational operators either, it is simply less. + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.1' < '1.1.0'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1.1' >= '1.1.0'", "", "")); + + // Two identical dotted version strings are still equal under == -- + // this is ordinary string equality doing its job, not version + // comparison; nothing about excluding == from the version-comparison + // path should affect the case where both sides are literally the + // same text. + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.1.0' == '1.1.0'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1.1.0' != '1.1.0'", "", "")); + + // Genuinely plain numbers (no dots) are still compared numerically + // under == -- this really is documented MSBuild behavior for ==, + // unlike the dotted-version case above, so leading zeros and the + // like still numerically match. + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'01' == '1'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'01' != '1'", "", "")); + + // $([MSBuild]::VersionEquals(...)) is a distinct, dedicated function + // with its own, deliberately zero-padding, semantics -- unaffected + // by any of the above, since it never goes through compare() at all. + // See testMSBuildStaticFunctions() for its general coverage; this is + // exactly the '1.1'/'1.1.0' pair that a plain == above now rejects. + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionEquals('1.1', '1.1.0'))")); + } + + // Regression coverage for property-name identifier parsing rejecting '-'. + // Microsoft documents a valid MSBuild property name as + // [A-Za-z_][A-Za-z0-9_-]* -- '-' IS allowed after the first character, + // e.g. foo referenced as $(My-Property) -- but + // PropertyValueExpander::parseIdentifier() (used by expandMSBuildVariables(), + // the general $(...) expansion path), ConditionParser's property-name + // parsing (used by $(...) inside a Condition="..."), and + // hasOnlyInvariantItemPathVariables()'s inline scanner (used for ClCompile + // item Include/Update/Remove paths, see vcxproj_item_macro_path_hyphen_test.py + // for CLI-level coverage of that one) all previously stopped at the first + // '-', silently truncating the name -- so $(My-Property) was parsed as a + // reference to a property literally named "My" followed by the literal + // text "-Property)", which (barring an actual property named exactly + // "My") just left the whole expression unexpanded, rather than resolving + // "My-Property" as one property name the way real MSBuild does. Method + // and member names are NOT affected -- MSBuild's own grammar restricts + // those to [A-Za-z_][A-Za-z0-9_]* with no '-' (see + // ConditionParser::parseMethodName()'s doc comment and the inline method + // scans in PropertyValueExpander::tryParseExpr()) -- so this test also + // confirms a hyphen right after a '.' in a method position is correctly + // NOT consumed as part of the method name. + // + // expandMSBuildExpression()/evaluateVcxprojCondition() (the testing hooks + // used here) only let a caller set Configuration/Platform in the + // properties map directly, so an OS environment variable -- the other + // source PropertyValueExpander::lookup() and ConditionParser::getPropertyValue() + // already fall back to for any name not in the properties map -- is the + // only way to inject an arbitrarily-named property through them. + void testPropertyNameAllowsHyphen() const { + const char *const envName = "CppcheckTest-Hyphen-Prop"; +#ifdef _WIN32 + _putenv_s(envName, "hyphen-value"); +#else + setenv(envName, "hyphen-value", 1); +#endif + + // General property expansion (expandMSBuildVariables(), via + // PropertyValueExpander::parseIdentifier()). + ASSERT_EQUALS("hyphen-value", cppcheck::testing::expandMSBuildExpression("$(CppcheckTest-Hyphen-Prop)")); + ASSERT_EQUALS("prefix-hyphen-value-suffix", + cppcheck::testing::expandMSBuildExpression("prefix-$(CppcheckTest-Hyphen-Prop)-suffix")); + + // Condition evaluation (ConditionParser::parsePropertyName(), via + // parsePropertyExpression()). + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'$(CppcheckTest-Hyphen-Prop)' == 'hyphen-value'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'$(CppcheckTest-Hyphen-Prop)' == 'wrong-value'", "", "")); + + // A hyphen is still correctly rejected in a METHOD name -- the + // property name parses in full ("CppcheckTest-Hyphen-Prop"), but the + // method-name scan then stops at the '-' in ".To-Upper", leaving + // "To" as an unrecognized no-paren property access (passing `value` + // through unchanged, exactly like an unrelated unknown property + // accessor would) and "-Upper)" behind as literal, unconsumed text + // that the surrounding expansion then copies through verbatim -- + // rather than treating "-" as part of the method name and matching + // some (nonexistent) "To-Upper" method. + ASSERT_EQUALS("hyphen-value-Upper)", cppcheck::testing::expandMSBuildExpression("$(CppcheckTest-Hyphen-Prop.To-Upper)")); + +#ifdef _WIN32 + _putenv_s(envName, ""); +#else + unsetenv(envName); +#endif + } + + 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'))")); + // Regression: space between method name and '(' must not drop the arg list. + // Before the fix this produced "" and a "unknown class MSBuild or member + // NormalizeDirectory" debug entry because the arg parser never saw '('. + ASSERT_EQUALS("/a/b/c/", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizeDirectory ('/a', 'b', 'c'))")); + ASSERT_EQUALS("/a/b/c", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizePath ('/a', 'b', 'c'))")); + // $(…) references inside quoted args must be expanded before the function + // runs, so that '..' resolution operates on real path components. + // expandMSBuildProperties pre-populates Configuration and Platform; use + // them here as stand-ins for real path properties. + ASSERT_EQUALS("Debug/", cppcheck::testing::expandMSBuildProperties("$([MSBuild]::NormalizeDirectory('$(Configuration)'))", "Debug", "Win32")); + ASSERT_EQUALS("Win32/sub/", cppcheck::testing::expandMSBuildProperties("$([MSBuild]::NormalizeDirectory('$(Platform)', 'sub'))", "Debug", "Win32")); + // The critical real-world pattern: NormalizeDirectory with an inner + // property and '..' segments — must NOT collapse to empty. + ASSERT_EQUALS("Debug/", cppcheck::testing::expandMSBuildProperties("$([MSBuild]::NormalizeDirectory('$(Configuration)', 'sub', '..'))", "Debug", "Win32")); + // Unquoted $(Var)trailing/text arg: trailing bare-word text must be + // concatenated onto the expanded value so that e.g. + // $(MSBuildThisFileDirectory)\..\tools → one concatenated arg, not two. + // Use $(Configuration) as a stand-in for a real directory property. + ASSERT_EQUALS("sub/", cppcheck::testing::expandMSBuildProperties("$([MSBuild]::NormalizeDirectory($(Configuration)\\..\\sub))", "Debug", "Win32")); + ASSERT_EQUALS("sub", cppcheck::testing::expandMSBuildProperties("$([MSBuild]::NormalizePath($(Configuration)\\..\\sub))", "Debug", "Win32")); + // Multi-arg quoted form with three inner $(…) args + ASSERT_EQUALS("Debug/Win32/sub/", cppcheck::testing::expandMSBuildProperties("$([MSBuild]::NormalizeDirectory('$(Configuration)', '$(Platform)', 'sub'))", "Debug", "Win32")); + // Version comparison functions (missing trailing components treated as 0) + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionGreaterThan('2.0', '1.9'))")); + ASSERT_EQUALS("False", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionGreaterThan('1.9', '2.0'))")); + ASSERT_EQUALS("False", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionGreaterThan('1.0', '1.0'))")); + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionGreaterThanOrEquals('1.0', '1.0'))")); + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionGreaterThanOrEquals('2.0', '1.0'))")); + ASSERT_EQUALS("False", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionGreaterThanOrEquals('1.0', '2.0'))")); + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionLessThan('1.9', '2.0'))")); + ASSERT_EQUALS("False", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionLessThan('2.0', '1.9'))")); + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionLessThanOrEquals('1.0', '1.0'))")); + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionEquals('1.0', '1.0.0'))")); // missing component = 0 + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionEquals('1.2.3', '1.2.3'))")); + ASSERT_EQUALS("False", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionEquals('1.0', '1.1'))")); + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionNotEquals('1.0', '1.1'))")); + ASSERT_EQUALS("False", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionNotEquals('1.0', '1.0.0'))")); + // multi-component: 1.2.3.4 vs 1.2.3.5 + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionLessThan('1.2.3.4', '1.2.3.5'))")); + ASSERT_EQUALS("False", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::VersionGreaterThan('1.2.3.4', '1.2.3.5'))")); + // usable in a condition + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$([MSBuild]::VersionGreaterThan('17.0', '16.9')) == 'True'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$([MSBuild]::VersionEquals('14.0', '14.0.0')) == 'True'", "", "")); + + // 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'))")); + // String.Format: plain substitution + ASSERT_EQUALS("foo and bar", cppcheck::testing::expandMSBuildExpression("$([System.String]::Format('{0} and {1}', 'foo', 'bar'))")); + // String.Format: escaped braces + ASSERT_EQUALS("{literal}", cppcheck::testing::expandMSBuildExpression("$([System.String]::Format('{{literal}}'))")); + // String.Format: D (decimal, optional zero-padding) + ASSERT_EQUALS("42", cppcheck::testing::expandMSBuildExpression("$([System.String]::Format('{0:D}', '42'))")); + ASSERT_EQUALS("000042", cppcheck::testing::expandMSBuildExpression("$([System.String]::Format('{0:D6}', '42'))")); + ASSERT_EQUALS("-000005", cppcheck::testing::expandMSBuildExpression("$([System.String]::Format('{0:D6}', '-5'))")); + // String.Format: X / x (hexadecimal) + ASSERT_EQUALS("FF", cppcheck::testing::expandMSBuildExpression("$([System.String]::Format('{0:X}', '255'))")); + ASSERT_EQUALS("00FF", cppcheck::testing::expandMSBuildExpression("$([System.String]::Format('{0:X4}', '255'))")); + ASSERT_EQUALS("00ff", cppcheck::testing::expandMSBuildExpression("$([System.String]::Format('{0:x4}', '255'))")); + // String.Format: F (fixed-point) + ASSERT_EQUALS("3.14", cppcheck::testing::expandMSBuildExpression("$([System.String]::Format('{0:F2}', '3.14159'))")); + ASSERT_EQUALS("3.1416", cppcheck::testing::expandMSBuildExpression("$([System.String]::Format('{0:F4}', '3.14159'))")); + // String.Format: non-numeric arg with a numeric specifier — pass through unchanged + ASSERT_EQUALS("abc", cppcheck::testing::expandMSBuildExpression("$([System.String]::Format('{0:D6}', 'abc'))")); + + // --- $([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("True", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::IsPathRooted('C:\\\\foo'))")); + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::IsPathRooted('\\\\server\\\\share'))")); + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::IsPathRooted('C:file.cpp'))")); // drive-relative: .NET returns True (has drive letter) + ASSERT_EQUALS("False", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::IsPathRooted('foo'))")); + // Path.Combine: 1, 2, 3, N args; absolute segment resets path + ASSERT_EQUALS("a", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::Combine('a'))")); + ASSERT_EQUALS("a/b", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::Combine('a', 'b'))")); + ASSERT_EQUALS("a/b/c", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::Combine('a', 'b', 'c'))")); + ASSERT_EQUALS("a/b/c/d", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::Combine('a', 'b', 'c', 'd'))")); + ASSERT_EQUALS("/abs/b", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::Combine('a', '/abs', 'b'))")); + ASSERT_EQUALS("/abs", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::Combine('a', 'b', '/abs'))")); + ASSERT_EQUALS(Path::fromNativeSeparators(Path::getCurrentPath()) + "/a/b/c/d", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::GetFullPath($([System.IO.Path]::Combine('a', 'b', 'c', 'd')))")); + + // --- $([System.IO.FileInfo]::new(...)) / $([System.IO.DirectoryInfo]::new(...)) --- + // ::new returns the normalized path; chains resolve on that string. + ASSERT_EQUALS("C:/foo/bar.cpp", cppcheck::testing::expandMSBuildExpression("$([System.IO.FileInfo]::new('C:/foo/bar.cpp').FullName)")); + ASSERT_EQUALS("C:/foo/", cppcheck::testing::expandMSBuildExpression("$([System.IO.FileInfo]::new('C:/foo/bar.cpp').DirectoryName)")); + ASSERT_EQUALS("bar.cpp", cppcheck::testing::expandMSBuildExpression("$([System.IO.FileInfo]::new('C:/foo/bar.cpp').Name)")); + ASSERT_EQUALS("C:/foo", cppcheck::testing::expandMSBuildExpression("$([System.IO.DirectoryInfo]::new('C:/foo').FullName)")); + // .Method(args) chain after ::new — e.g. replace in the resolved path + ASSERT_EQUALS("C:/foo/baz.cpp", cppcheck::testing::expandMSBuildExpression("$([System.IO.FileInfo]::new('C:/foo/bar.cpp').FullName.Replace('bar', 'baz'))")); + + // --- 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'", "", "")); + } + + void testVcxitemsPathResolution() const { + // importVcxitems must absolutise relative paths relative to ProjectDir, + // MSBuild always sets ProjectDir with a trailing slash — use that canonical form. + // Path styles differ by platform: Path::isAbsolute requires C:/ on Windows, / on Linux. +#ifdef _WIN32 + const std::string projDir = "C:/proj/"; + const std::string projSub = "C:/proj/sub/Shared.vcxitems"; + const std::string projRoot = "C:/proj/Shared.vcxitems"; + const std::string absPath = "C:/absolute/Shared.vcxitems"; + const std::string absInput = "C:/absolute/Shared.vcxitems"; +#else + const std::string projDir = "/proj/"; + const std::string projSub = "/proj/sub/Shared.vcxitems"; + const std::string projRoot = "/proj/Shared.vcxitems"; + const std::string absPath = "/absolute/Shared.vcxitems"; + const std::string absInput = "/absolute/Shared.vcxitems"; +#endif + // Relative path + ProjectDir → absolutised result + ASSERT_EQUALS(projSub, cppcheck::testing::resolveVcxitemsFilename("sub/Shared.vcxitems", projDir)); + ASSERT_EQUALS(projRoot, cppcheck::testing::resolveVcxitemsFilename("Shared.vcxitems", projDir)); + + // Already-absolute path must pass through unchanged regardless of ProjectDir + ASSERT_EQUALS(absPath, cppcheck::testing::resolveVcxitemsFilename(absInput, projDir)); + + // No ProjectDir → relative path is returned as-is + ASSERT_EQUALS("Shared.vcxitems", cppcheck::testing::resolveVcxitemsFilename("Shared.vcxitems", "")); + } + + void testMissingChildImportNonFatal() const { + // A .vcxproj that imports a non-existent .props file must still import + // successfully. Before the fix, result > NotResolvable caused errors.emplace_back() + // + return false, aborting the whole project. After the fix it is a debug warning + // and the remaining ClCompile items are still collected. + + // Use the current directory (no subdirectory) to avoid ScopedFile throwing + // "directory already exists" when the test is re-run without a clean build. + const ScopedFile mainCpp("testm3_main.cpp", ""); + const ScopedFile vcxproj( + "testm3.vcxproj", + "\n" + "\n" + " \n" + " \n" + " Debug\n" + " Win32\n" + " \n" + " \n" + " \n" + " Application\n" + " v143\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"); + + ImportProject project; + const ImportProject::Type result = project.import(vcxproj.path()); + + // Must succeed — missing child imports must not abort the project. + ASSERT_EQUALS(static_cast(ImportProject::Type::VS_VCXPROJ), static_cast(result)); + + // No hard errors — the missing import is demoted to a debug warning. + ASSERT(project.errors.empty()); + + // The source file must still have been collected despite the missing import. + const bool foundMain = std::any_of(project.fileSettings.begin(), project.fileSettings.end(), [](const FileSettings &fs) { + return fs.filename().find("main.cpp") != std::string::npos; + }); + ASSERT(foundMain); + } + + void testCurrentToolsVersionFromProps() const { + // L-3: "Current" keyword in relational conditions must use VisualStudioVersion + // from the properties map rather than a hardcoded major version number. + // + // evaluateVcxprojCondition uses an empty properties map, so VisualStudioVersion + // is absent and the fallback value { 18 } applies. These assertions verify + // the comparison logic is correct for the fallback case; an end-to-end + // .vcxproj test with set would be needed to exercise + // the property-lookup path directly. + + // fallback Current = MSBuildVersion::parse("18.0") = {18,0} (VS 2026) + // Now that the fallback is a two-component version "18.0", the equal-major + // comparisons work correctly without .NET single-component ambiguity. + // "Current" >= "18.0" -> {18,0} >= {18,0} -> true + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'Current' >= '18.0'", "Debug", "Win32")); + // "Current" <= "18.0" -> {18,0} <= {18,0} -> true + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'Current' <= '18.0'", "Debug", "Win32")); + // "Current" > "17.0" -> {18,0} > {17,0} -> true + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'Current' > '17.0'", "Debug", "Win32")); + // "17.0" < "Current" -> {17,0} < {18,0} -> true + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17.0' < 'Current'", "Debug", "Win32")); + // "Current" < "19.0" -> {18,0} < {19,0} -> true + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'Current' < '19.0'", "Debug", "Win32")); + // "Current" > "19.0" -> {18,0} > {19,0} -> false + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'Current' > '19.0'", "Debug", "Win32")); + } + + void testMetadataSelfReferenceCaseInsensitive() const { + // MSBuild item metadata names are case-insensitive (MetadataMap already + // uses cppcheck::stricmp for exactly this reason). Some .vcxproj/.props + // files use property-style accumulation in an ItemDefinitionGroup (e.g. + // $(PreprocessorDefinitions);EXTRA) + // instead of the %(...) item-metadata-reference syntax -- addMetadata() + // handles that "$(eName) self-reference" idiom by substituting in the + // metadata value accumulated so far. That substitution used to go through + // the plain, case-sensitive findAndReplace() instead of + // findAndReplaceCaseInsensitive() (used everywhere else in this file for + // exactly this kind of property/metadata name comparison), so a + // self-reference spelled with different casing than the element's own + // tag name -- e.g. $(preprocessordefinitions) inside a + // element -- was not recognized and was left + // in the output as literal, unresolved macro text instead of being + // replaced by the value accumulated from the preceding + // ItemDefinitionGroup. + // + // Two sequential ItemDefinitionGroup blocks exercise this: the first + // establishes the base value; the second references it back via + // $(preprocessordefinitions) in a different case than the + // tag itself. + + const ScopedFile mainCpp("testcasemeta_main.cpp", ""); + const ScopedFile vcxproj( + "testcasemeta.vcxproj", + "\n" + "\n" + " \n" + " \n" + " Debug\n" + " Win32\n" + " \n" + " \n" + " \n" + " Application\n" + " v143\n" + " \n" + " \n" + " \n" + " BASE_DEFINE\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " $(preprocessordefinitions);EXTRA_DEFINE\n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"); + + ImportProject project; + const ImportProject::Type result = project.import(vcxproj.path()); + ASSERT_EQUALS(static_cast(ImportProject::Type::VS_VCXPROJ), static_cast(result)); + + const auto it = std::find_if(project.fileSettings.begin(), project.fileSettings.end(), [](const FileSettings &fs) { + return fs.filename().find("testcasemeta_main.cpp") != std::string::npos; + }); + ASSERT(it != project.fileSettings.end()); + + // Must end with the fully-resolved accumulation + // "BASE_DEFINE=1;EXTRA_DEFINE=1" -- not a leftover literal + // "$(preprocessordefinitions)" macro reference, which would mean the + // case-mismatched self-reference was never recognized and substituted. + // (fs.defines is prefixed with the toolset's own predefined macros -- + // e.g. _MSC_VER -- which this test does not otherwise care about.) + ASSERT(it->defines.find("$(preprocessordefinitions)") == std::string::npos); + const std::string expectedSuffix = ";BASE_DEFINE=1;EXTRA_DEFINE=1"; + ASSERT(it->defines.size() >= expectedSuffix.size()); + ASSERT_EQUALS(expectedSuffix, it->defines.substr(it->defines.size() - expectedSuffix.size())); } // TODO: test fsParseCommand()