From c790d437e2e930f355728fdc6f3d264bf141bab7 Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Wed, 19 Aug 2026 18:37:20 -0400 Subject: [PATCH 01/14] refactor importing of visual studio files --- Makefile | 2 +- lib/importproject.cpp | 1054 +++++++++-------- lib/importproject.h | 20 +- test/cli/proj2_test.py | 9 +- test/cli/props-dirs/ProjA/ProjA.vcxproj | 32 + test/cli/props-dirs/ProjA/a.cpp | 11 + test/cli/props-dirs/ProjB/ProjB.vcxproj | 29 + test/cli/props-dirs/ProjB/b.cpp | 11 + test/cli/props-dirs/common/common.h | 3 + test/cli/props-dirs/common/common.props | 13 + test/cli/props-dirs/props-dirs.slnx | 7 + test/cli/props-dirs/shared/shared.props | 14 + test/cli/props_dirs_test.py | 78 ++ test/cli/vcxproj-unicode/main.cpp | 7 + .../vcxproj-unicode/vcxproj_unicode.vcxproj | 33 + test/cli/vcxproj_forced_includes/AllX64.h | 6 + test/cli/vcxproj_forced_includes/DebugX64.cpp | 8 + test/cli/vcxproj_forced_includes/DebugX64.h | 6 + .../vcxproj_forced_includes/GlobalDebugX64.h | 6 + .../GlobalReleaseX64.h | 6 + .../vcxproj_forced_includes/PropsDebugX64.h | 6 + .../vcxproj_forced_includes/PropsReleaseX64.h | 6 + .../vcxproj_forced_includes/ReleaseX64.cpp | 8 + test/cli/vcxproj_forced_includes/ReleaseX64.h | 6 + test/cli/vcxproj_forced_includes/foo.h | 1 + .../vcxproj_forced_includes.props | 8 + .../vcxproj_forced_includes.slnx | 6 + .../vcxproj_forced_includes.vcxproj | 103 ++ test/cli/vcxproj_forced_includes_test.py | 59 + test/cli/vcxproj_unicode_test.py | 42 + test/testimportproject.cpp | 88 +- 31 files changed, 1122 insertions(+), 566 deletions(-) create mode 100644 test/cli/props-dirs/ProjA/ProjA.vcxproj create mode 100644 test/cli/props-dirs/ProjA/a.cpp create mode 100644 test/cli/props-dirs/ProjB/ProjB.vcxproj create mode 100644 test/cli/props-dirs/ProjB/b.cpp create mode 100644 test/cli/props-dirs/common/common.h create mode 100644 test/cli/props-dirs/common/common.props create mode 100644 test/cli/props-dirs/props-dirs.slnx create mode 100644 test/cli/props-dirs/shared/shared.props create mode 100644 test/cli/props_dirs_test.py create mode 100644 test/cli/vcxproj-unicode/main.cpp create mode 100644 test/cli/vcxproj-unicode/vcxproj_unicode.vcxproj create mode 100644 test/cli/vcxproj_forced_includes/AllX64.h create mode 100644 test/cli/vcxproj_forced_includes/DebugX64.cpp create mode 100644 test/cli/vcxproj_forced_includes/DebugX64.h create mode 100644 test/cli/vcxproj_forced_includes/GlobalDebugX64.h create mode 100644 test/cli/vcxproj_forced_includes/GlobalReleaseX64.h create mode 100644 test/cli/vcxproj_forced_includes/PropsDebugX64.h create mode 100644 test/cli/vcxproj_forced_includes/PropsReleaseX64.h create mode 100644 test/cli/vcxproj_forced_includes/ReleaseX64.cpp create mode 100644 test/cli/vcxproj_forced_includes/ReleaseX64.h create mode 100644 test/cli/vcxproj_forced_includes/foo.h create mode 100644 test/cli/vcxproj_forced_includes/vcxproj_forced_includes.props create mode 100644 test/cli/vcxproj_forced_includes/vcxproj_forced_includes.slnx create mode 100644 test/cli/vcxproj_forced_includes/vcxproj_forced_includes.vcxproj create mode 100644 test/cli/vcxproj_forced_includes_test.py create mode 100644 test/cli/vcxproj_unicode_test.py diff --git a/Makefile b/Makefile index a7f3feefba6..f7680be02f7 100644 --- a/Makefile +++ b/Makefile @@ -819,7 +819,7 @@ test/testfunctions.o: test/testfunctions.cpp lib/check.h lib/checkers.h lib/chec test/testgarbage.o: test/testgarbage.cpp lib/check.h lib/checkers.h lib/checks.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/settings.h lib/smallvector.h lib/standards.h lib/templatesimplifier.h lib/token.h lib/tokenize.h lib/tokenlist.h lib/utils.h lib/vfvalue.h test/fixture.h test/helpers.h $(CXX) ${INCLUDE_FOR_TEST} ${CFLAGS_FOR_TEST} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ test/testgarbage.cpp -test/testimportproject.o: test/testimportproject.cpp externals/tinyxml2/tinyxml2.h lib/check.h lib/checkers.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/filesettings.h lib/importproject.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/utils.h lib/xml.h test/fixture.h test/redirect.h +test/testimportproject.o: test/testimportproject.cpp lib/check.h lib/checkers.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/filesettings.h lib/importproject.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/utils.h test/fixture.h test/redirect.h $(CXX) ${INCLUDE_FOR_TEST} ${CFLAGS_FOR_TEST} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ test/testimportproject.cpp test/testincompletestatement.o: test/testincompletestatement.cpp lib/check.h lib/checkers.h lib/checkimpl.h lib/checkother.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/settings.h lib/standards.h lib/tokenize.h lib/tokenlist.h lib/utils.h test/fixture.h test/helpers.h diff --git a/lib/importproject.cpp b/lib/importproject.cpp index 5c9bcaadd3f..fb6a5621b3b 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -28,6 +28,7 @@ #include "utils.h" #include +#include #include #include #include @@ -246,7 +247,7 @@ void ImportProject::fsSetDefines(FileSettings& fs, std::string defs) fs.defines.swap(defs); } -static bool simplifyPathWithVariables(std::string &s, std::map &variables) +static bool simplifyPathWithVariables(std::string &s, VariablesMap &variables) { std::set expanded; std::string::size_type start = 0; @@ -277,7 +278,24 @@ static bool simplifyPathWithVariables(std::string &s, std::map &in, std::map &variables) +static std::string toAbsolute(const std::string &path) +{ + if (Path::isAbsolute(path)) + return Path::simplifyPath(path); + return Path::simplifyPath(Path::getCurrentPath() + "/" + path); +} + +static std::string toAbsolute(const std::string &filename, const std::string &baseDir, VariablesMap &variables) +{ + std::string resolved(filename); + simplifyPathWithVariables(resolved, variables); + + if (Path::isAbsolute(resolved)) + return Path::simplifyPath(resolved); + return Path::simplifyPath(baseDir + resolved); +} + +void ImportProject::fsSetIncludePaths(FileSettings& fs, const std::string &basepath, const std::list &in, VariablesMap &variables) { std::set found; // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) @@ -342,9 +360,7 @@ ImportProject::Type ImportProject::import(const std::string &filename, Settings return ImportProject::Type::VS_SLNX; } } else if (endsWith(filename, ".vcxproj")) { - std::map variables; - std::vector sharedItemsProjects; - if (importVcxproj(filename, variables, "", fileFilters, sharedItemsProjects)) { + if (importVcxproj(toAbsolute(filename), mVariables, fileFilters)) { setRelativePaths(filename); return ImportProject::Type::VS_VCXPROJ; } @@ -452,7 +468,7 @@ bool ImportProject::importCompileCommands(std::istream &istr) path = Path::simplifyPath(directory + file); FileSettings fs{path, Standards::Language::None, 0}; // file will be identified later on parseArgs(fs, arguments); - std::map variables; + VariablesMap variables; fsSetIncludePaths(fs, directory, fs.includePaths, variables); // Assign a unique index to each file path. If the file path already exists in the map, // increment the index to handle duplicate file entries. @@ -480,11 +496,10 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const } } - std::map variables; - variables["SolutionDir"] = path; + const std::string solutionDir = toAbsolute(path); + mVariables["SolutionDir"] = solutionDir; bool found = false; - std::vector sharedItemsProjects; while (std::getline(istr,line)) { if (!startsWith(line,"Project(")) continue; @@ -496,10 +511,9 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const continue; std::string vcxproj(line.substr(pos1+1, pos-pos1+7)); vcxproj = Path::toNativeSeparators(std::move(vcxproj)); - if (!Path::isAbsolute(vcxproj)) - vcxproj = path + vcxproj; + vcxproj = toAbsolute(vcxproj, solutionDir, mVariables); vcxproj = Path::fromNativeSeparators(std::move(vcxproj)); - if (!importVcxproj(vcxproj, variables, "", fileFilters, sharedItemsProjects)) { + if (!importVcxproj(vcxproj, mVariables, fileFilters)) { errors.emplace_back("failed to load '" + vcxproj + "' from Visual Studio solution"); return false; } @@ -534,11 +548,9 @@ bool ImportProject::importSlnx(const std::string& filename, const std::vector variables; - variables["SolutionDir"] = Path::simplifyPath(Path::getPathFromFilename(filename)); + mVariables["SolutionDir"] = toAbsolute(Path::getPathFromFilename(filename)); bool found = false; - std::vector sharedItemsProjects; auto processProject = [&](const tinyxml2::XMLElement* projectNode) { const char* pathAttribute = projectNode->Attribute("Path"); @@ -551,11 +563,10 @@ bool ImportProject::importSlnx(const std::string& filename, const std::vectorAttribute("Include"); if (a) @@ -617,306 +627,284 @@ namespace { std::string platformStr; }; - struct Conditional { - explicit Conditional(const tinyxml2::XMLElement *idg){ - const char *condAttr = idg->Attribute("Condition"); - if (condAttr) - mCondition = condAttr; - } - explicit Conditional(std::string condition) : mCondition(std::move(condition)) {} - - static void replaceAll(std::string &c, const std::string &from, const std::string &to) { - std::string::size_type pos; - while ((pos = c.find(from)) != std::string::npos) { - c.erase(pos,from.size()); - c.insert(pos,to); - } - } - - // 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; - } - } - - static bool evalCondition(const std::string& condition, const ProjectConfiguration &p) { - std::string c = '(' + condition + ")"; - replaceAll(c, "$(Configuration)", p.configuration); - replaceAll(c, "$(Platform)", p.platformStr); - - const Settings s; - TokenList tokenlist(s, Standards::Language::C); - if (!tokenlist.createTokensFromBuffer(c.data(), c.size())) { - throw std::runtime_error("Can not tokenize condition"); + // 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 + std::string execute(const Token* tok, const VariablesMap &variables) { + if (!tok) + throw std::runtime_error("Missing operator"); + auto boolResult = [](bool b) -> std::string { + return b ? "True" : "False"; + }; + if (tok->isUnaryOp("!")) + return boolResult(execute(tok->astOperand1(), variables) == "False"); + if (tok->str() == "==") + return boolResult(execute(tok->astOperand1(), variables) == execute(tok->astOperand2(), variables)); + if (tok->str() == "!=") + return boolResult(execute(tok->astOperand1(), variables) != execute(tok->astOperand2(), variables)); + if (tok->str() == "<" || tok->str() == ">" || tok->str() == "<=" || tok->str() == ">=") { + const std::string lhs = execute(tok->astOperand1(), variables); + const std::string rhs = execute(tok->astOperand2(), variables); + // Try integer comparison first + char* lhsEnd = nullptr; + char* rhsEnd = nullptr; + const long lhsInt = std::strtol(lhs.c_str(), &lhsEnd, 10); + const long rhsInt = std::strtol(rhs.c_str(), &rhsEnd, 10); + if (lhsEnd == lhs.c_str() + lhs.size() && rhsEnd == rhs.c_str() + rhs.size()) { + if (tok->str() == "<") return boolResult(lhsInt < rhsInt); + if (tok->str() == ">") return boolResult(lhsInt > rhsInt); + if (tok->str() == "<=") return boolResult(lhsInt <= rhsInt); + if (tok->str() == ">=") return boolResult(lhsInt >= rhsInt); } - - // 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(); - } + // Fall back to dotted-version comparison (e.g. "14.0" >= "12.0") + const auto parseVersion = [](const std::string& s) -> std::vector { + std::vector parts; + std::string::size_type pos = 0; + // strip leading v/V + if (!s.empty() && (s[0] == 'v' || s[0] == 'V')) + pos = 1; + while (pos <= s.size()) { + const std::string::size_type dot = s.find('.', pos); + const std::string part = s.substr(pos, dot == std::string::npos ? std::string::npos : dot - pos); + char* end = nullptr; + const long val = std::strtol(part.c_str(), &end, 10); + if (end == part.c_str()) + return {}; // not a valid version part + parts.push_back(static_cast(val)); + if (dot == std::string::npos) + break; + pos = dot + 1; } - if (!lpar.empty()) - throw std::runtime_error("'(' without closing ')'!"); + // pad to 4 parts with zeros, matching System.Version semantics + while (parts.size() < 4) + parts.push_back(0); + return parts; + }; + const std::vector lhsVer = parseVersion(lhs); + const std::vector rhsVer = parseVersion(rhs); + if (!lhsVer.empty() && !rhsVer.empty()) { + if (tok->str() == "<") return boolResult(lhsVer < rhsVer); + if (tok->str() == ">") return boolResult(lhsVer > rhsVer); + if (tok->str() == "<=") return boolResult(lhsVer <= rhsVer); + if (tok->str() == ">=") return boolResult(lhsVer >= rhsVer); } - - // 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("||"); + throw std::runtime_error("Cannot compare '" + lhs + "' and '" + rhs + "'"); + } + if (tok->str() == "&&") + return boolResult(execute(tok->astOperand1(), variables) == "True" && execute(tok->astOperand2(), variables) == "True"); + if (tok->str() == "||") + return boolResult(execute(tok->astOperand1(), variables) == "True" || execute(tok->astOperand2(), variables) == "True"); + if (tok->str() == "(" && Token::Match(tok->previous(), "$ ( %name% . %name% (")) { + const std::string &varName = tok->strAt(1); + auto varIt = variables.find(varName); + std::string property = (varIt != variables.end()) ? varIt->second : varName; + const std::string &method = tok->strAt(3); + if (method == "ToUpper") { + std::string result = property; + std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { + return std::toupper(c); + }); + return result; } + if (method == "ToLower") { + std::string result = property; + std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { + return std::tolower(c); + }); + return result; + } + std::string arg = execute(tok->tokAt(4)->astOperand2(), variables); + if (method == "Contains") + return boolResult(property.find(arg) != std::string::npos); + if (method == "EndsWith") + return boolResult(endsWith(property,arg.c_str(),arg.size())); + if (method == "StartsWith") + return boolResult(startsWith(property,arg)); + throw std::runtime_error("Unhandled method '" + method + "'"); + } + if (tok->str().size() >= 2 && tok->str()[0] == '\'') // String Literal + return tok->str().substr(1, tok->str().size() - 2); + if (tok->str() == "(" && tok->previous() && caseInsensitiveStringCompare(tok->strAt(-1), "exists") == 0) { + return boolResult(false); + } + + throw std::runtime_error("Unknown/unhandled operator/operand '" + tok->str() + "'"); + } - tokenlist.createAst(); - - // Locate ast top and execute the condition - for (const Token *tok = tokenlist.front(); tok; tok = tok->next()) { - if (tok->astParent()) { - return execute(tok->astTop(), p) == "True"; + bool evalCondition(const std::string &condition, const VariablesMap &variables) { + std::string c = '(' + condition + ")"; + for (const auto &var : variables) + findAndReplace(c, "$(" + var.first + ")", var.second); + + const Settings s; + TokenList tokenlist(s, Standards::Language::C); + if (!tokenlist.createTokensFromBuffer(c.data(), c.size())) { + throw std::runtime_error("Can not tokenize condition"); + } + + // 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(); } } - throw std::runtime_error("Invalid condition: '" + condition + "'"); + if (!lpar.empty()) + throw std::runtime_error("'(' without closing ')'!"); } - - private: - - static std::string executeOp1(const Token* tok, const ProjectConfiguration &p) { - return execute(tok->astOperand1(), p); - } - - static std::string executeOp2(const Token* tok, const ProjectConfiguration &p) { - return execute(tok->astOperand2(), p); - } - - static std::string execute(const Token* tok, const ProjectConfiguration &p) { - if (!tok) - throw std::runtime_error("Missing operator"); - auto boolResult = [](bool b) -> std::string { - return b ? "True" : "False"; - }; - if (tok->isUnaryOp("!")) - return boolResult(executeOp1(tok, p) == "False"); - if (tok->str() == "==") - return boolResult(executeOp1(tok, p) == executeOp2(tok, p)); - if (tok->str() == "!=") - return boolResult(executeOp1(tok, p) != executeOp2(tok, p)); - if (tok->str() == "&&") - return boolResult(executeOp1(tok, p) == "True" && executeOp2(tok, p) == "True"); - if (tok->str() == "||") - return boolResult(executeOp1(tok, p) == "True" || executeOp2(tok, p) == "True"); - if (tok->str() == "(" && Token::Match(tok->previous(), "$ ( %name% . %name% (")) { - const std::string& propertyName = tok->strAt(1); - std::string propertyValue; - if (propertyName == "Configuration") - propertyValue = p.configuration; - else if (propertyName == "Platform") - propertyValue = p.platformStr; - else - throw std::runtime_error("Unhandled property '" + propertyName + "'"); - const std::string& method = tok->strAt(3); - std::string arg = executeOp2(tok->tokAt(4), p); - if (arg.size() >= 2 && arg[0] == '\'') - arg = arg.substr(1, arg.size() - 2); - if (method == "Contains") - return boolResult(propertyValue.find(arg) != std::string::npos); - if (method == "EndsWith") - return boolResult(endsWith(propertyValue,arg.c_str(),arg.size())); - if (method == "StartsWith") - return boolResult(startsWith(propertyValue,arg)); - throw std::runtime_error("Unhandled method '" + method + "'"); - } - if (tok->str().size() >= 2 && tok->str()[0] == '\'') // String Literal - return tok->str(); - - throw std::runtime_error("Unknown/unhandled operator/operand '" + tok->str() + "'"); + // 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("||"); } - std::string mCondition; - }; + tokenlist.createAst(); - 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; - } - } - } + // Locate ast top and execute the condition + for (const Token *tok = tokenlist.front(); tok; tok = tok->next()) { + if (tok->astParent()) { + return execute(tok->astTop(), variables) == "True"; } } + throw std::runtime_error("Invalid condition: '" + condition + "'"); + } - std::string enhancedInstructionSet; - std::string preprocessorDefinitions; - std::string additionalIncludePaths; - std::string entryPointSymbol; // TODO: use this - Standards::cppstd_t cppstd = Standards::CPPLatest; - }; + bool conditionIsTrue(const tinyxml2::XMLElement *node, const VariablesMap &variables) { + const char *condAttr = node->Attribute("Condition"); + if (!condAttr) + return true; + std::string condition(condAttr); + for (const auto &var : variables) + findAndReplace(condition, "$(" + var.first + ")", var.second); + return evalCondition(condition, variables); + } - 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; - } - } - } + bool hasName(const tinyxml2::XMLElement *node, const char *nodeName, const VariablesMap &variables) { + const char *name = node->Name(); + if (!name || std::strcmp(nodeName, name) != 0) + return false; + return conditionIsTrue(node, variables); + } - bool useOfMfc = false; - bool useUnicode = false; - }; + bool hasNameAndLabel(const tinyxml2::XMLElement *node, const char *nodeName, const char *nodeAttr, const VariablesMap &variables) { + const char *name = node->Name(); + const char *label = node->Attribute("Label"); + if (!name || !label || std::strcmp(nodeName, name) != 0 || std::strcmp(label, nodeAttr) != 0) + return false; + return conditionIsTrue(node, variables); + } - struct ItemGroupClCompile { - explicit ItemGroupClCompile(std::string filename) : mFilename(std::move(filename)) {} - ItemGroupClCompile(const tinyxml2::XMLElement *element, std::string file) : mFilename(std::move(file)) { - for (const tinyxml2::XMLElement* childElement = element->FirstChildElement(); childElement; childElement = childElement->NextSiblingElement()) { - const char *name = childElement->Name(); - if (!name) - continue; - if (std::strcmp(name, "ExcludedFromBuild") == 0) { - const char *condition = childElement->Attribute("Condition"); - const char *text = childElement->GetText(); - if (!condition || !text || std::strcmp(text, "true") != 0) - continue; - mConditions.emplace_back(condition); - } - // TODO: ForcedIncludeFiles and PrecompiledHeaderFile - } - } - 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; - } + bool hasNameAndNotLabel(const tinyxml2::XMLElement *node, const char *nodeName, const char *nodeAttr, const VariablesMap &variables) { + const char *name = node->Name(); + if (!name || std::strcmp(nodeName, name) != 0) + return false; + const char *label = node->Attribute("Label"); + if (label && std::strcmp(label, nodeAttr) == 0) return false; + return conditionIsTrue(node, variables); + } + + void addProperty(const tinyxml2::XMLElement *node, VariablesMap &variables) { + const char *eName = node->Name(); + const char *eText = node->GetText(); + if (eName && eText && conditionIsTrue(node, variables)) { + std::string original = variables[eName]; + std::string text(eText); + for (const auto &var : variables) + findAndReplace(text, "$(" + var.first + ")", var.second); + findAndReplace(text, "%(" + std::string(eName) + ")", original); + variables[eName] = text; } - std::string mFilename; - std::list mConditions; - }; -} + } -static std::list toStringList(const std::string &s) -{ - std::list ret; - std::string::size_type pos1 = 0; - std::string::size_type pos2; - while ((pos2 = s.find(';',pos1)) != std::string::npos) { - ret.push_back(s.substr(pos1, pos2-pos1)); - pos1 = pos2 + 1; - if (pos1 >= s.size()) - break; + std::string getProperty(const tinyxml2::XMLElement *node, const VariablesMap &variables, const std::string &original) { + std::string text; + const char *eName = node->Name(); + const char *eText = node->GetText(); + if (eName && eText && conditionIsTrue(node, variables)) { + text = eText; + for (const auto &var : variables) + findAndReplace(text, "$(" + var.first + ")", var.second); + findAndReplace(text, "%(" + std::string(eName) + ")", original); + } + return text; } - if (pos1 < s.size()) - ret.push_back(s.substr(pos1)); - return ret; -} -static void importPropertyGroup(const tinyxml2::XMLElement *node, std::map &variables, std::string &includePath) -{ - 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; - } + struct ItemGroupClCompile { + explicit ItemGroupClCompile(std::string filename) : filename(std::move(filename)) {} + std::string filename; + std::string additionalIncludeDirectories; + std::string forcedIncludeFiles; + std::string preprocessorDefinitions; + std::string languageStandard; + }; - } 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::list toStringList(const std::string &s) + { + std::list ret; + std::string::size_type pos1 = 0; + std::string::size_type pos2; + while ((pos2 = s.find(';',pos1)) != std::string::npos) { + ret.push_back(s.substr(pos1, pos2-pos1)); + pos1 = pos2 + 1; + if (pos1 >= s.size()) + break; } + if (pos1 < s.size()) + ret.push_back(s.substr(pos1)); + return ret; } } -static void loadVisualStudioProperties(const std::string &props, std::map &variables, std::string &includePath, const std::string &additionalIncludeDirectories, std::list &itemDefinitionGroupList) +enum class PropsImportResult : std::uint8_t { Ok, NotFound, Cycle }; + +static PropsImportResult loadVisualStudioProperties(const std::string &props, + VariablesMap &variables, + std::unordered_set &importStack) { std::string filename(props); // variables can't be resolved if (!simplifyPathWithVariables(filename, variables)) - return; + return PropsImportResult::NotFound; // prepend project dir (if it exists) to transform relative paths into absolute ones if (!Path::isAbsolute(filename) && variables.count("ProjectDir") > 0) - filename = Path::getAbsoluteFilePath(variables.at("ProjectDir") + filename); + filename = toAbsolute(filename, variables.at("ProjectDir"), variables); + + // detect circular property sheet imports (A imports B, B imports A, a file importing + // itself, ...) instead of recursing until the stack overflows - mirrors MSBuild's own + // import-cycle detection, which errors out rather than looping forever + const std::string simplifiedFilename = Path::simplifyPath(filename); + if (!importStack.insert(simplifiedFilename).second) + return PropsImportResult::Cycle; tinyxml2::XMLDocument doc; - if (doc.LoadFile(filename.c_str()) != tinyxml2::XML_SUCCESS) - return; + if (doc.LoadFile(filename.c_str()) != tinyxml2::XML_SUCCESS) { + importStack.erase(simplifiedFilename); + return PropsImportResult::NotFound; + } 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) { + if (rootnode == nullptr) { + importStack.erase(simplifiedFilename); + return PropsImportResult::NotFound; + } + + const std::string previousThisFileDir = variables["MSBuildThisFileDirectory"]; + variables["MSBuildThisFileDirectory"] = Path::simplifyPath(Path::getPathFromFilename(filename)); + + PropsImportResult ret = PropsImportResult::Ok; + for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); ret != PropsImportResult::Cycle && node; node = node->NextSiblingElement()) { + if (hasNameAndLabel(node, "ImportGroup", "PropertySheets", variables)) { + for (const tinyxml2::XMLElement *importGroup = node->FirstChildElement(); ret != PropsImportResult::Cycle && importGroup; importGroup = importGroup->NextSiblingElement()) { + if (hasName(importGroup, "Import", variables)) { const char *projectAttribute = importGroup->Attribute("Project"); if (projectAttribute == nullptr) continue; @@ -924,22 +912,120 @@ static void loadVisualStudioProperties(const std::string &props, std::mapFirstChildElement(); e; e = e->NextSiblingElement()) + addProperty(e, variables); + } else if (hasName(node, "ItemDefinitionGroup", variables)) { + for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { + if (hasName(e1, "ClCompile", variables)) { + for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) { + addProperty(e2, variables); + } + } else if (hasName(e1, "Link", variables)) { + for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) { + addProperty(e2, variables); // fixme + } + } + } + } + } + + variables["MSBuildThisFileDirectory"] = previousThisFileDir; + importStack.erase(simplifiedFilename); + return ret; +} + +// returns false only for a genuine import cycle; a props file that couldn't be found/parsed +// is silently ignored (see PropsImportResult::NotFound above) and this still returns true +static bool loadVisualStudioProperties(const std::string &props, + VariablesMap &variables) +{ + std::unordered_set importStack; + return loadVisualStudioProperties(props, variables, importStack) != PropsImportResult::Cycle; +} + +static bool importVcxitems(const std::string &filename, + VariablesMap &variables, + std::list &compileList) +{ + tinyxml2::XMLDocument doc; + const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); + if (error != tinyxml2::XML_SUCCESS) + return false; + + const tinyxml2::XMLElement *const rootnode = doc.FirstChildElement(); + if (rootnode == nullptr) + return false; + + const std::string itemsDir = Path::simplifyPath(Path::getPathFromFilename(filename)); + const std::string previousThisFileDir = variables["MSBuildThisFileDirectory"]; + variables["MSBuildThisFileDirectory"] = itemsDir; + + for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { + if (hasName(node, "ItemGroup", variables)) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { + if (hasName(e, "ClCompile", variables)) { + const char *include = e->Attribute("Include"); + if (!include || !Path::acceptFile(include)) + continue; + std::string toInclude = toAbsolute(include, itemsDir, variables); + + ItemGroupClCompile compile(toInclude); + // a file with no override of its own inherits the ItemDefinitionGroup value outright + compile.additionalIncludeDirectories = variables["AdditionalIncludeDirectories"]; + compile.forcedIncludeFiles = variables["ForcedIncludeFiles"]; + compile.preprocessorDefinitions = variables["PreprocessorDefinitions"]; + compile.languageStandard = variables["LanguageStandard"]; + bool excludedFromBuild = false; + + for (const tinyxml2::XMLElement *e1 = e->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { + const char *text = e1->GetText(); + if (!text) + continue; + + if (hasName(e1, "ExcludedFromBuild", variables)) { + if (std::strcmp(text, "true") == 0) { + excludedFromBuild = true; + break; + } + } else if (hasName(e1, "AdditionalIncludeDirectories", variables)) { + compile.additionalIncludeDirectories = getProperty(e1, variables, compile.additionalIncludeDirectories); + } else if (hasName(e1, "ForcedIncludeFiles", variables)) { + compile.forcedIncludeFiles = getProperty(e1, variables, compile.forcedIncludeFiles); + } else if (hasName(e1, "PreprocessorDefinitions", variables)) { + compile.preprocessorDefinitions = getProperty(e1, variables, compile.preprocessorDefinitions); + } else if (hasName(e1, "LanguageStandard", variables)) { + compile.languageStandard = getProperty(e1, variables, compile.languageStandard); + } + } + + if (!excludedFromBuild) + compileList.emplace_back(std::move(compile)); + } + } + } else if (hasName(node, "ItemDefinitionGroup", variables)) { + for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { + if (hasName(e1, "ClCompile", variables)) { + for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) { + addProperty(e2, variables); + } } } - } else if (std::strcmp(name,"PropertyGroup")==0) { - importPropertyGroup(node, variables, includePath); - } else if (std::strcmp(name,"ItemDefinitionGroup")==0) { - itemDefinitionGroupList.emplace_back(node, additionalIncludeDirectories); } } + + variables["MSBuildThisFileDirectory"] = previousThisFileDir; + return true; } bool ImportProject::importVcxproj(const std::string &filename, - std::map &variables, - const std::string &additionalIncludeDirectories, - const std::vector &fileFilters, - std::vector &cache) + VariablesMap &variables, + const std::vector &fileFilters) { tinyxml2::XMLDocument doc; const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); @@ -947,250 +1033,235 @@ 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)); + const auto solutionDirIt = variables.find("SolutionDir"); + const bool hasSolutionDir = solutionDirIt != variables.end(); + const std::string solutionDir = hasSolutionDir ? solutionDirIt->second : std::string(); + variables.clear(); + if (hasSolutionDir) + variables["SolutionDir"] = solutionDir; + + std::string projectDir = Path::simplifyPath(Path::getPathFromFilename(filename)); + + variables["ProjectDir"] = projectDir; + variables.emplace("SolutionDir", projectDir); + variables["MSBuildThisFileDirectory"] = projectDir; + + VariablesMap originalVariables = variables; std::list projectConfigurationList; std::list compileList; - std::list itemDefinitionGroupList; - std::vector configurationPropertyGroups; - std::string includePath; - std::vector sharedItemsProjects; const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement(); if (rootnode == nullptr) { errors.emplace_back("Visual Studio project file has no XML root node"); return false; } + + // find all Visual Studio project configurations for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { - const char* name = node->Name(); - if (std::strcmp(name, "ItemGroup") == 0) { - const char *labelAttribute = node->Attribute("Label"); - if (labelAttribute && std::strcmp(labelAttribute, "ProjectConfigurations") == 0) { - for (const tinyxml2::XMLElement *cfg = node->FirstChildElement(); cfg; cfg = cfg->NextSiblingElement()) { - if (std::strcmp(cfg->Name(), "ProjectConfiguration") == 0) { - const ProjectConfiguration p(cfg); - if (p.platform != ProjectConfiguration::Unknown) { - projectConfigurationList.emplace_back(cfg); - mAllVSConfigs.insert(p.configuration); - } - } + if (hasNameAndLabel(node, "ItemGroup", "ProjectConfigurations", variables)) { + for (const tinyxml2::XMLElement *pcNode = node->FirstChildElement("ProjectConfiguration"); pcNode; pcNode = pcNode->NextSiblingElement("ProjectConfiguration")) { + const ProjectConfiguration pc(pcNode); + if (pc.platform != ProjectConfiguration::Unknown) { + projectConfigurationList.emplace_back(pc); + mAllVSConfigs.insert(pc.configuration); } - } else { + } + } + } + + bool first = true; + + for (const ProjectConfiguration &pc : projectConfigurationList) { + if (!first) { + compileList.clear(); + variables = originalVariables; + } else + first = false; + + variables["Configuration"] = pc.configuration; + variables["Platform"] = pc.platformStr; + + for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { + if (hasNameAndNotLabel(node, "ItemGroup", "ProjectConfigurations", variables)) { for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (std::strcmp(e->Name(), "ClCompile") == 0) { + if (hasName(e, "ClCompile", variables)) { 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); + std::string toInclude = toAbsolute(include, projectDir, variables); + ItemGroupClCompile compile(toInclude); + // a file with no override of its own inherits the ItemDefinitionGroup value outright + compile.additionalIncludeDirectories = variables["AdditionalIncludeDirectories"]; + compile.forcedIncludeFiles = variables["ForcedIncludeFiles"]; + compile.preprocessorDefinitions = variables["PreprocessorDefinitions"]; + compile.languageStandard = variables["LanguageStandard"]; + bool excludedFromBuild = false; + + for (const tinyxml2::XMLElement *e1 = e->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { + const char *text = e1->GetText(); + if (!text) + continue; + + if (hasName(e1, "ExcludedFromBuild", variables)) { + if (std::strcmp(text, "true") == 0) { + excludedFromBuild = true; + break; + } + } else if (hasName(e1, "AdditionalIncludeDirectories", variables)) { + compile.additionalIncludeDirectories = getProperty(e1, variables, compile.additionalIncludeDirectories); + } else if (hasName(e1, "ForcedIncludeFiles", variables)) { + compile.forcedIncludeFiles = getProperty(e1, variables, compile.forcedIncludeFiles); + } else if (hasName(e1, "PreprocessorDefinitions", variables)) { + compile.preprocessorDefinitions = getProperty(e1, variables, compile.preprocessorDefinitions); + } else if (hasName(e1, "LanguageStandard", variables)) { + compile.languageStandard = getProperty(e1, variables, compile.languageStandard); + } + } + + if (!excludedFromBuild) + compileList.emplace_back(compile); } } } - } - } 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 (hasName(node, "ItemDefinitionGroup", variables)) { + for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { + if (hasName(e1, "ClCompile", variables)) { + for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) { + addProperty(e2, variables); + } + } else if (hasName(e1, "Link", variables)) { + for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) { + addProperty(e2, variables); // fixme + } } } - } else if (labelAttribute && std::strcmp(labelAttribute, "Shared") == 0) { - for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (std::strcmp(e->Name(), "Import") == 0) { - const char *projectAttribute = e->Attribute("Project"); - if (projectAttribute) { - // Path to shared items project is relative to current project directory, - // unless the string starts with $(SolutionDir) - std::string pathToSharedItemsFile; - if (std::string(projectAttribute).rfind("$(SolutionDir)", 0) == 0) { - pathToSharedItemsFile = projectAttribute; - } else { - pathToSharedItemsFile = variables["ProjectDir"] + projectAttribute; - } - if (!simplifyPathWithVariables(pathToSharedItemsFile, variables)) { - errors.emplace_back("Could not simplify path to referenced shared items project"); + } else if (hasName(node, "PropertyGroup", variables)) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) + addProperty(e, variables); + } else if (hasName(node, "ImportGroup", variables)) { + const char *labelAttribute = node->Attribute("Label"); + if (labelAttribute && std::strcmp(labelAttribute, "PropertySheets") == 0) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { + if (hasName(e, "Import", variables)) { + const char *projectAttribute = e->Attribute("Project"); + if (projectAttribute && !loadVisualStudioProperties(projectAttribute, variables)) { + errors.emplace_back("Could not load property sheet \"" + std::string(projectAttribute) + "\" - it may be missing, invalid, or part of a circular import"); 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; + } + } + } else if (labelAttribute && std::strcmp(labelAttribute, "Shared") == 0) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { + if (hasName(e, "Import", variables)) { + const char *projectAttribute = e->Attribute("Project"); + if (projectAttribute) { + // Path to shared items project is relative to current project directory, + // unless the string starts with $(SolutionDir) + std::string pathToSharedItemsFile = projectAttribute; + if (!simplifyPathWithVariables(pathToSharedItemsFile, variables)) { + errors.emplace_back("Could not simplify path to referenced shared items project"); + return false; + } + + if (!importVcxitems(pathToSharedItemsFile, variables, compileList)) { + errors.emplace_back("Could not load shared items project \"" + pathToSharedItemsFile + "\" from original path \"" + std::string(projectAttribute) + "\"."); + return false; + } } - sharedItemsProjects.emplace_back(toAdd); } } } + } else if (hasName(node, "Import", variables)) { + const char *projectAttribute = node->Attribute("Project"); + if (projectAttribute && !loadVisualStudioProperties(projectAttribute, variables)) { + errors.emplace_back("Could not load property sheet \"" + std::string(projectAttribute) + "\" - it may be missing, invalid, or part of a circular import"); + return false; + } } } - } - // # TODO: support signedness of char via /J (and potential XML option for it)? - // we can only set it globally but in this context it needs to be treated per file - - // Include shared items project files - std::vector sharedItemsIncludePaths; - for (const auto& sharedProject : sharedItemsProjects) { - for (const auto &file : sharedProject.sourceFiles) { - std::string pathToFile = Path::simplifyPath(Path::getPathFromFilename(sharedProject.pathToProjectFile) + file); - compileList.emplace_back(pathToFile); - } - for (const auto &p : sharedProject.includePaths) { - std::string path = Path::simplifyPath(Path::getPathFromFilename(sharedProject.pathToProjectFile) + p); - sharedItemsIncludePaths.emplace_back(std::move(path)); - } - } - // Project files - PathMatch filtermatcher(fileFilters, Path::getCurrentPath()); - for (const ItemGroupClCompile& compile : compileList) { - if (!fileFilters.empty() && !filtermatcher.match(compile.mFilename)) - continue; + // # TODO: support signedness of char via /J (and potential XML option for it)? + // we can only set it globally but in this context it needs to be treated per file - for (const ProjectConfiguration &p : projectConfigurationList) { + // Project files + PathMatch filtermatcher(fileFilters, Path::getCurrentPath()); + for (const ItemGroupClCompile &compile : compileList) { + if (!fileFilters.empty() && !filtermatcher.match(compile.filename)) + continue; if (!guiProject.checkVsConfigs.empty()) { - const bool doChecking = std::any_of(guiProject.checkVsConfigs.cbegin(), guiProject.checkVsConfigs.cend(), [&](const std::string& c) { - return c == p.configuration; + const bool doChecking = std::any_of(guiProject.checkVsConfigs.cbegin(), guiProject.checkVsConfigs.cend(), [&](const std::string &c) { + return c == pc.configuration; }); if (!doChecking) continue; } - // check if the file should be excluded for this configuration - if (compile.exclude(p, errors)) - continue; - - FileSettings fs{ compile.mFilename, Standards::Language::None, 0}; // file will be identified later on - fs.cfg = p.name; + FileSettings fs{ compile.filename, Standards::Language::None, 0 }; // file will be identified later on + fs.cfg = pc.name; // TODO: detect actual MSC version fs.msc = true; fs.defines = "_WIN32=1"; - if (p.platform == ProjectConfiguration::Win32) + if (pc.platform == ProjectConfiguration::Win32) fs.platformType = Platform::Type::Win32W; - else if (p.platform == ProjectConfiguration::x64) { + else if (pc.platform == ProjectConfiguration::x64) { fs.platformType = Platform::Type::Win64; fs.defines += ";_WIN64=1"; } - 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; - } + + Standards::cppstd_t cppstd = Standards::CPPLatest; + const std::string &languageStandard = compile.languageStandard; + if (languageStandard == "stdcpp14") + cppstd = Standards::CPP14; + else if (languageStandard == "stdcpp17") + cppstd = Standards::CPP17; + else if (languageStandard == "stdcpp20") + cppstd = Standards::CPP20; + else if (languageStandard == "stdcpp23") + cppstd = Standards::CPP23; + else if (languageStandard == "stdcpplatest") + cppstd = Standards::CPPLatest; + fs.standard = Standards::getCPP(cppstd); + + std::string enableEnhancedInstructionSet = variables["EnableEnhancedInstructionSet"]; + if (enableEnhancedInstructionSet == "StreamingSIMDExtensions") + fs.defines += ";__SSE__"; + else if (enableEnhancedInstructionSet == "StreamingSIMDExtensions2") + fs.defines += ";__SSE2__"; + else if (enableEnhancedInstructionSet == "AdvancedVectorExtensions") + fs.defines += ";__AVX__"; + else if (enableEnhancedInstructionSet == "AdvancedVectorExtensions2") + fs.defines += ";__AVX2__"; + else if (enableEnhancedInstructionSet == "AdvancedVectorExtensions512") + fs.defines += ";__AVX512__"; + + auto it = variables.find("CharacterSet"); + bool useUnicode = it != variables.end() && it->second == "Unicode"; + + const auto useOfMfcIt = variables.find("UseOfMfc"); + fs.useMfc = useOfMfcIt != variables.end() && !useOfMfcIt->second.empty() && + caseInsensitiveStringCompare(useOfMfcIt->second, "false") != 0; + if (useUnicode) { fs.defines += ";UNICODE=1;_UNICODE=1"; } - fsSetDefines(fs, fs.defines); - fsSetIncludePaths(fs, Path::getPathFromFilename(compile.mFilename), toStringList(includePath + ';' + additionalIncludePaths), variables); - for (const auto &path : sharedItemsIncludePaths) { - fs.includePaths.emplace_back(path); - } - fileSettings.push_back(std::move(fs)); - } - } - - return true; -} - -ImportProject::SharedItemsProject ImportProject::importVcxitems(const std::string& filename, const std::vector& fileFilters, std::vector &cache) -{ - auto isInCacheCheck = [filename](const ImportProject::SharedItemsProject& e) -> bool { - return filename == e.pathToProjectFile; - }; - const auto iterator = std::find_if(cache.begin(), cache.end(), isInCacheCheck); - if (iterator != std::end(cache)) { - return *iterator; - } - - SharedItemsProject result; - result.pathToProjectFile = filename; - - PathMatch filtermatcher(fileFilters, Path::getCurrentPath()); - - tinyxml2::XMLDocument doc; - const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); - if (error != tinyxml2::XML_SUCCESS) { - errors.emplace_back(std::string("Visual Studio project file is not a valid XML - ") + tinyxml2::XMLDocument::ErrorIDToName(error)); - return result; - } - const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement(); - if (rootnode == nullptr) { - errors.emplace_back("Visual Studio project file has no XML root node"); - return result; - } - for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { - if (std::strcmp(node->Name(), "ItemGroup") == 0) { - for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (std::strcmp(e->Name(), "ClCompile") == 0) { - const char* include = e->Attribute("Include"); - if (include && Path::acceptFile(include)) { - std::string file(include); - findAndReplace(file, "$(MSBuildThisFileDirectory)", "./"); - - // Skip file if it doesn't match the filter - if (!fileFilters.empty() && !filtermatcher.match(file)) - continue; - result.sourceFiles.emplace_back(file); - } else { - errors.emplace_back("Could not find shared items source file"); - return result; - } - } - } - } else if (std::strcmp(node->Name(), "ItemDefinitionGroup") == 0) { - ItemDefinitionGroup temp(node, ""); - for (const auto& includePath : toStringList(temp.additionalIncludePaths)) { - if (includePath == "%(AdditionalIncludeDirectories)") - continue; + std::string defines = fs.defines; + defines += (";" + compile.preprocessorDefinitions); + fsSetDefines(fs, defines); + fsSetIncludePaths(fs, projectDir, toStringList(variables["IncludePath"]), variables); + fs.systemIncludePaths = std::move(fs.includePaths); + fsSetIncludePaths(fs, projectDir, toStringList(compile.additionalIncludeDirectories), variables); + fs.forcedIncludes = toStringList(compile.forcedIncludeFiles); + for (auto &forcedInclude : fs.forcedIncludes) + forcedInclude = toAbsolute(forcedInclude, projectDir, variables); - std::string toAdd(includePath); - findAndReplace(toAdd, "$(MSBuildThisFileDirectory)", "./"); - result.includePaths.emplace_back(toAdd); - } + fileSettings.push_back(std::move(fs)); } } - result.successful = true; - cache.emplace_back(result); - return result; + return true; } bool ImportProject::importBcb6Prj(const std::string &projectFilename) @@ -1435,7 +1506,7 @@ bool ImportProject::importBcb6Prj(const std::string &projectFilename) // Reading the BCB6 install location from registry in windows environments would also be possible, // but I didn't see any such functionality around the source. Not in favor of adding it only // for the BCB6 project loading. - std::map variables; + VariablesMap variables; const std::string defines = predefines + ";" + sysdefines + ";" + userdefines; const std::string cppDefines = cppPredefines + ";" + defines; const bool forceCppMode = (cflags.find("-P") != cflags.end()); @@ -1780,16 +1851,23 @@ void ImportProject::setRelativePaths(const std::string &filename) const std::string rel = Path::getRelativePath(includePath, basePaths); includePath = rel.empty() ? "." : rel; } + for (auto &includePath: fs.systemIncludePaths) { + const std::string rel = Path::getRelativePath(includePath, basePaths); + includePath = rel.empty() ? "." : rel; + } + for (auto &forcedInclude: fs.forcedIncludes) + forcedInclude = Path::getRelativePath(forcedInclude, basePaths); } } // only used by tests (testimportproject.cpp::testVcxprojConditions): // cppcheck-suppress unusedFunction -bool cppcheck::testing::evaluateVcxprojCondition(const std::string& condition, const std::string& configuration, +bool cppcheck::testing::evaluateVcxprojCondition(const std::string& condition, + const std::string& configuration, const std::string& platform) { - ProjectConfiguration p; - p.configuration = configuration; - p.platformStr = platform; - return Conditional::evalCondition(condition, p); + VariablesMap variables; + variables["Platform"] = platform; + variables["Configuration"] = configuration; + return evalCondition(condition, variables); } diff --git a/lib/importproject.h b/lib/importproject.h index b8bbbed3fa3..0c601c914de 100644 --- a/lib/importproject.h +++ b/lib/importproject.h @@ -36,9 +36,6 @@ class Settings; struct Suppressions; -namespace tinyxml2 { - class XMLDocument; -} /// @addtogroup Core /// @{ @@ -56,11 +53,14 @@ namespace cppcheck { } } +using VariablesMap = std::map; + /** * @brief Importing project settings. */ class CPPCHECKLIB WARN_UNUSED ImportProject { public: + enum class Type : std::uint8_t { NONE, UNKNOWN, @@ -76,7 +76,7 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { 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); + static void fsSetIncludePaths(FileSettings& fs, const std::string &basepath, const std::list &in, VariablesMap &variables); public: std::list fileSettings; @@ -106,28 +106,22 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { void ignoreOtherConfigs(const std::string &cfg); Type import(const std::string &filename, Settings *settings=nullptr, Suppressions *supprs=nullptr); + protected: bool importCompileCommands(std::istream &istr); bool importCppcheckGuiProject(std::istream &istr, Settings &settings, Suppressions &supprs); static std::string collectArgs(const std::string &cmd, std::vector &args); void setRelativePaths(const std::string &filename); - struct SharedItemsProject { - bool successful = false; - std::string pathToProjectFile; - std::vector includePaths; - std::vector sourceFiles; - }; + VariablesMap mVariables; - bool importVcxproj(const std::string &filename, std::map &variables, const std::string &additionalIncludeDirectories, const std::vector &fileFilters, std::vector &cache); - bool importVcxproj(const std::string &filename, const tinyxml2::XMLDocument &doc, std::map &variables, const std::string &additionalIncludeDirectories, const std::vector &fileFilters, std::vector &cache); + bool importVcxproj(const std::string &filename, VariablesMap &variables, const std::vector &fileFilters); private: static void parseArgs(FileSettings &fs, const std::vector &args); bool importSln(std::istream &istr, const std::string &path, const std::vector &fileFilters); bool importSlnx(const std::string& filename, const std::vector& fileFilters); - SharedItemsProject importVcxitems(const std::string &filename, const std::vector &fileFilters, std::vector &cache); bool importBcb6Prj(const std::string &projectFilename); std::string mPath; diff --git a/test/cli/proj2_test.py b/test/cli/proj2_test.py index c9516d9ddbf..aa8cf6d3355 100644 --- a/test/cli/proj2_test.py +++ b/test/cli/proj2_test.py @@ -18,6 +18,11 @@ 'x = 3 / 0;\n' + ' ^\n') % os.path.join('b', 'b.c') +def __get_lines(s): + # file order is not guaranteed when multiple jobs are used (TEST_CPPCHECK_INJECT_J) so + # compare output order-independently + return sorted(s.split('\n')) + def __create_compile_commands(proj_dir): proj_dir = str(proj_dir) j = [{'directory': os.path.join(proj_dir, 'a'), 'command': 'gcc -c a.c', 'file': 'a.c'}, @@ -152,7 +157,7 @@ def test_gui_project_loads_relative_vs_solution_2(tmp_path): create_gui_project_file(os.path.join(tmp_path, 'test.cppcheck'), root_path='proj2', import_project='proj2/proj2.sln') ret, stdout, stderr = cppcheck(['--project=test.cppcheck'], cwd=tmp_path) assert ret == 0, stdout - assert stderr == __ERR_A + __ERR_B + assert __get_lines(stderr) == __get_lines(__ERR_A + __ERR_B) def test_gui_project_loads_relative_vs_solution_with_exclude(tmp_path): proj_dir = tmp_path / 'proj2' @@ -170,4 +175,4 @@ def test_gui_project_loads_absolute_vs_solution_2(tmp_path): import_project=os.path.join(proj_dir, 'proj2.sln')) ret, stdout, stderr = cppcheck(['--project=test.cppcheck'], cwd=tmp_path) assert ret == 0, stdout - assert stderr == __ERR_A + __ERR_B + assert __get_lines(stderr) == __get_lines(__ERR_A + __ERR_B) diff --git a/test/cli/props-dirs/ProjA/ProjA.vcxproj b/test/cli/props-dirs/ProjA/ProjA.vcxproj new file mode 100644 index 00000000000..5ce4fbd069d --- /dev/null +++ b/test/cli/props-dirs/ProjA/ProjA.vcxproj @@ -0,0 +1,32 @@ + + + + + Debug + x64 + + + + {a1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1} + ProjA + + + + Application + v143 + + + + + + + + + PROJA_DEFINE;%(PreprocessorDefinitions) + + + + + + + diff --git a/test/cli/props-dirs/ProjA/a.cpp b/test/cli/props-dirs/ProjA/a.cpp new file mode 100644 index 00000000000..4eb775af468 --- /dev/null +++ b/test/cli/props-dirs/ProjA/a.cpp @@ -0,0 +1,11 @@ +#include "common.h" + +#ifndef COMMON_H_INCLUDED_MARKER +#error "common.h was not found - AdditionalIncludeDirectories from common.props did not resolve" +#endif + +int main() +{ + int x = 1; + return x / 0; +} diff --git a/test/cli/props-dirs/ProjB/ProjB.vcxproj b/test/cli/props-dirs/ProjB/ProjB.vcxproj new file mode 100644 index 00000000000..047f5d4b8fa --- /dev/null +++ b/test/cli/props-dirs/ProjB/ProjB.vcxproj @@ -0,0 +1,29 @@ + + + + + Debug + x64 + + + + {b2b2b2b2-b2b2-b2b2-b2b2-b2b2b2b2b2b2} + ProjB + + + + Application + v143 + + + + + + + + + + + diff --git a/test/cli/props-dirs/ProjB/b.cpp b/test/cli/props-dirs/ProjB/b.cpp new file mode 100644 index 00000000000..c24977c13ae --- /dev/null +++ b/test/cli/props-dirs/ProjB/b.cpp @@ -0,0 +1,11 @@ +#include "common.h" + +#ifndef COMMON_H_INCLUDED_MARKER +#error "common.h was not found - AdditionalIncludeDirectories from common.props did not resolve" +#endif + +int main() +{ + int y = 2; + return y / 0; +} diff --git a/test/cli/props-dirs/common/common.h b/test/cli/props-dirs/common/common.h new file mode 100644 index 00000000000..72674fc6c62 --- /dev/null +++ b/test/cli/props-dirs/common/common.h @@ -0,0 +1,3 @@ +#ifndef COMMON_H_INCLUDED_MARKER +#define COMMON_H_INCLUDED_MARKER +#endif diff --git a/test/cli/props-dirs/common/common.props b/test/cli/props-dirs/common/common.props new file mode 100644 index 00000000000..f969a6e897d --- /dev/null +++ b/test/cli/props-dirs/common/common.props @@ -0,0 +1,13 @@ + + + + + + COMMON_DEFINE;%(PreprocessorDefinitions) + $(MSBuildThisFileDirectory);%(AdditionalIncludeDirectories) + stdcpp17 + + + diff --git a/test/cli/props-dirs/props-dirs.slnx b/test/cli/props-dirs/props-dirs.slnx new file mode 100644 index 00000000000..0aae89ada2a --- /dev/null +++ b/test/cli/props-dirs/props-dirs.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/cli/props-dirs/shared/shared.props b/test/cli/props-dirs/shared/shared.props new file mode 100644 index 00000000000..623e1094e9f --- /dev/null +++ b/test/cli/props-dirs/shared/shared.props @@ -0,0 +1,14 @@ + + + + + + + + + SHARED_DEFINE;%(PreprocessorDefinitions) + + + diff --git a/test/cli/props_dirs_test.py b/test/cli/props_dirs_test.py new file mode 100644 index 00000000000..123cc010b5e --- /dev/null +++ b/test/cli/props_dirs_test.py @@ -0,0 +1,78 @@ + +# python -m pytest props_dirs_test.py +# +# Regression coverage for MSBuild property-sheet (.props) loading across multiple +# directories: +# - $(MSBuildThisFileDirectory) must resolve to each .props file's own directory, +# not the importing project's directory, even through a chain of nested imports +# (ProjA/ -> shared/shared.props -> common/common.props). +# - AdditionalIncludeDirectories set via that chain must actually make a header in a +# different directory (common/common.h) resolvable from the project's source file. +# - A project that imports common/common.props directly (ProjB) must pick up exactly +# what that file sets and nothing that a *different* project in the same solution +# (ProjA) added on top - no cross-project variable leakage. + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + +__ERR_A = ('%s:10:14: error: Division by zero. [zerodiv]\n' + ' return x / 0;\n' + ' ^\n') % os.path.join('props-dirs', 'ProjA', 'a.cpp') +__ERR_B = ('%s:10:14: error: Division by zero. [zerodiv]\n' + ' return y / 0;\n' + ' ^\n') % os.path.join('props-dirs', 'ProjB', 'b.cpp') + + +def __get_lines(s): + # file order is not guaranteed when multiple jobs are used (TEST_CPPCHECK_INJECT_J) so + # compare output order-independently + return sorted(s.split('\n')) + + +def test_props_dirs_solution(): + args = [ + '--project=props-dirs/props-dirs.slnx', + '--no-cppcheck-build-dir' + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + # both files were actually analyzed (division by zero fires) which also proves + # "common.h" was found via AdditionalIncludeDirectories - if it hadn't resolved, the + # #error guard in each .cpp would have fired instead and there would be no zerodiv + assert __get_lines(stderr) == __get_lines(__ERR_A + __ERR_B) + + +def test_props_dirs_defines_and_standard(): + args = [ + '--project=props-dirs/props-dirs.slnx', + '--no-cppcheck-build-dir', + '--dump' + ] + ret, stdout, _ = cppcheck(args, cwd=__script_dir) + assert ret == 0, stdout + + dump_a = os.path.join(__script_dir, 'props-dirs', 'ProjA', 'a.cpp.dump') + dump_b = os.path.join(__script_dir, 'props-dirs', 'ProjB', 'b.cpp.dump') + assert os.path.exists(dump_a), f"Dump file not found at {dump_a}" + assert os.path.exists(dump_b), f"Dump file not found at {dump_b}" + + with open(dump_a, 'rt') as f: + dump_a_content = f.read() + with open(dump_b, 'rt') as f: + dump_b_content = f.read() + + # ProjA imports shared/shared.props (which itself imports common/common.props), and + # also sets its own PROJA_DEFINE - all three must be present, most specific first + assert 'cfg="_WIN32=1;_WIN64=1;PROJA_DEFINE=1;SHARED_DEFINE=1;COMMON_DEFINE=1;_MSC_VER=1900"' in dump_a_content + assert '' in dump_a_content + + # ProjB imports common/common.props directly - it must see COMMON_DEFINE, but neither + # PROJA_DEFINE nor SHARED_DEFINE, which only ever applied to ProjA + assert 'cfg="_WIN32=1;_WIN64=1;COMMON_DEFINE=1;_MSC_VER=1900"' in dump_b_content + assert '' in dump_b_content + assert 'PROJA_DEFINE' not in dump_b_content + assert 'SHARED_DEFINE' not in dump_b_content diff --git a/test/cli/vcxproj-unicode/main.cpp b/test/cli/vcxproj-unicode/main.cpp new file mode 100644 index 00000000000..1a0e6f02e86 --- /dev/null +++ b/test/cli/vcxproj-unicode/main.cpp @@ -0,0 +1,7 @@ +#include + +int main() { + std::cout << "Hello world!" << std::endl; + return 0; +} + diff --git a/test/cli/vcxproj-unicode/vcxproj_unicode.vcxproj b/test/cli/vcxproj-unicode/vcxproj_unicode.vcxproj new file mode 100644 index 00000000000..e85592a647e --- /dev/null +++ b/test/cli/vcxproj-unicode/vcxproj_unicode.vcxproj @@ -0,0 +1,33 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + + + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + NotSet + Static + + + + + diff --git a/test/cli/vcxproj_forced_includes/AllX64.h b/test/cli/vcxproj_forced_includes/AllX64.h new file mode 100644 index 00000000000..0c3063b59f4 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/AllX64.h @@ -0,0 +1,6 @@ +class all +{ + all() { + int x = 3 / 0; (void)x; // ERROR + } +}; diff --git a/test/cli/vcxproj_forced_includes/DebugX64.cpp b/test/cli/vcxproj_forced_includes/DebugX64.cpp new file mode 100644 index 00000000000..cfb1fce687a --- /dev/null +++ b/test/cli/vcxproj_forced_includes/DebugX64.cpp @@ -0,0 +1,8 @@ +#include + +int foo() +{ + std::cout << "DebugX64\n"; + int x = 3 / 0; (void)x; // ERROR + return 0; +} diff --git a/test/cli/vcxproj_forced_includes/DebugX64.h b/test/cli/vcxproj_forced_includes/DebugX64.h new file mode 100644 index 00000000000..ab3bfb495da --- /dev/null +++ b/test/cli/vcxproj_forced_includes/DebugX64.h @@ -0,0 +1,6 @@ +class debug +{ + debug() { + int x = 3 / 0; (void)x; // ERROR + } +}; \ No newline at end of file diff --git a/test/cli/vcxproj_forced_includes/GlobalDebugX64.h b/test/cli/vcxproj_forced_includes/GlobalDebugX64.h new file mode 100644 index 00000000000..48038886307 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/GlobalDebugX64.h @@ -0,0 +1,6 @@ +class global +{ + global() { + int x = 3 / 0; (void)x; // ERROR + } +}; diff --git a/test/cli/vcxproj_forced_includes/GlobalReleaseX64.h b/test/cli/vcxproj_forced_includes/GlobalReleaseX64.h new file mode 100644 index 00000000000..48038886307 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/GlobalReleaseX64.h @@ -0,0 +1,6 @@ +class global +{ + global() { + int x = 3 / 0; (void)x; // ERROR + } +}; diff --git a/test/cli/vcxproj_forced_includes/PropsDebugX64.h b/test/cli/vcxproj_forced_includes/PropsDebugX64.h new file mode 100644 index 00000000000..49643c7ea86 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/PropsDebugX64.h @@ -0,0 +1,6 @@ +class props +{ + props() { + int x = 3 / 0; (void)x; // ERROR + } +}; diff --git a/test/cli/vcxproj_forced_includes/PropsReleaseX64.h b/test/cli/vcxproj_forced_includes/PropsReleaseX64.h new file mode 100644 index 00000000000..49643c7ea86 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/PropsReleaseX64.h @@ -0,0 +1,6 @@ +class props +{ + props() { + int x = 3 / 0; (void)x; // ERROR + } +}; diff --git a/test/cli/vcxproj_forced_includes/ReleaseX64.cpp b/test/cli/vcxproj_forced_includes/ReleaseX64.cpp new file mode 100644 index 00000000000..8fa6e6d0f82 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/ReleaseX64.cpp @@ -0,0 +1,8 @@ +#include + +int foo() +{ + std::cout << "ReleaseX64\n"; + int x = 3 / 0; (void)x; // ERROR + return 0; +} diff --git a/test/cli/vcxproj_forced_includes/ReleaseX64.h b/test/cli/vcxproj_forced_includes/ReleaseX64.h new file mode 100644 index 00000000000..49f9766f927 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/ReleaseX64.h @@ -0,0 +1,6 @@ +class release +{ + release() { + int x = 3 / 0; (void)x; // ERROR + } +}; \ No newline at end of file diff --git a/test/cli/vcxproj_forced_includes/foo.h b/test/cli/vcxproj_forced_includes/foo.h new file mode 100644 index 00000000000..5d5f8f0c9e7 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/foo.h @@ -0,0 +1 @@ +int foo(); diff --git a/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.props b/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.props new file mode 100644 index 00000000000..22e858590c1 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.props @@ -0,0 +1,8 @@ + + + + PropsDebugX64.h;%(ForcedIncludeFiles) + PropsReleaseX64.h;%(ForcedIncludeFiles) + + + diff --git a/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.slnx b/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.slnx new file mode 100644 index 00000000000..f586cfa3a29 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.slnx @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.vcxproj b/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.vcxproj new file mode 100644 index 00000000000..bd1676af947 --- /dev/null +++ b/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.vcxproj @@ -0,0 +1,103 @@ + + + + + Debug + x64 + + + Release + x64 + + + + 18.0 + Win32Proj + {c9d1dca1-d8ff-4c05-9159-f00816645319} + exclude + 10.0 + + + + StaticLibrary + true + v145 + Unicode + + + Application + false + v145 + true + Unicode + + + + + + + + + + + + + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + $(MSBuildThisFileDirectory)GlobalDebugX64.h;%(ForcedIncludeFiles) + + + Console + true + + + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + $(MSBuildThisFileDirectory)GlobalReleaseX64.h;%(ForcedIncludeFiles) + + + Console + true + + + true + + + + + $(MSBuildThisFileDirectory)AllX64.h;%(ForcedIncludeFiles) + $(MSBuildThisFileDirectory)DebugX64.h;%(ForcedIncludeFiles) + $(MSBuildThisFileDirectory)ReleaseX64.h;%(ForcedIncludeFiles) + true + + + $(MSBuildThisFileDirectory)AllX64.h;%(ForcedIncludeFiles) + $(MSBuildThisFileDirectory)DebugX64.h;%(ForcedIncludeFiles) + $(MSBuildThisFileDirectory)ReleaseX64.h;%(ForcedIncludeFiles) + true + + + + + + + + + \ No newline at end of file diff --git a/test/cli/vcxproj_forced_includes_test.py b/test/cli/vcxproj_forced_includes_test.py new file mode 100644 index 00000000000..e86b254f7da --- /dev/null +++ b/test/cli/vcxproj_forced_includes_test.py @@ -0,0 +1,59 @@ + +# python -m pytest vcxproj_forced_includes_test.py + +import os + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) +__proj_dir = os.path.join(__script_dir, 'vcxproj_forced_includes') + +def get_lines(s): + return sorted(s.split('\n')) + +def test_vcxproj_forced_includes_debug(): + args = [ + '--template=cppcheck1', + '--project=vcxproj_forced_includes/vcxproj_forced_includes.slnx', + '--project-configuration=Debug|x64', + '--no-cppcheck-build-dir' + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + filename1 = os.path.join('vcxproj_forced_includes', 'DebugX64.cpp') + filename2 = os.path.join('vcxproj_forced_includes', 'DebugX64.h') + filename3 = os.path.join('vcxproj_forced_includes', 'AllX64.h') + filename4 = os.path.join('vcxproj_forced_includes', 'GlobalDebugX64.h') + filename5 = os.path.join('vcxproj_forced_includes', 'PropsDebugX64.h') + assert ret == 0, stdout + expected = ( + '[%s:6]: (error) Division by zero.\n' + '[%s:4]: (error) Division by zero.\n' + '[%s:4]: (error) Division by zero.\n' + '[%s:4]: (error) Division by zero.\n' + '[%s:4]: (error) Division by zero.\n' % (filename1, filename2, filename3, filename4, filename5) + ) + assert get_lines(stderr) == get_lines(expected) + + +def test_vcxproj_forced_includes_release(): + args = [ + '--template=cppcheck1', + '--project=vcxproj_forced_includes/vcxproj_forced_includes.slnx', + '--project-configuration=Release|x64', + '--no-cppcheck-build-dir' + ] + ret, stdout, stderr = cppcheck(args, cwd=__script_dir) + filename1 = os.path.join('vcxproj_forced_includes', 'ReleaseX64.cpp') + filename2 = os.path.join('vcxproj_forced_includes', 'ReleaseX64.h') + filename3 = os.path.join('vcxproj_forced_includes', 'AllX64.h') + filename4 = os.path.join('vcxproj_forced_includes', 'GlobalReleaseX64.h') + filename5 = os.path.join('vcxproj_forced_includes', 'PropsReleaseX64.h') + assert ret == 0, stdout + expected = ( + '[%s:6]: (error) Division by zero.\n' + '[%s:4]: (error) Division by zero.\n' + '[%s:4]: (error) Division by zero.\n' + '[%s:4]: (error) Division by zero.\n' + '[%s:4]: (error) Division by zero.\n' % (filename1, filename2, filename3, filename4, filename5) + ) + assert get_lines(stderr) == get_lines(expected) diff --git a/test/cli/vcxproj_unicode_test.py b/test/cli/vcxproj_unicode_test.py new file mode 100644 index 00000000000..7e1dd22954e --- /dev/null +++ b/test/cli/vcxproj_unicode_test.py @@ -0,0 +1,42 @@ + +# python -m pytest vcxproj_unicode_test.py + +from testutils import cppcheck + +import os +import shutil + +__script_dir = os.path.dirname(os.path.abspath(__file__)) +__proj_dir = os.path.join(__script_dir, 'vcxproj-unicode') + +def _get_dump_for_configuration(tmp_path, configuration): + proj_dir = tmp_path / 'vcxproj-unicode' + shutil.copytree(__proj_dir, proj_dir) + + args = [ + '--template=cppcheck1', + '--project=vcxproj-unicode/vcxproj_unicode.vcxproj', + f'--project-configuration={configuration}', + '--no-cppcheck-build-dir', + '--dump' + ] + ret, stdout, stderr = cppcheck(args, cwd=str(tmp_path)) + assert ret == 0, stdout + assert stderr == '', stderr + + dump_path = proj_dir / 'main.cpp.dump' + assert dump_path.exists(), f"Dump file not found at {dump_path}" + + with open(dump_path, 'rt') as f: + return f.read() + +def test_vcxproj_unicode_debug(tmp_path): + dump_content = _get_dump_for_configuration(tmp_path, 'Debug|Win32') + + # the resolved defines are recorded in the attribute + assert 'cfg="_WIN32=1;UNICODE=1;_UNICODE=1;_MSC_VER=1900"' in dump_content + +def test_vcxproj_unicode_release(tmp_path): + dump_content = _get_dump_for_configuration(tmp_path, 'Release|Win32') + + assert 'cfg="_WIN32=1;_MSC_VER=1900;__AFXWIN_H__=1"' in dump_content diff --git a/test/testimportproject.cpp b/test/testimportproject.cpp index 873272030f6..e8840b0d063 100644 --- a/test/testimportproject.cpp +++ b/test/testimportproject.cpp @@ -23,10 +23,8 @@ #include "settings.h" #include "standards.h" #include "suppressions.h" -#include "xml.h" #include -#include #include #include #include @@ -37,8 +35,6 @@ class TestImporter final : public ImportProject { public: using ImportProject::importCompileCommands; using ImportProject::importCppcheckGuiProject; - using ImportProject::importVcxproj; - using ImportProject::SharedItemsProject; using ImportProject::collectArgs; using ImportProject::fsSetDefines; using ImportProject::fsSetIncludePaths; @@ -82,7 +78,6 @@ class TestImportProject : public TestFixture { TEST_CASE(importCppcheckGuiProjectDuplicateSuppressions); TEST_CASE(importCppcheckGuiProjectPremiumMisra); TEST_CASE(ignorePaths); - TEST_CASE(testVcxprojUnicode); TEST_CASE(testCollectArgs1); TEST_CASE(testCollectArgs2); TEST_CASE(testCollectArgs3); @@ -112,7 +107,7 @@ class TestImportProject : public TestFixture { void setIncludePaths1() const { FileSettings fs{"test.cpp", Standards::Language::CPP, 0}; std::list in(1, "../include"); - std::map variables; + VariablesMap variables; TestImporter::fsSetIncludePaths(fs, "abc/def/", in, variables); ASSERT_EQUALS(1U, fs.includePaths.size()); ASSERT_EQUALS("abc/include/", fs.includePaths.front()); @@ -121,7 +116,7 @@ class TestImportProject : public TestFixture { void setIncludePaths2() const { FileSettings fs{"test.cpp", Standards::Language::CPP, 0}; std::list in(1, "$(SolutionDir)other"); - std::map variables; + VariablesMap variables; variables["SolutionDir"] = "c:/abc/"; TestImporter::fsSetIncludePaths(fs, "/home/fred", in, variables); ASSERT_EQUALS(1U, fs.includePaths.size()); @@ -131,7 +126,7 @@ class TestImportProject : public TestFixture { void setIncludePaths3() const { // macro names are case insensitive FileSettings fs{"test.cpp", Standards::Language::CPP, 0}; std::list in(1, "$(SOLUTIONDIR)other"); - std::map variables; + VariablesMap variables; variables["SolutionDir"] = "c:/abc/"; TestImporter::fsSetIncludePaths(fs, "/home/fred", in, variables); ASSERT_EQUALS(1U, fs.includePaths.size()); @@ -595,59 +590,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; @@ -756,8 +698,26 @@ class TestImportProject : public TestFixture { 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'", "", "")); + // 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,7 +726,11 @@ class TestImportProject : public TestFixture { ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("'' == '')", "", ""), std::runtime_error, "unmatched ')' in condition '' == '')"); ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("''", "", ""), std::runtime_error, "Invalid condition: ''''"); ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("'' == '", "", ""), std::runtime_error, "Can not tokenize condition"); - ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Lower())", "", ""), std::runtime_error, "Missing operator"); + // ToUpper / ToLower + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.ToUpper()) == 'DEBUG'", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.ToLower()) == 'debug'", "Debug", "Win32")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("$(Configuration.ToUpper()) == 'debug'", "Debug", "Win32")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("$(Configuration.ToUpper()) == 'RELEASE'", "Debug", "Win32")); // invalid expression in => no error. We are ok with that as long as we don't crash ASSERT(!cppcheck::testing::evaluateVcxprojCondition("' ' && ' '", "", "")); } From 8e11725f4b64d75b71083bd65d32eed56b303dc8 Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Wed, 19 Aug 2026 18:40:32 -0400 Subject: [PATCH 02/14] Force GitHub Action run From ee538013ea876504e1099e3c2fb8d393a1ab18a1 Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Sat, 22 Aug 2026 08:59:31 -0400 Subject: [PATCH 03/14] use Path::getAbsoluteFilePath --- lib/importproject.cpp | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/lib/importproject.cpp b/lib/importproject.cpp index fb6a5621b3b..5afb2793d7f 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -278,13 +278,6 @@ static bool simplifyPathWithVariables(std::string &s, VariablesMap &variables) return true; } -static std::string toAbsolute(const std::string &path) -{ - if (Path::isAbsolute(path)) - return Path::simplifyPath(path); - return Path::simplifyPath(Path::getCurrentPath() + "/" + path); -} - static std::string toAbsolute(const std::string &filename, const std::string &baseDir, VariablesMap &variables) { std::string resolved(filename); @@ -360,7 +353,7 @@ ImportProject::Type ImportProject::import(const std::string &filename, Settings return ImportProject::Type::VS_SLNX; } } else if (endsWith(filename, ".vcxproj")) { - if (importVcxproj(toAbsolute(filename), mVariables, fileFilters)) { + if (importVcxproj(Path::getAbsoluteFilePath(filename), mVariables, fileFilters)) { setRelativePaths(filename); return ImportProject::Type::VS_VCXPROJ; } @@ -496,7 +489,7 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const } } - const std::string solutionDir = toAbsolute(path); + const std::string solutionDir = Path::getAbsoluteFilePath(path); mVariables["SolutionDir"] = solutionDir; bool found = false; @@ -548,7 +541,7 @@ bool ImportProject::importSlnx(const std::string& filename, const std::vector Date: Sat, 22 Aug 2026 09:26:53 -0400 Subject: [PATCH 04/14] Revert "use Path::getAbsoluteFilePath" This reverts commit ee538013ea876504e1099e3c2fb8d393a1ab18a1. --- lib/importproject.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/importproject.cpp b/lib/importproject.cpp index 5afb2793d7f..fb6a5621b3b 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -278,6 +278,13 @@ static bool simplifyPathWithVariables(std::string &s, VariablesMap &variables) return true; } +static std::string toAbsolute(const std::string &path) +{ + if (Path::isAbsolute(path)) + return Path::simplifyPath(path); + return Path::simplifyPath(Path::getCurrentPath() + "/" + path); +} + static std::string toAbsolute(const std::string &filename, const std::string &baseDir, VariablesMap &variables) { std::string resolved(filename); @@ -353,7 +360,7 @@ ImportProject::Type ImportProject::import(const std::string &filename, Settings return ImportProject::Type::VS_SLNX; } } else if (endsWith(filename, ".vcxproj")) { - if (importVcxproj(Path::getAbsoluteFilePath(filename), mVariables, fileFilters)) { + if (importVcxproj(toAbsolute(filename), mVariables, fileFilters)) { setRelativePaths(filename); return ImportProject::Type::VS_VCXPROJ; } @@ -489,7 +496,7 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const } } - const std::string solutionDir = Path::getAbsoluteFilePath(path); + const std::string solutionDir = toAbsolute(path); mVariables["SolutionDir"] = solutionDir; bool found = false; @@ -541,7 +548,7 @@ bool ImportProject::importSlnx(const std::string& filename, const std::vector Date: Sun, 23 Aug 2026 10:38:34 -0400 Subject: [PATCH 05/14] fix a case insensitive bug --- lib/importproject.cpp | 4 ++-- test/testimportproject.cpp | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/importproject.cpp b/lib/importproject.cpp index fb6a5621b3b..8cf3bdee3dc 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -760,9 +760,9 @@ namespace { // Replace "And" and "Or" with "&&" and "||" for (Token *tok = tokenlist.front(); tok; tok = tok->next()) { - if (tok->str() == "And") + if (caseInsensitiveStringCompare(tok->str(), "and") == 0) tok->str("&&"); - else if (tok->str() == "Or") + else if (caseInsensitiveStringCompare(tok->str(), "or") == 0) tok->str("||"); } diff --git a/test/testimportproject.cpp b/test/testimportproject.cpp index e8840b0d063..37fdec624bb 100644 --- a/test/testimportproject.cpp +++ b/test/testimportproject.cpp @@ -694,6 +694,8 @@ class TestImportProject : public TestFixture { ASSERT(!cppcheck::testing::evaluateVcxprojCondition("!('$(Configuration)|$(Platform)' == 'Debug|Win32' )", "Debug", "Win32")); ASSERT(cppcheck::testing::evaluateVcxprojCondition(" '$(Configuration)' == 'Debug' And '$(Platform)' == 'Win32'", "Debug", "Win32")); ASSERT(cppcheck::testing::evaluateVcxprojCondition(" '$(Configuration)' == 'Debug' Or '$(Platform)' == 'Win32'", "Release", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition(" '$(Configuration)' == 'Debug' and '$(Platform)' == 'Win32'", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition(" '$(Configuration)' == 'Debug' or '$(Platform)' == 'Win32'", "Release", "Win32")); ASSERT(cppcheck::testing::evaluateVcxprojCondition(" $(Configuration.StartsWith('Debug'))", "Debug-AddressSanitizer", "Win32")); ASSERT(cppcheck::testing::evaluateVcxprojCondition(" $(Configuration.EndsWith('AddressSanitizer'))", "Debug-AddressSanitizer", "Win32")); ASSERT(cppcheck::testing::evaluateVcxprojCondition(" $(Configuration.Contains('Address'))", "Debug-AddressSanitizer", "Win32")); From 2997d039ad93384f33fb0a250d2347fac19e84a9 Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Thu, 27 Aug 2026 12:31:15 -0400 Subject: [PATCH 06/14] more Visual Studio import refactoring added support for nested folders in .sln files added support for ProjectConfigurations in .props files added support for reading .targets files added more properties with known values added debug logging of unsupported properties and unresolved paths replaced Condition parser to support arbitrary chaining rather than pattern matching refactored similar code for better reuse added more Condition parser tests extended existing test for better coverage of new features --- Makefile | 2 +- lib/importproject.cpp | 1565 +++++++++++++++++++++++++----------- lib/importproject.h | 71 +- oss-fuzz/Makefile | 2 +- test/testimportproject.cpp | 88 +- 5 files changed, 1262 insertions(+), 466 deletions(-) diff --git a/Makefile b/Makefile index f7680be02f7..9b245226036 100644 --- a/Makefile +++ b/Makefile @@ -615,7 +615,7 @@ $(libcppdir)/forwardanalyzer.o: lib/forwardanalyzer.cpp lib/analyzer.h lib/astut $(libcppdir)/fwdanalysis.o: lib/fwdanalysis.cpp lib/astutils.h lib/checkers.h lib/config.h lib/errortypes.h lib/fwdanalysis.h lib/library.h lib/mathlib.h lib/platform.h lib/settings.h lib/smallvector.h lib/sourcelocation.h lib/standards.h lib/symboldatabase.h lib/templatesimplifier.h lib/token.h lib/utils.h lib/vfvalue.h $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/fwdanalysis.cpp -$(libcppdir)/importproject.o: lib/importproject.cpp externals/picojson/picojson.h externals/tinyxml2/tinyxml2.h lib/checkers.h lib/config.h lib/errortypes.h lib/filesettings.h lib/importproject.h lib/json.h lib/library.h lib/mathlib.h lib/path.h lib/pathmatch.h lib/platform.h lib/settings.h lib/smallvector.h lib/standards.h lib/suppressions.h lib/templatesimplifier.h lib/token.h lib/tokenlist.h lib/utils.h lib/vfvalue.h lib/xml.h +$(libcppdir)/importproject.o: lib/importproject.cpp externals/picojson/picojson.h externals/tinyxml2/tinyxml2.h lib/checkers.h lib/config.h lib/filesettings.h lib/importproject.h lib/json.h lib/library.h lib/mathlib.h lib/path.h lib/pathmatch.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/utils.h lib/xml.h $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/importproject.cpp $(libcppdir)/infer.o: lib/infer.cpp lib/calculate.h lib/config.h lib/errortypes.h lib/infer.h lib/mathlib.h lib/smallvector.h lib/templatesimplifier.h lib/token.h lib/utils.h lib/valueptr.h lib/vfvalue.h diff --git a/lib/importproject.cpp b/lib/importproject.cpp index 8cf3bdee3dc..4003c33a79f 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -23,8 +23,6 @@ #include "settings.h" #include "standards.h" #include "suppressions.h" -#include "token.h" -#include "tokenlist.h" #include "utils.h" #include @@ -35,7 +33,9 @@ #include #include #include -#include +#include +#include +#include #include #include #include @@ -247,87 +247,36 @@ void ImportProject::fsSetDefines(FileSettings& fs, std::string defs) fs.defines.swap(defs); } -static bool simplifyPathWithVariables(std::string &s, VariablesMap &variables) +static void expandMSBuildVariables(std::string &s, VariablesMap &variables) { - std::set expanded; - std::string::size_type start = 0; - while ((start = s.find("$(")) != std::string::npos) { - const std::string::size_type end = s.find(')',start); - if (end == std::string::npos) - break; - const std::string var = s.substr(start+2,end-start-2); - if (expanded.find(var) != expanded.end()) - break; - expanded.insert(var); - auto it1 = utils::as_const(variables).find(var); - // variable was not found within defined variables - if (it1 == variables.end()) { - const char *envValue = std::getenv(var.c_str()); - if (!envValue) { - //! \todo generate a debug/info message about undefined variable + // Use multiple passes with a "no change" termination guard. + // A cap of 50 prevents infinite loops from genuinely cyclic variables (A=$(B), B=$(A)). + const int maxPasses = 50; + for (int pass = 0; pass < maxPasses; ++pass) { + bool changed = false; + std::string::size_type pos = 0; + while ((pos = s.find("$(", pos)) != std::string::npos) { + const std::string::size_type end = s.find(')', pos); + if (end == std::string::npos) break; + const std::string var = s.substr(pos + 2, end - pos - 2); + auto it = variables.find(var); + if (it == variables.end()) { + // fall back to environment variable and cache for future passes + const char *envValue = std::getenv(var.c_str()); + if (!envValue) { + pos = end + 1; // unknown — skip and keep going + continue; + } + variables[var] = envValue; + it = variables.find(var); } - variables[var] = std::string(envValue); - it1 = variables.find(var); + s.replace(pos, end - pos + 1, it->second); + pos += it->second.size(); // advance past the replacement + changed = true; } - s.replace(start, end - start + 1, it1->second); - } - if (s.find("$(") != std::string::npos) - return false; - s = Path::simplifyPath(std::move(s)); - return true; -} - -static std::string toAbsolute(const std::string &path) -{ - if (Path::isAbsolute(path)) - return Path::simplifyPath(path); - return Path::simplifyPath(Path::getCurrentPath() + "/" + path); -} - -static std::string toAbsolute(const std::string &filename, const std::string &baseDir, VariablesMap &variables) -{ - std::string resolved(filename); - simplifyPathWithVariables(resolved, variables); - - if (Path::isAbsolute(resolved)) - return Path::simplifyPath(resolved); - return Path::simplifyPath(baseDir + resolved); -} - -void ImportProject::fsSetIncludePaths(FileSettings& fs, const std::string &basepath, const std::list &in, VariablesMap &variables) -{ - std::set found; - // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) - const std::list copyIn(in); - fs.includePaths.clear(); - for (const std::string &ipath : copyIn) { - if (ipath.empty()) - continue; - if (startsWith(ipath,"%(")) - continue; - std::string s(Path::fromNativeSeparators(ipath)); - if (!found.insert(s).second) - continue; - if (s[0] == '/' || (s.size() > 1U && s.compare(1,2,":/") == 0)) { - if (!endsWith(s,'/')) - s += '/'; - fs.includePaths.push_back(std::move(s)); - continue; - } - - if (endsWith(s,'/')) // this is a temporary hack, simplifyPath can crash if path ends with '/' - s.pop_back(); - - if (s.find("$(") == std::string::npos) { - s = Path::simplifyPath(basepath + s); - } else { - if (!simplifyPathWithVariables(s, variables)) - continue; - } - if (s.empty()) - continue; - fs.includePaths.push_back(s.back() == '/' ? s : (s + '/')); + if (!changed) + break; // nothing left to expand — done } } @@ -479,10 +428,24 @@ bool ImportProject::importCompileCommands(std::istream &istr) return true; } +void ImportProject::setSolution(const std::string &filename, VariablesMap &variables) { + const std::string absolutePath = toAbsolute(filename); + variables["SolutionDir"] = Path::getPathFromFilename(absolutePath); + variables["SolutionExt"] = Path::getFilenameExtensionInLowerCase(absolutePath); + variables["SolutionPath"] = absolutePath; + // Path::stripDirectoryPart doesn't work on windows with unix paths + variables["SolutionFileName"] = absolutePath.substr(absolutePath.rfind('/') + 1, absolutePath.size()); + std::string temp = variables["SolutionFileName"]; + findAndReplace(temp, Path::getFilenameExtension(temp), ""); + variables["SolutionName"] = temp; +} + bool ImportProject::importSln(std::istream &istr, const std::string &path, const std::vector &fileFilters) { std::string line; + debugs.clear(); + if (!std::getline(istr,line)) { errors.emplace_back("Visual Studio solution file is empty"); return false; @@ -496,11 +459,22 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const } } - const std::string solutionDir = toAbsolute(path); - mVariables["SolutionDir"] = solutionDir; + VariablesMap solutionVariables; + setSolution(path, solutionVariables); + + solutionVariables["VisualStudioVersion"] = "17.0"; + + const std::string solutionDir = solutionVariables["SolutionDir"]; bool found = false; while (std::getline(istr,line)) { + if (startsWith(line, "VisualStudioVersion = ")) { + const std::string ver = line.substr(std::strlen("VisualStudioVersion = ")); + const std::string::size_type dot = ver.find('.'); + const std::string::size_type dot2 = (dot != std::string::npos) ? ver.find('.', dot + 1) : std::string::npos; + solutionVariables["VisualStudioVersion"] = (dot2 != std::string::npos) ? ver.substr(0, dot2) : ver; + continue; + } if (!startsWith(line,"Project(")) continue; const std::string::size_type pos = line.find(".vcxproj"); @@ -511,8 +485,10 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const continue; std::string vcxproj(line.substr(pos1+1, pos-pos1+7)); vcxproj = Path::toNativeSeparators(std::move(vcxproj)); - vcxproj = toAbsolute(vcxproj, solutionDir, mVariables); + vcxproj = toAbsolute(vcxproj, solutionDir, solutionVariables); vcxproj = Path::fromNativeSeparators(std::move(vcxproj)); + + mVariables = solutionVariables; if (!importVcxproj(vcxproj, mVariables, fileFilters)) { errors.emplace_back("failed to load '" + vcxproj + "' from Visual Studio solution"); return false; @@ -530,6 +506,8 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const bool ImportProject::importSlnx(const std::string& filename, const std::vector& fileFilters) { + debugs.clear(); + tinyxml2::XMLDocument doc; const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); if (error != tinyxml2::XML_SUCCESS) { @@ -548,7 +526,10 @@ bool ImportProject::importSlnx(const std::string& filename, const std::vectorFirstChildElement(); childNode; childNode = childNode->NextSiblingElement()) { - if (std::strcmp(childNode->Name(), "Project") == 0) { - if (!processProject(childNode)) - return false; + // Walk nested Folder/Project nodes recursively + std::function processFolder; + processFolder = [&](const tinyxml2::XMLElement *folder) -> bool { + for (const tinyxml2::XMLElement *child = folder->FirstChildElement(); child; child = child->NextSiblingElement()) { + const char *childName = child->Name(); + if (std::strcmp(childName, "Project") == 0) { + if (!processProject(child)) + return false; + } else if (std::strcmp(childName, "Folder") == 0) { + if (!processFolder(child)) + return false; + } } - } + return true; + }; + if (!processFolder(node)) + return false; } } @@ -597,194 +591,623 @@ bool ImportProject::importSlnx(const std::string& filename, const std::vectorAttribute("Include"); + if (a) + name = a; + for (const tinyxml2::XMLElement *e = cfg->FirstChildElement(); e; e = e->NextSiblingElement()) { + const char * const text = e->GetText(); + if (!text) + continue; + const char * ename = e->Name(); + if (std::strcmp(ename,"Configuration")==0) + configuration = text; + else if (std::strcmp(ename,"Platform")==0) { + platformStr = text; + if (platformStr == "Win32") + platform = Win32; + else if (platformStr == "x64") + platform = x64; + else + platform = Unknown; + } + } +} + +void ImportProject::checkUnexpandedExpressions(const std::string &text, const char *context) +{ + std::string::size_type pos = 0; + while ((pos = text.find("$(", pos)) != std::string::npos) { + const std::string::size_type end = text.find(')', pos + 2); + if (end == std::string::npos) + break; + std::stringstream message; + message << "unexpanded property $(" + << text.substr(pos + 2, end - pos - 2) + << ")" + << (context ? " in " : "") + << (context ? context : "") + << ": " << text << '\n'; + debugs.emplace_back(message.str()); + pos = end + 1; + } + pos = 0; + while ((pos = text.find("%(", pos)) != std::string::npos) { + const std::string::size_type end = text.find(')', pos + 2); + if (end == std::string::npos) + break; + std::stringstream message; + message << "unexpanded metadata %(" + << text.substr(pos + 2, end - pos - 2) + << ")" + << (context ? " in " : "") + << (context ? context : "") + << ": " << text << '\n'; + debugs.emplace_back(message.str()); + pos = end + 1; + } +} + namespace { - struct ProjectConfiguration { - explicit ProjectConfiguration(const tinyxml2::XMLElement *cfg) { - const char *a = cfg->Attribute("Include"); - if (a) - name = a; - for (const tinyxml2::XMLElement *e = cfg->FirstChildElement(); e; e = e->NextSiblingElement()) { - const char * const text = e->GetText(); - if (!text) - continue; - const char * ename = e->Name(); - if (std::strcmp(ename,"Configuration")==0) - configuration = text; - else if (std::strcmp(ename,"Platform")==0) { - platformStr = text; - if (platformStr == "Win32") - platform = Win32; - else if (platformStr == "x64") - platform = x64; - else - platform = Unknown; + // see https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-conditions + class ConditionParser { + public: + ConditionParser(const std::string &condition, const VariablesMap &variables) + : mCondition(condition), mVariables(variables) {} + + bool parse() { + const std::string value = parseOr(); + + skipWhitespace(); + + if (mPos != mCondition.size()) { + if (mCondition[mPos] == ')') + throw std::runtime_error("unmatched ')' in condition " + mCondition); + + throw std::runtime_error("Invalid condition: '" + mCondition + "'"); + } + + if (value != "True" && value != "False") + throw std::runtime_error("Invalid condition: '" + mCondition + "'"); + + return value == "True"; + } + + private: + const std::string &mCondition; + const VariablesMap &mVariables; + std::size_t mPos = 0; + + void skipWhitespace() { + while (mPos < mCondition.size() && std::isspace(static_cast(mCondition[mPos]))) + ++mPos; + } + + bool match(const std::string &text) { + skipWhitespace(); + if (mCondition.compare(mPos, text.size(), text) != 0) + return false; + mPos += text.size(); + return true; + } + + bool matchWord(const std::string &word) { + skipWhitespace(); + if (mCondition.size() - mPos < word.size()) + return false; + if (caseInsensitiveStringCompare(mCondition.substr(mPos, word.size()), word) != 0) + return false; + + const std::size_t end = mPos + word.size(); + if (end < mCondition.size() && + (std::isalnum(static_cast(mCondition[end])) || mCondition[end] == '_')) + return false; + + mPos = end; + return true; + } + + void expect(const std::string &text) { + if (match(text)) + return; + + if (text == ")") + throw std::runtime_error("'(' without closing ')'!"); + + throw std::runtime_error("Expected '" + text + "' in condition '" + mCondition + "'"); + } + + std::string parseOr() { + std::string lhs = parseAnd(); + while (matchWord("or") || match("||")) { + const std::string rhs = parseAnd(); + lhs = rhs == "True" ? "True" : (lhs == "True" ? "True" : "False"); + } + return lhs; + } + + std::string parseAnd() { + std::string lhs = parseUnary(); + while (matchWord("and") || match("&&")) { + const std::string rhs = parseUnary(); + lhs = (lhs == "True" && rhs == "True") ? "True" : "False"; + } + return lhs; + } + + std::string parseUnary() { + if (match("!")) { + skipWhitespace(); + if (mPos == mCondition.size()) + throw std::runtime_error("Invalid condition: '" + mCondition + "'"); + + return parseUnary() == "False" ? "True" : "False"; + } + + return parsePrimary(); + } + + std::string parsePrimary() { + skipWhitespace(); + + if (match("(")) { + std::string value = parseOr(); + expect(")"); + return value; + } + + if (matchWord("Exists")) + return parseExists(); + + if (matchWord("And") || matchWord("Or") || match("!")) + throw std::runtime_error("Invalid condition: '" + mCondition + "'"); + + if (matchWord("HasTrailingSlash")) + return parseHasTrailingSlash(); + + return parseComparison(); + } + + std::string parseComparison() { + const std::string lhs = parseValue(); + skipWhitespace(); + + static constexpr const char *ops[] = { "==", "!=", "<=", ">=", "<", ">" }; + for (const char *op : ops) { + // cppcheck-suppress useStlAlgorithm + if (match(op)) + return compare(lhs, op, parseValue()) ? "True" : "False"; + } + + return lhs; + } + + std::string parseValue() { + skipWhitespace(); + + if (mPos >= mCondition.size()) + throw std::runtime_error("Missing operator"); + + if (matchWord("true")) + return "True"; + + if (matchWord("false")) + return "False"; + + if (mCondition[mPos] == '\'') + return parseString(); + + if (mCondition.compare(mPos, 2, "$(") == 0) + return parsePropertyExpression(); + + if (std::isdigit(static_cast(mCondition[mPos])) || + (mCondition[mPos] == '-' && + mPos + 1 < mCondition.size() && std::isdigit(static_cast(mCondition[mPos + 1])))) { + const std::size_t begin = mPos++; + + while (mPos < mCondition.size() && std::isdigit(static_cast(mCondition[mPos]))) + ++mPos; + + return mCondition.substr(begin, mPos - begin); + } + + const std::size_t begin = mPos; + while (mPos < mCondition.size()) { + const auto c = static_cast(mCondition[mPos]); + if (!std::isalnum(c) && c != '_' && c != '-' && c != '.') + break; + ++mPos; + } + + if (mPos != begin) + return mCondition.substr(begin, mPos - begin); + throw std::runtime_error("Unknown/unhandled operator/operand '" + mCondition.substr(mPos) + "'"); + } + + std::string parseString() { + ++mPos; + std::string value; + + while (mPos < mCondition.size()) { + const char c = mCondition[mPos++]; + + if (c == '\'') + return expandProperties(value); + + value += c; + } + + throw std::runtime_error("Can not tokenize condition"); + } + + static bool parseInteger(const std::string &s, long &value) + { + if (s.empty()) + return false; + + const char *begin = s.c_str(); + char *end = nullptr; + int base = 10; + + if (s.size() > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { + begin += 2; + if (*begin == '\0') + return false; + base = 16; + } + + value = std::strtol(begin, &end, base); + return end != begin && *end == '\0'; + } + + std::string parseIdentifier() { + skipWhitespace(); + const std::size_t begin = mPos; + while (mPos < mCondition.size()) { + const auto c = static_cast(mCondition[mPos]); + if (!std::isalnum(c) && c != '_' && c != '-') + break; + ++mPos; + } + if (begin == mPos) + throw std::runtime_error("Expected identifier in condition '" + mCondition + "'"); + return mCondition.substr(begin, mPos - begin); + } + + std::string parsePropertyExpression() { + expect("$("); + std::string value = getPropertyValue(parseIdentifier()); + + while (true) { + skipWhitespace(); + if (!match(".")) + break; + + const std::string method = parseIdentifier(); + expect("("); + std::vector args; + skipWhitespace(); + if (!match(")")) { + do { + args.push_back(parseValue()); + } while (match(",")); + expect(")"); } + value = applyMethod(value, method, args); } + + expect(")"); + return value; } - std::string name; - std::string configuration; - enum : std::uint8_t { Win32, x64, Unknown } platform = Unknown; - std::string platformStr; - }; - // 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 - std::string execute(const Token* tok, const VariablesMap &variables) { - if (!tok) - throw std::runtime_error("Missing operator"); - auto boolResult = [](bool b) -> std::string { - return b ? "True" : "False"; - }; - if (tok->isUnaryOp("!")) - return boolResult(execute(tok->astOperand1(), variables) == "False"); - if (tok->str() == "==") - return boolResult(execute(tok->astOperand1(), variables) == execute(tok->astOperand2(), variables)); - if (tok->str() == "!=") - return boolResult(execute(tok->astOperand1(), variables) != execute(tok->astOperand2(), variables)); - if (tok->str() == "<" || tok->str() == ">" || tok->str() == "<=" || tok->str() == ">=") { - const std::string lhs = execute(tok->astOperand1(), variables); - const std::string rhs = execute(tok->astOperand2(), variables); - // Try integer comparison first - char* lhsEnd = nullptr; - char* rhsEnd = nullptr; - const long lhsInt = std::strtol(lhs.c_str(), &lhsEnd, 10); - const long rhsInt = std::strtol(rhs.c_str(), &rhsEnd, 10); - if (lhsEnd == lhs.c_str() + lhs.size() && rhsEnd == rhs.c_str() + rhs.size()) { - if (tok->str() == "<") return boolResult(lhsInt < rhsInt); - if (tok->str() == ">") return boolResult(lhsInt > rhsInt); - if (tok->str() == "<=") return boolResult(lhsInt <= rhsInt); - if (tok->str() == ">=") return boolResult(lhsInt >= rhsInt); + std::string parseExists() { + expect("("); + const std::string filename = parseValue(); + expect(")"); + + std::string path = filename; + if (!Path::isAbsolute(path)) { + auto it = mVariables.find("MSBuildThisFileDirectory"); + if (it == mVariables.end()) + it = mVariables.find("ProjectDir"); + if (it != mVariables.end()) + path = it->second + path; } - // Fall back to dotted-version comparison (e.g. "14.0" >= "12.0") - const auto parseVersion = [](const std::string& s) -> std::vector { - std::vector parts; - std::string::size_type pos = 0; - // strip leading v/V - if (!s.empty() && (s[0] == 'v' || s[0] == 'V')) - pos = 1; - while (pos <= s.size()) { - const std::string::size_type dot = s.find('.', pos); - const std::string part = s.substr(pos, dot == std::string::npos ? std::string::npos : dot - pos); - char* end = nullptr; - const long val = std::strtol(part.c_str(), &end, 10); - if (end == part.c_str()) - return {}; // not a valid version part - parts.push_back(static_cast(val)); - if (dot == std::string::npos) - break; - pos = dot + 1; + + 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 { + std::string result = input; + std::size_t pos = 0; + while ((pos = result.find("$(", pos)) != std::string::npos) { + const std::size_t begin = pos + 2; + const std::size_t end = result.find(')', begin); + if (end == std::string::npos) + break; + const std::string name = result.substr(begin, end - begin); + // Only expand simple property references here. Property methods + // are parsed by parsePropertyExpression(). + if (name.find_first_of("()") != std::string::npos) { + pos = end + 1; + continue; } - // pad to 4 parts with zeros, matching System.Version semantics - while (parts.size() < 4) - parts.push_back(0); - return parts; - }; - const std::vector lhsVer = parseVersion(lhs); - const std::vector rhsVer = parseVersion(rhs); - if (!lhsVer.empty() && !rhsVer.empty()) { - if (tok->str() == "<") return boolResult(lhsVer < rhsVer); - if (tok->str() == ">") return boolResult(lhsVer > rhsVer); - if (tok->str() == "<=") return boolResult(lhsVer <= rhsVer); - if (tok->str() == ">=") return boolResult(lhsVer >= rhsVer); + const std::string value = getPropertyValue(name); + result.replace(pos, end - pos + 1, value); + pos += value.size(); } - throw std::runtime_error("Cannot compare '" + lhs + "' and '" + rhs + "'"); + return result; } - if (tok->str() == "&&") - return boolResult(execute(tok->astOperand1(), variables) == "True" && execute(tok->astOperand2(), variables) == "True"); - if (tok->str() == "||") - return boolResult(execute(tok->astOperand1(), variables) == "True" || execute(tok->astOperand2(), variables) == "True"); - if (tok->str() == "(" && Token::Match(tok->previous(), "$ ( %name% . %name% (")) { - const std::string &varName = tok->strAt(1); - auto varIt = variables.find(varName); - std::string property = (varIt != variables.end()) ? varIt->second : varName; - const std::string &method = tok->strAt(3); - if (method == "ToUpper") { - std::string result = property; - std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { + + static std::string applyMethod(std::string value, + const std::string &method, + const std::vector &args) { + if (caseInsensitiveStringCompare(method, "ToUpper") == 0) { + if (!args.empty()) + throw std::runtime_error("ToUpper takes no arguments"); + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { return std::toupper(c); }); - return result; + return value; } - if (method == "ToLower") { - std::string result = property; - std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { + + if (caseInsensitiveStringCompare(method, "ToLower") == 0) { + if (!args.empty()) + throw std::runtime_error("ToLower takes no arguments"); + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { return std::tolower(c); }); - return result; + return value; + } + + if (caseInsensitiveStringCompare(method, "Contains") == 0) { + if (args.size() != 1) + throw std::runtime_error("Contains requires one argument"); + return value.find(args[0]) != std::string::npos ? "True" : "False"; + } + + if (caseInsensitiveStringCompare(method, "StartsWith") == 0) { + if (args.size() != 1) + throw std::runtime_error("StartsWith requires one argument"); + return startsWith(value, args[0]) ? "True" : "False"; + } + + if (caseInsensitiveStringCompare(method, "EndsWith") == 0) { + if (args.size() != 1) + throw std::runtime_error("EndsWith requires one argument"); + return endsWith(value, args[0].c_str(), args[0].size()) ? "True" : "False"; + } + + if (caseInsensitiveStringCompare(method, "Trim") == 0) { + if (args.empty()) { + const std::size_t first = value.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) + return ""; + + const std::size_t last = value.find_last_not_of(" \t\r\n"); + return value.substr(first, last - first + 1); + } + + std::string chars; + for (const std::string &arg : args) + chars += arg; + + const std::size_t first = value.find_first_not_of(chars); + if (first == std::string::npos) + return ""; + + const std::size_t last = value.find_last_not_of(chars); + return value.substr(first, last - first + 1); + } + + if (caseInsensitiveStringCompare(method, "TrimStart") == 0) { + if (args.empty()) { + const std::size_t first = value.find_first_not_of(" \t\r\n"); + return first == std::string::npos ? "" : value.substr(first); + } + + std::string chars; + for (const std::string &arg : args) + chars += arg; + + const std::size_t first = value.find_first_not_of(chars); + return first == std::string::npos ? "" : value.substr(first); + } + + if (caseInsensitiveStringCompare(method, "TrimEnd") == 0) { + if (args.empty()) { + const std::size_t last = value.find_last_not_of(" \t\r\n"); + return last == std::string::npos ? "" : value.substr(0, last + 1); + } + + std::string chars; + for (const std::string &arg : args) + chars += arg; + + const std::size_t last = value.find_last_not_of(chars); + return last == std::string::npos ? "" : value.substr(0, last + 1); + } + + if (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)); } - std::string arg = execute(tok->tokAt(4)->astOperand2(), variables); - if (method == "Contains") - return boolResult(property.find(arg) != std::string::npos); - if (method == "EndsWith") - return boolResult(endsWith(property,arg.c_str(),arg.size())); - if (method == "StartsWith") - return boolResult(startsWith(property,arg)); + + if (caseInsensitiveStringCompare(method, "Replace") == 0) { + if (args.size() != 2) + throw std::runtime_error("Replace requires two arguments"); + + if (args[0].empty()) + throw std::runtime_error("Replace search string cannot be empty"); + + std::size_t pos = 0; + while ((pos = value.find(args[0], pos)) != std::string::npos) { + value.replace(pos, args[0].size(), args[1]); + pos += args[1].size(); + } + return value; + } + throw std::runtime_error("Unhandled method '" + method + "'"); } - if (tok->str().size() >= 2 && tok->str()[0] == '\'') // String Literal - return tok->str().substr(1, tok->str().size() - 2); - if (tok->str() == "(" && tok->previous() && caseInsensitiveStringCompare(tok->strAt(-1), "exists") == 0) { - return boolResult(false); - } - throw std::runtime_error("Unknown/unhandled operator/operand '" + tok->str() + "'"); - } + static int compareVersions(const std::vector &lhs, + const std::vector &rhs) { + const std::size_t count = std::max(lhs.size(), rhs.size()); - bool evalCondition(const std::string &condition, const VariablesMap &variables) { - std::string c = '(' + condition + ")"; - for (const auto &var : variables) - findAndReplace(c, "$(" + var.first + ")", var.second); + for (std::size_t i = 0; i < count; ++i) { + if (i >= lhs.size()) + return -1; - 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 (i >= rhs.size()) + return 1; - // generate links - { - std::stack lpar; - for (Token *tok2 = tokenlist.front(); tok2; tok2 = tok2->next()) { - if (tok2->str() == "(") - lpar.push(tok2); - else if (tok2->str() == ")") { - if (lpar.empty()) - throw std::runtime_error("unmatched ')' in condition " + condition); - Token::createMutualLinks(lpar.top(), tok2); - lpar.pop(); - } + if (lhs[i] < rhs[i]) + return -1; + + if (lhs[i] > rhs[i]) + return 1; } - if (!lpar.empty()) - throw std::runtime_error("'(' without closing ')'!"); + + return 0; } - // Replace "And" and "Or" with "&&" and "||" - for (Token *tok = tokenlist.front(); tok; tok = tok->next()) { - if (caseInsensitiveStringCompare(tok->str(), "and") == 0) - tok->str("&&"); - else if (caseInsensitiveStringCompare(tok->str(), "or") == 0) - tok->str("||"); + static bool compareVersionResult(int result, const std::string &op) { + if (op == "<") + return result < 0; + if (op == ">") + return result > 0; + if (op == "<=") + return result <= 0; + if (op == ">=") + return result >= 0; + return false; } - tokenlist.createAst(); + static bool compare(const std::string &lhs, const std::string &op, const std::string &rhs) { + const auto parseVersion = [](const std::string &s) -> std::vector { + if (s.empty()) + return {}; + + std::size_t pos = (s[0] == 'v' || s[0] == 'V') ? 1 : 0; + if (pos == s.size()) + return {}; + + std::vector parts; + while (pos < s.size()) { + const std::size_t dot = s.find('.', pos); + const std::size_t end = + dot == std::string::npos ? s.size() : dot; - // Locate ast top and execute the condition - for (const Token *tok = tokenlist.front(); tok; tok = tok->next()) { - if (tok->astParent()) { - return execute(tok->astTop(), variables) == "True"; + if (end == pos) + return {}; + + const std::string part = s.substr(pos, end - pos); + char *endPtr = nullptr; + const long value = std::strtol(part.c_str(), &endPtr, 10); + + if (endPtr != part.c_str() && *endPtr == '\0') + parts.push_back(static_cast(value)); + else + return {}; + + if (dot == std::string::npos) + break; + + pos = dot + 1; + } + + if (parts.empty() || parts.size() > 4) + return {}; + + return parts; + }; + + if (op == "==") + return caseInsensitiveStringCompare(lhs, rhs) == 0; + if (op == "!=") + return caseInsensitiveStringCompare(lhs, rhs) != 0; + + if (caseInsensitiveStringCompare(lhs, "Current") == 0) { + const auto rhsVersion = parseVersion(rhs); + if (!rhsVersion.empty()) { + const std::vector currentVersion{ 18 }; + return compareVersionResult(compareVersions(currentVersion, rhsVersion), op); + } + } + + long lhsInt = 0; + long rhsInt = 0; + if (parseInteger(lhs, lhsInt) && parseInteger(rhs, rhsInt)) { + if (op == "<") return lhsInt < rhsInt; + if (op == ">") return lhsInt > rhsInt; + if (op == "<=") return lhsInt <= rhsInt; + if (op == ">=") return lhsInt >= rhsInt; } + + const std::vector lhsVersion = parseVersion(lhs); + const std::vector rhsVersion = parseVersion(rhs); + + if (!lhsVersion.empty() && !rhsVersion.empty()) + return compareVersionResult(compareVersions(lhsVersion, rhsVersion), op); + + throw std::runtime_error("Cannot compare '" + lhs + "' and '" + rhs + "'"); } - throw std::runtime_error("Invalid condition: '" + condition + "'"); + }; + + bool evalCondition(const std::string &condition, const VariablesMap &variables) { + return ConditionParser(condition, variables).parse(); } - bool conditionIsTrue(const tinyxml2::XMLElement *node, const VariablesMap &variables) { + bool conditionIsTrue(const tinyxml2::XMLElement *node, const VariablesMap &variables) { const char *condAttr = node->Attribute("Condition"); if (!condAttr) return true; - std::string condition(condAttr); - for (const auto &var : variables) - findAndReplace(condition, "$(" + var.first + ")", var.second); - return evalCondition(condition, variables); + return evalCondition(condAttr, variables); } bool hasName(const tinyxml2::XMLElement *node, const char *nodeName, const VariablesMap &variables) { @@ -794,6 +1217,14 @@ namespace { return conditionIsTrue(node, variables); } + bool hasNameAndAttribute(const tinyxml2::XMLElement *node, const char *nodeName, const char *attrName, const VariablesMap &variables) { + const char *name = node->Name(); + const char *attr = node->Attribute(attrName); + if (!name || !attr || std::strcmp(nodeName, name) != 0) + return false; + return conditionIsTrue(node, variables); + } + bool hasNameAndLabel(const tinyxml2::XMLElement *node, const char *nodeName, const char *nodeAttr, const VariablesMap &variables) { const char *name = node->Name(); const char *label = node->Attribute("Label"); @@ -812,41 +1243,6 @@ namespace { return conditionIsTrue(node, variables); } - void addProperty(const tinyxml2::XMLElement *node, VariablesMap &variables) { - const char *eName = node->Name(); - const char *eText = node->GetText(); - if (eName && eText && conditionIsTrue(node, variables)) { - std::string original = variables[eName]; - std::string text(eText); - for (const auto &var : variables) - findAndReplace(text, "$(" + var.first + ")", var.second); - findAndReplace(text, "%(" + std::string(eName) + ")", original); - variables[eName] = text; - } - } - - std::string getProperty(const tinyxml2::XMLElement *node, const VariablesMap &variables, const std::string &original) { - std::string text; - const char *eName = node->Name(); - const char *eText = node->GetText(); - if (eName && eText && conditionIsTrue(node, variables)) { - text = eText; - for (const auto &var : variables) - findAndReplace(text, "$(" + var.first + ")", var.second); - findAndReplace(text, "%(" + std::string(eName) + ")", original); - } - return text; - } - - struct ItemGroupClCompile { - explicit ItemGroupClCompile(std::string filename) : filename(std::move(filename)) {} - std::string filename; - std::string additionalIncludeDirectories; - std::string forcedIncludeFiles; - std::string preprocessorDefinitions; - std::string languageStandard; - }; - std::list toStringList(const std::string &s) { std::list ret; @@ -862,18 +1258,281 @@ namespace { ret.push_back(s.substr(pos1)); return ret; } + + std::string findFile(const std::string &startDirectory, const std::string &file) + { + std::string currentDir = startDirectory; + if (currentDir.back() == '/' && currentDir.size() > 1 && currentDir[currentDir.size() - 2] != ':') + currentDir.pop_back(); + + while (!currentDir.empty()) { + std::string targetFile = Path::join(currentDir, file); + if (Path::isFile(targetFile)) + return targetFile; + if (currentDir.back() == '/' || (currentDir.back() == ':' && currentDir.size() == 2)) + break; + size_t lastSlash = currentDir.find_last_of('/'); + if (lastSlash == std::string::npos) + break; + currentDir.resize(lastSlash); + } + + return ""; + } + + struct MSBuildThis { + VariablesMap &variablesMap; + std::string thisFile; + std::string thisFileName; + std::string thisFileExtension; + std::string thisFileDirectory; + std::string thisFileDirectoryNoRoot; + std::string thisFileFullPath; + + MSBuildThis(const std::string &filename, VariablesMap &variables) + : variablesMap(variables) + , thisFile(variables.at("MSBuildThisFile")) + , thisFileName(variables.at("MSBuildThisFileName")) + , thisFileExtension(variables.at("MSBuildThisFileExtension")) + , thisFileDirectory(variables.at("MSBuildThisFileDirectory")) + , thisFileDirectoryNoRoot(variables.at("MSBuildThisFileDirectoryNoRoot")) + , thisFileFullPath(variables.at("MSBuildThisFileFullPath")) { + setMSBuildThis(filename, variables); + } + + static void setMSBuildThis(const std::string &filename, VariablesMap &variables) { + variables["MSBuildThisFileFullPath"] = filename; + std::string temp = filename.substr(filename.rfind('/') + 1, filename.size()); + variables["MSBuildThisFile"] = temp; + findAndReplace(temp, Path::getFilenameExtension(temp), ""); + variables["MSBuildThisFileName"] = temp; + variables["MSBuildThisFileDirectory"] = Path::simplifyPath(Path::getPathFromFilename(filename)); + temp = Path::simplifyPath(Path::getPathFromFilename(filename)); + std::string::size_type pos = filename.find('/', 0); + temp.erase(0, pos + 1); + variables["MSBuildThisFileDirectoryNoRoot"] = temp; + variables["MSBuildThisFileExtension"] = Path::getFilenameExtensionInLowerCase(filename); + } + + ~MSBuildThis() { + variablesMap["MSBuildThisFile"] = thisFile; + variablesMap["MSBuildThisFileName"] = thisFileName; + variablesMap["MSBuildThisFileExtension"] = thisFileExtension; + variablesMap["MSBuildThisFileDirectory"] = thisFileDirectory; + variablesMap["MSBuildThisFileDirectoryNoRoot"] = thisFileDirectoryNoRoot; + variablesMap["MSBuildThisFileFullPath"] = thisFileFullPath; + } + }; + + struct ImportStackGuard { + std::unordered_set &mStack; + std::string mKey; + + ImportStackGuard(std::unordered_set &stack, std::string key) + : mStack(stack), mKey(std::move(key)) {} + + ~ImportStackGuard() { + mStack.erase(mKey); + } + }; +} + +std::string ImportProject::toAbsolute(const std::string &path) +{ + if (Path::isAbsolute(path)) + return Path::simplifyPath(path); + return Path::simplifyPath(Path::getCurrentPath() + "/" + path); +} + +std::string ImportProject::toAbsolute(const std::string &filename, const std::string &baseDir, VariablesMap &variables) +{ + std::string resolved(filename); + if (!simplifyPathWithVariables(resolved, variables)) + return resolved; + + if (Path::isAbsolute(resolved)) + return Path::simplifyPath(resolved); + return Path::simplifyPath(baseDir + resolved); } -enum class PropsImportResult : std::uint8_t { Ok, NotFound, Cycle }; +bool ImportProject::simplifyPathWithVariables(std::string &s, VariablesMap &variables) +{ + expandMSBuildVariables(s, variables); + checkUnexpandedExpressions(s, "path"); + if (s.find("$(") != std::string::npos) + return false; + s = Path::simplifyPath(std::move(s)); + return true; +} -static PropsImportResult loadVisualStudioProperties(const std::string &props, - VariablesMap &variables, - std::unordered_set &importStack) +void ImportProject::fsSetIncludePaths(FileSettings &fs, const std::string &basepath, const std::list &in, VariablesMap &variables) { - std::string filename(props); + std::set found; + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + const std::list copyIn(in); + fs.includePaths.clear(); + for (const std::string &ipath : copyIn) { + if (ipath.empty()) + continue; + if (startsWith(ipath, "%(")) + continue; + std::string s(Path::fromNativeSeparators(ipath)); + if (!found.insert(s).second) + continue; + if (s[0] == '/' || (s.size() > 1U && s.compare(1, 2, ":/") == 0)) { + if (!endsWith(s, '/')) + s += '/'; + fs.includePaths.push_back(std::move(s)); + continue; + } + + if (endsWith(s, '/')) // this is a temporary hack, simplifyPath can crash if path ends with '/' + s.pop_back(); + + if (s.find("$(") == std::string::npos) { + s = Path::simplifyPath(basepath + s); + } else { + if (!simplifyPathWithVariables(s, variables)) + continue; + } + if (s.empty()) + continue; + fs.includePaths.push_back(s.back() == '/' ? s : (s + '/')); + } +} + +void ImportProject::addProperty(const tinyxml2::XMLElement *node, VariablesMap &variables) { + const char *eName = node->Name(); + if (!eName || !conditionIsTrue(node, variables)) + return; + const char *eText = node->GetText(); + std::string text(eText ? eText : ""); + const std::string original = variables[eName]; + findAndReplace(text, "%(" + std::string(eName) + ")", original); + expandMSBuildVariables(text, variables); + variables[eName] = text; + checkUnexpandedExpressions(text, eName); +} + +std::string ImportProject::getProperty(const tinyxml2::XMLElement *node, VariablesMap &variables, const std::string &original) { + const char *eName = node->Name(); + const char *eText = node->GetText(); + if (!eName || !eText || !conditionIsTrue(node, variables)) + return original; + std::string text(eText); + findAndReplace(text, "%(" + std::string(eName) + ")", original); + expandMSBuildVariables(text, variables); + checkUnexpandedExpressions(text, eName); + return text; +} + +const std::string &ImportProject::importResultStr(ImportProject::ImportResult result) { + static std::string ok("ok"); + static std::string notResolvable("Not Resolvable"); + static std::string notFound("Not Found"); + static std::string notValid("Not Valid"); + static std::string cycle("Cycle"); + static std::string unknown("Unknown"); + + switch (result) { + case ImportProject::ImportResult::Ok: + return ok; + case ImportProject::ImportResult::NotResolvable: + return notResolvable; + case ImportProject::ImportResult::NotFound: + return notFound; + case ImportProject::ImportResult::NotValid: + return notValid; + case ImportProject::ImportResult::Cycle: + return cycle; + } + return unknown; +} + +ImportProject::ImportResult ImportProject::importCompile(const tinyxml2::XMLElement *node, + const std::string &projectDir, + VariablesMap &variables, + std::list &compileList) { + const char *include = node->Attribute("Include"); + if (!include) + return ImportResult::NotFound; + + std::string toInclude = toAbsolute(include, projectDir, variables); + if (!Path::acceptFile(toInclude)) + return ImportResult::NotFound; + + ItemGroupClCompile compile(toInclude); + // a file with no override of its own inherits the ItemDefinitionGroup value outright + compile.additionalIncludeDirectories = variables["AdditionalIncludeDirectories"]; + compile.forcedIncludeFiles = variables["ForcedIncludeFiles"]; + compile.preprocessorDefinitions = variables["PreprocessorDefinitions"]; + compile.languageStandard = variables["LanguageStandard"]; + bool excludedFromBuild = false; + + for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { + const char *text = e1->GetText(); + if (!text) + continue; + + if (hasName(e1, "ExcludedFromBuild", variables)) { + if (caseInsensitiveStringCompare(text, "true") == 0) { + excludedFromBuild = true; + break; + } + } else if (hasName(e1, "AdditionalIncludeDirectories", variables)) { + compile.additionalIncludeDirectories = getProperty(e1, variables, compile.additionalIncludeDirectories); + } else if (hasName(e1, "ForcedIncludeFiles", variables)) { + compile.forcedIncludeFiles = getProperty(e1, variables, compile.forcedIncludeFiles); + } else if (hasName(e1, "PreprocessorDefinitions", variables)) { + compile.preprocessorDefinitions = getProperty(e1, variables, compile.preprocessorDefinitions); + } else if (hasName(e1, "LanguageStandard", variables)) { + compile.languageStandard = getProperty(e1, variables, compile.languageStandard); + } + } + + if (!excludedFromBuild) + compileList.emplace_back(compile); + + return ImportResult::Ok; +} + +ImportProject::ImportResult ImportProject::importProject(const tinyxml2::XMLElement *node, + const std::string &projectDir, + VariablesMap &variables, + std::list &projectConfigurationList, + std::unordered_set &importStack) { + const char *projectAttribute = node->Attribute("Project"); + if (!projectAttribute) + return ImportResult::Ok; + std::string file = toAbsolute(projectAttribute, projectDir, variables); + std::string extension = Path::getFilenameExtensionInLowerCase(file); + if (extension == ".props" || extension == ".targets") { + const char *sdk = node->Attribute("Sdk"); + if (sdk) + return ImportResult::NotResolvable; + ImportResult result = importPropsOrTargets(file, variables, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); + return result; + } + if (result == ImportResult::NotResolvable) { + debugs.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); + } + } else { + debugs.emplace_back("Could not import \"" + file + "\" - "); + } + return ImportResult::Ok; +} + +ImportProject::ImportResult ImportProject::importPropsOrTargets(const std::string &file, + VariablesMap &variables, + std::list &projectConfigurationList, + std::unordered_set &importStack) +{ + std::string filename(file); // variables can't be resolved if (!simplifyPathWithVariables(filename, variables)) - return PropsImportResult::NotFound; + return ImportResult::NotResolvable; // prepend project dir (if it exists) to transform relative paths into absolute ones if (!Path::isAbsolute(filename) && variables.count("ProjectDir") > 0) @@ -884,37 +1543,37 @@ static PropsImportResult loadVisualStudioProperties(const std::string &props, // import-cycle detection, which errors out rather than looping forever const std::string simplifiedFilename = Path::simplifyPath(filename); if (!importStack.insert(simplifiedFilename).second) - return PropsImportResult::Cycle; + return ImportResult::Cycle; + + ImportStackGuard guard(importStack, simplifiedFilename); // erases on any exit from here tinyxml2::XMLDocument doc; - if (doc.LoadFile(filename.c_str()) != tinyxml2::XML_SUCCESS) { - importStack.erase(simplifiedFilename); - return PropsImportResult::NotFound; - } + if (doc.LoadFile(filename.c_str()) != tinyxml2::XML_SUCCESS) + return ImportResult::NotFound; + const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement(); - if (rootnode == nullptr) { - importStack.erase(simplifiedFilename); - return PropsImportResult::NotFound; - } + if (rootnode == nullptr) + return ImportResult::NotValid; + + MSBuildThis msBuildThis(filename, variables); + std::string propsDir = Path::getPathFromFilename(filename); - const std::string previousThisFileDir = variables["MSBuildThisFileDirectory"]; - variables["MSBuildThisFileDirectory"] = Path::simplifyPath(Path::getPathFromFilename(filename)); - - PropsImportResult ret = PropsImportResult::Ok; - for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); ret != PropsImportResult::Cycle && node; node = node->NextSiblingElement()) { - if (hasNameAndLabel(node, "ImportGroup", "PropertySheets", variables)) { - for (const tinyxml2::XMLElement *importGroup = node->FirstChildElement(); ret != PropsImportResult::Cycle && importGroup; importGroup = importGroup->NextSiblingElement()) { - if (hasName(importGroup, "Import", variables)) { - 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; + ImportResult ret = ImportResult::Ok; + for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { + if (hasName(node, "ImportGroup", variables)) { + // Accept any (PropertySheets, Shared, unlabeled) — .targets files + // commonly use unlabeled or differently-labeled groups for transitive imports. + const char* label = node->Attribute("Label"); + const bool isPropertySheets = (label == nullptr) || + (std::strcmp(label, "PropertySheets") == 0) || + (std::strcmp(label, "Shared") == 0); + if (isPropertySheets) { + for (const tinyxml2::XMLElement *importGroup = node->FirstChildElement(); importGroup; importGroup = importGroup->NextSiblingElement()) { + if (hasNameAndAttribute(importGroup, "Import", "Project", variables)) { + ImportResult result = importProject(importGroup, propsDir, variables, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) + return result; } - const PropsImportResult nestedRet = loadVisualStudioProperties(loadprj, variables, importStack); - if (nestedRet == PropsImportResult::Cycle) - ret = PropsImportResult::Cycle; } } } else if (hasName(node, "PropertyGroup", variables)) { @@ -926,101 +1585,77 @@ static PropsImportResult loadVisualStudioProperties(const std::string &props, for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) { addProperty(e2, variables); } - } else if (hasName(e1, "Link", variables)) { - for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) { - addProperty(e2, variables); // fixme - } } } + } else if (hasNameAndLabel(node, "ItemGroup", "ProjectConfigurations", variables)) { + for (const tinyxml2::XMLElement *pcNode = node->FirstChildElement("ProjectConfiguration"); pcNode; pcNode = pcNode->NextSiblingElement("ProjectConfiguration")) { + const ProjectConfiguration pc(pcNode); + if (pc.platform != ProjectConfiguration::Unknown) { + projectConfigurationList.emplace_back(pc); + mAllVSConfigs.insert(pc.configuration); + } + } + } else if (hasNameAndAttribute(node, "Import", "Project", variables)) { + ImportResult result = importProject(node, propsDir, variables, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) + return result; } } - variables["MSBuildThisFileDirectory"] = previousThisFileDir; - importStack.erase(simplifiedFilename); return ret; } -// returns false only for a genuine import cycle; a props file that couldn't be found/parsed -// is silently ignored (see PropsImportResult::NotFound above) and this still returns true -static bool loadVisualStudioProperties(const std::string &props, - VariablesMap &variables) +ImportProject::ImportResult ImportProject::importVcxitems(const std::string &items, + VariablesMap &variables, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack) { - std::unordered_set importStack; - return loadVisualStudioProperties(props, variables, importStack) != PropsImportResult::Cycle; -} + std::string filename(items); + // variables can't be resolved + if (!simplifyPathWithVariables(filename, variables)) + return ImportResult::NotResolvable; + + const std::string simplifiedFilename = Path::simplifyPath(filename); + if (!importStack.insert(simplifiedFilename).second) + return ImportResult::Cycle; + + ImportStackGuard guard(importStack, simplifiedFilename); // erases on any exit from here -static bool importVcxitems(const std::string &filename, - VariablesMap &variables, - std::list &compileList) -{ tinyxml2::XMLDocument doc; const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); if (error != tinyxml2::XML_SUCCESS) - return false; + return ImportResult::NotFound; const tinyxml2::XMLElement *const rootnode = doc.FirstChildElement(); if (rootnode == nullptr) - return false; + return ImportResult::NotValid; const std::string itemsDir = Path::simplifyPath(Path::getPathFromFilename(filename)); - const std::string previousThisFileDir = variables["MSBuildThisFileDirectory"]; - variables["MSBuildThisFileDirectory"] = itemsDir; + MSBuildThis msBuildThis(filename, variables); + ImportResult ret = ImportResult::Ok; for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { if (hasName(node, "ItemGroup", variables)) { for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { if (hasName(e, "ClCompile", variables)) { - const char *include = e->Attribute("Include"); - if (!include || !Path::acceptFile(include)) - continue; - std::string toInclude = toAbsolute(include, itemsDir, variables); - - ItemGroupClCompile compile(toInclude); - // a file with no override of its own inherits the ItemDefinitionGroup value outright - compile.additionalIncludeDirectories = variables["AdditionalIncludeDirectories"]; - compile.forcedIncludeFiles = variables["ForcedIncludeFiles"]; - compile.preprocessorDefinitions = variables["PreprocessorDefinitions"]; - compile.languageStandard = variables["LanguageStandard"]; - bool excludedFromBuild = false; - - for (const tinyxml2::XMLElement *e1 = e->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { - const char *text = e1->GetText(); - if (!text) - continue; - - if (hasName(e1, "ExcludedFromBuild", variables)) { - if (std::strcmp(text, "true") == 0) { - excludedFromBuild = true; - break; - } - } else if (hasName(e1, "AdditionalIncludeDirectories", variables)) { - compile.additionalIncludeDirectories = getProperty(e1, variables, compile.additionalIncludeDirectories); - } else if (hasName(e1, "ForcedIncludeFiles", variables)) { - compile.forcedIncludeFiles = getProperty(e1, variables, compile.forcedIncludeFiles); - } else if (hasName(e1, "PreprocessorDefinitions", variables)) { - compile.preprocessorDefinitions = getProperty(e1, variables, compile.preprocessorDefinitions); - } else if (hasName(e1, "LanguageStandard", variables)) { - compile.languageStandard = getProperty(e1, variables, compile.languageStandard); - } - } - - if (!excludedFromBuild) - compileList.emplace_back(std::move(compile)); + importCompile(e, itemsDir, variables, compileList); } } } else if (hasName(node, "ItemDefinitionGroup", variables)) { for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { if (hasName(e1, "ClCompile", variables)) { - for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) { + for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) addProperty(e2, variables); - } } } + } else if (hasNameAndAttribute(node, "Import", "Project", variables)) { + if (importProject(node, itemsDir, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) + break; } } - variables["MSBuildThisFileDirectory"] = previousThisFileDir; - return true; + return ret; } bool ImportProject::importVcxproj(const std::string &filename, @@ -1034,23 +1669,41 @@ bool ImportProject::importVcxproj(const std::string &filename, return false; } - const auto solutionDirIt = variables.find("SolutionDir"); - const bool hasSolutionDir = solutionDirIt != variables.end(); - const std::string solutionDir = hasSolutionDir ? solutionDirIt->second : std::string(); - variables.clear(); - if (hasSolutionDir) - variables["SolutionDir"] = solutionDir; + variables.emplace("VisualStudioVersion", "17.0"); + + variables["ProjectPath"] = filename; + std::string temp = filename.substr(filename.rfind('/') + 1, filename.size()); + variables["ProjectFileName"] = temp; + findAndReplace(temp, Path::getFilenameExtension(temp), ""); + variables["ProjectName"] = temp; + temp.resize(std::min(temp.size(), size_t(16))); + variables["ShortProjectName"] = temp; + variables["ProjectExt"] = Path::getFilenameExtensionInLowerCase(filename); + variables["ProjectDir"] = Path::simplifyPath(Path::getPathFromFilename(filename)); + + // importVcxproj called directly + if (variables.find("SolutionDir") == variables.end()) { + debugs.clear(); + variables["SolutionDir"] = variables["ProjectDir"]; + } - std::string projectDir = Path::simplifyPath(Path::getPathFromFilename(filename)); + variables["MSBuildProjectName"] = variables["ProjectName"]; + variables["MSBuildProjectExtension"] = variables["ProjectExt"]; + variables["MSBuildProjectDirectory"] = variables["ProjectDir"]; + variables["MSBuildProjectFile"] = variables["ProjectFileName"]; + variables["MSBuildProjectFullPath"] = variables["ProjectPath"]; - variables["ProjectDir"] = projectDir; - variables.emplace("SolutionDir", projectDir); - variables["MSBuildThisFileDirectory"] = projectDir; + // common defaults + variables["IntDir"] = "$(Platform)/$(Configuration)/"; + variables["OutDir"] = "$(SolutionDir)$(Platform)/$(Configuration)/"; + variables["GeneratedFilesDir"] = "$(IntDir)Generated Files/"; - VariablesMap originalVariables = variables; + MSBuildThis::setMSBuildThis(filename, variables); + std::string projectDir = variables["ProjectDir"]; std::list projectConfigurationList; std::list compileList; + std::unordered_set importStack; const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement(); if (rootnode == nullptr) { @@ -1058,12 +1711,17 @@ bool ImportProject::importVcxproj(const std::string &filename, return false; } + // Read MSBuildToolsVersion directly from . + // "Current" is the standard value for VS2019+ and is the correct fallback. + const char *toolsVersion = rootnode->Attribute("ToolsVersion"); + variables.emplace("MSBuildToolsVersion", toolsVersion ? toolsVersion : "Current"); + // find all Visual Studio project configurations for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { if (hasNameAndLabel(node, "ItemGroup", "ProjectConfigurations", variables)) { for (const tinyxml2::XMLElement *pcNode = node->FirstChildElement("ProjectConfiguration"); pcNode; pcNode = pcNode->NextSiblingElement("ProjectConfiguration")) { const ProjectConfiguration pc(pcNode); - if (pc.platform != ProjectConfiguration::Unknown) { + if (!pc.configuration.empty()) { // only require a configuration name projectConfigurationList.emplace_back(pc); mAllVSConfigs.insert(pc.configuration); } @@ -1071,6 +1729,47 @@ bool ImportProject::importVcxproj(const std::string &filename, } } + // fixme how to do this right + std::string directoryBuildProps = findFile(projectDir, "Directory.Build.props"); + if (!directoryBuildProps.empty()) { + if (importPropsOrTargets(directoryBuildProps, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) { + errors.emplace_back("Could not load property sheet \"" + directoryBuildProps + "\" - it may be missing, invalid, or part of a circular import"); + } + // fixme is this right + std::string forceImportBeforeCppProps = variables["ForceImportBeforeCppProps"]; + if (!forceImportBeforeCppProps.empty()) { + if (importPropsOrTargets(forceImportBeforeCppProps, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) { + errors.emplace_back("Could not load property sheet \"" + forceImportBeforeCppProps + "\" - it may be missing, invalid, or part of a circular import"); + } + } + +/* fixme are these really all of them ? + std::string forceImportAfterCppDefaultProp = variables["ForceImportAfterCppDefaultProp"]; + if (!forceImportAfterCppDefaultProp.empty()) + importPropsOrTargets(forceImportAfterCppDefaultProp, variables, projectConfigurationList, importStack); + + std::string forceImportAfterCppProps = variables["ForceImportAfterCppProps"]; + if (!forceImportAfterCppProps.empty()) + importPropsOrTargets(forceImportAfterCppProps, variables, projectConfigurationList, importStack); + */ + } + + std::string directoryBuildTargets = findFile(projectDir, "Directory.Build.targets"); + if (!directoryBuildTargets.empty()) { + if (importPropsOrTargets(directoryBuildTargets, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) { + errors.emplace_back("Could not load targets \"" + directoryBuildTargets + "\" - it may be missing, invalid, or part of a circular import"); + } +/* fixme + ForceImportBeforeCppProps + ForceImportAfterCppDefaultProps + ForceImportAfterCppProps + ForceImportBeforeCppTargets + ForceImportAfterCppTargets + */ + } + + VariablesMap originalVariables = variables; + bool first = true; for (const ProjectConfiguration &pc : projectConfigurationList) { @@ -1086,54 +1785,14 @@ bool ImportProject::importVcxproj(const std::string &filename, for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { if (hasNameAndNotLabel(node, "ItemGroup", "ProjectConfigurations", variables)) { for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (hasName(e, "ClCompile", variables)) { - const char *include = e->Attribute("Include"); - if (include && Path::acceptFile(include)) { - std::string toInclude = toAbsolute(include, projectDir, variables); - ItemGroupClCompile compile(toInclude); - // a file with no override of its own inherits the ItemDefinitionGroup value outright - compile.additionalIncludeDirectories = variables["AdditionalIncludeDirectories"]; - compile.forcedIncludeFiles = variables["ForcedIncludeFiles"]; - compile.preprocessorDefinitions = variables["PreprocessorDefinitions"]; - compile.languageStandard = variables["LanguageStandard"]; - bool excludedFromBuild = false; - - for (const tinyxml2::XMLElement *e1 = e->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { - const char *text = e1->GetText(); - if (!text) - continue; - - if (hasName(e1, "ExcludedFromBuild", variables)) { - if (std::strcmp(text, "true") == 0) { - excludedFromBuild = true; - break; - } - } else if (hasName(e1, "AdditionalIncludeDirectories", variables)) { - compile.additionalIncludeDirectories = getProperty(e1, variables, compile.additionalIncludeDirectories); - } else if (hasName(e1, "ForcedIncludeFiles", variables)) { - compile.forcedIncludeFiles = getProperty(e1, variables, compile.forcedIncludeFiles); - } else if (hasName(e1, "PreprocessorDefinitions", variables)) { - compile.preprocessorDefinitions = getProperty(e1, variables, compile.preprocessorDefinitions); - } else if (hasName(e1, "LanguageStandard", variables)) { - compile.languageStandard = getProperty(e1, variables, compile.languageStandard); - } - } - - if (!excludedFromBuild) - compileList.emplace_back(compile); - } - } + if (hasNameAndAttribute(e, "ClCompile", "Include", variables)) + importCompile(e, projectDir, variables, compileList); } } else if (hasName(node, "ItemDefinitionGroup", variables)) { for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { if (hasName(e1, "ClCompile", variables)) { - for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) { + for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) addProperty(e2, variables); - } - } else if (hasName(e1, "Link", variables)) { - for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) { - addProperty(e2, variables); // fixme - } } } } else if (hasName(node, "PropertyGroup", variables)) { @@ -1145,39 +1804,38 @@ bool ImportProject::importVcxproj(const std::string &filename, for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { if (hasName(e, "Import", variables)) { const char *projectAttribute = e->Attribute("Project"); - if (projectAttribute && !loadVisualStudioProperties(projectAttribute, variables)) { - errors.emplace_back("Could not load property sheet \"" + std::string(projectAttribute) + "\" - it may be missing, invalid, or part of a circular import"); + if (!projectAttribute) + continue; + if (importProject(e, projectDir, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) return false; - } } } } else if (labelAttribute && std::strcmp(labelAttribute, "Shared") == 0) { for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { if (hasName(e, "Import", variables)) { const char *projectAttribute = e->Attribute("Project"); - if (projectAttribute) { - // Path to shared items project is relative to current project directory, - // unless the string starts with $(SolutionDir) - std::string pathToSharedItemsFile = projectAttribute; - if (!simplifyPathWithVariables(pathToSharedItemsFile, variables)) { - errors.emplace_back("Could not simplify path to referenced shared items project"); + if (!projectAttribute) + continue; + std::string file = toAbsolute(projectAttribute, projectDir, variables); + std::string extension = Path::getFilenameExtensionInLowerCase(file); + if (extension == ".vcxitems") { + ImportResult result = importVcxitems(file, variables, compileList, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import items \"" + file + "\" - " + importResultStr(result)); return false; } - - if (!importVcxitems(pathToSharedItemsFile, variables, compileList)) { - errors.emplace_back("Could not load shared items project \"" + pathToSharedItemsFile + "\" from original path \"" + std::string(projectAttribute) + "\"."); - return false; + if (result == ImportResult::NotResolvable) { + debugs.emplace_back("Could not import items \"" + file + "\" - " + importResultStr(result)); } + } else { + debugs.emplace_back("Could not import unknown file type \"" + file + "\""); } } } } - } else if (hasName(node, "Import", variables)) { - const char *projectAttribute = node->Attribute("Project"); - if (projectAttribute && !loadVisualStudioProperties(projectAttribute, variables)) { - errors.emplace_back("Could not load property sheet \"" + std::string(projectAttribute) + "\" - it may be missing, invalid, or part of a circular import"); + } else if (hasNameAndAttribute(node, "Import", "Project", variables)) { + if (importProject(node, projectDir, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) return false; - } } } @@ -1212,7 +1870,9 @@ bool ImportProject::importVcxproj(const std::string &filename, Standards::cppstd_t cppstd = Standards::CPPLatest; const std::string &languageStandard = compile.languageStandard; - if (languageStandard == "stdcpp14") + if (languageStandard == "stdcpp11") + cppstd = Standards::CPP11; + else if (languageStandard == "stdcpp14") cppstd = Standards::CPP14; else if (languageStandard == "stdcpp17") cppstd = Standards::CPP17; @@ -1234,21 +1894,24 @@ bool ImportProject::importVcxproj(const std::string &filename, else if (enableEnhancedInstructionSet == "AdvancedVectorExtensions2") fs.defines += ";__AVX2__"; else if (enableEnhancedInstructionSet == "AdvancedVectorExtensions512") - fs.defines += ";__AVX512__"; + fs.defines += ";__AVX512F__"; - auto it = variables.find("CharacterSet"); - bool useUnicode = it != variables.end() && it->second == "Unicode"; + const auto charSetIt = variables.find("CharacterSet"); + const std::string charSet = (charSetIt != variables.end()) ? charSetIt->second : std::string(); const auto useOfMfcIt = variables.find("UseOfMfc"); fs.useMfc = useOfMfcIt != variables.end() && !useOfMfcIt->second.empty() && caseInsensitiveStringCompare(useOfMfcIt->second, "false") != 0; - if (useUnicode) { + if (charSet == "Unicode") { fs.defines += ";UNICODE=1;_UNICODE=1"; + } else if (charSet == "MultiByte") { + fs.defines += ";_MBCS=1"; } std::string defines = fs.defines; - defines += (";" + compile.preprocessorDefinitions); + if (!compile.preprocessorDefinitions.empty()) + defines += (";" + compile.preprocessorDefinitions); fsSetDefines(fs, defines); fsSetIncludePaths(fs, projectDir, toStringList(variables["IncludePath"]), variables); fs.systemIncludePaths = std::move(fs.includePaths); diff --git a/lib/importproject.h b/lib/importproject.h index 0c601c914de..4492eab86ff 100644 --- a/lib/importproject.h +++ b/lib/importproject.h @@ -32,11 +32,16 @@ #include #include #include +#include #include class Settings; struct Suppressions; +namespace tinyxml2 { + class XMLElement; +} + /// @addtogroup Core /// @{ @@ -73,14 +78,22 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { BORLAND, CPPCHECK_GUI }; + enum class ImportResult : std::uint8_t { + Ok, + NotResolvable, + NotFound, + NotValid, + Cycle + }; protected: static void fsSetDefines(FileSettings& fs, std::string defs); - static void fsSetIncludePaths(FileSettings& fs, const std::string &basepath, const std::list &in, VariablesMap &variables); + void fsSetIncludePaths(FileSettings& fs, const std::string &basepath, const std::list &in, VariablesMap &variables); public: std::list fileSettings; std::vector errors; + std::vector debugs; ImportProject() = default; virtual ~ImportProject() = default; @@ -107,6 +120,8 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { Type import(const std::string &filename, Settings *settings=nullptr, Suppressions *supprs=nullptr); + static const std::string &importResultStr(ImportResult result); + protected: bool importCompileCommands(std::istream &istr); bool importCppcheckGuiProject(std::istream &istr, Settings &settings, Suppressions &supprs); @@ -115,14 +130,60 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { VariablesMap mVariables; - bool importVcxproj(const std::string &filename, VariablesMap &variables, const std::vector &fileFilters); private: static void parseArgs(FileSettings &fs, const std::vector &args); + bool importBcb6Prj(const std::string &projectFilename); + + struct ProjectConfiguration { + explicit ProjectConfiguration(const tinyxml2::XMLElement *cfg); + + std::string name; + std::string configuration; + enum : std::uint8_t { Win32, x64, Unknown } platform = Unknown; + std::string platformStr; + }; + + struct ItemGroupClCompile { + explicit ItemGroupClCompile(std::string filename) : filename(std::move(filename)) {} + std::string filename; + std::string additionalIncludeDirectories; + std::string forcedIncludeFiles; + std::string preprocessorDefinitions; + std::string languageStandard; + }; + bool importSln(std::istream &istr, const std::string &path, const std::vector &fileFilters); bool importSlnx(const std::string& filename, const std::vector& fileFilters); - bool importBcb6Prj(const std::string &projectFilename); + bool importVcxproj(const std::string &filename, VariablesMap &variables, const std::vector &fileFilters); + + ImportResult importPropsOrTargets(const std::string &file, + VariablesMap &variables, + std::list &projectConfigurationList, + std::unordered_set &importStack); + ImportResult importVcxitems(const std::string &items, + VariablesMap &variables, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack); + ImportResult importProject(const tinyxml2::XMLElement *node, + const std::string &projectDir, + VariablesMap &variables, + std::list &projectConfigurationList, + std::unordered_set &importStack); + ImportResult importCompile(const tinyxml2::XMLElement *node, + const std::string &projectDir, + VariablesMap &variables, + std::list &compileList); + void checkUnexpandedExpressions(const std::string &text, const char *context); + bool simplifyPathWithVariables(std::string &s, VariablesMap &variables); + void addProperty(const tinyxml2::XMLElement *node, VariablesMap &variables); + std::string getProperty(const tinyxml2::XMLElement *node, VariablesMap &variables, const std::string &original); + std::string toAbsolute(const std::string &filename, const std::string &baseDir, VariablesMap &variables); + static std::string toAbsolute(const std::string &path); + static void setSolution(const std::string &filename, VariablesMap &variables); + std::string mPath; std::set mAllVSConfigs; @@ -195,10 +256,6 @@ namespace CppcheckXml { static constexpr char ProjectNameElementName[] = "project-name"; } -namespace testing -{ - CPPCHECKLIB bool evaluateVcxprojCondition(const std::string& condition, const std::string& configuration, const std::string& platform); -} /// @} //--------------------------------------------------------------------------- #endif // importprojectH diff --git a/oss-fuzz/Makefile b/oss-fuzz/Makefile index e6966747958..7413910834f 100644 --- a/oss-fuzz/Makefile +++ b/oss-fuzz/Makefile @@ -285,7 +285,7 @@ $(libcppdir)/forwardanalyzer.o: ../lib/forwardanalyzer.cpp ../lib/analyzer.h ../ $(libcppdir)/fwdanalysis.o: ../lib/fwdanalysis.cpp ../lib/astutils.h ../lib/checkers.h ../lib/config.h ../lib/errortypes.h ../lib/fwdanalysis.h ../lib/library.h ../lib/mathlib.h ../lib/platform.h ../lib/settings.h ../lib/smallvector.h ../lib/sourcelocation.h ../lib/standards.h ../lib/symboldatabase.h ../lib/templatesimplifier.h ../lib/token.h ../lib/utils.h ../lib/vfvalue.h $(CXX) ${LIB_FUZZING_ENGINE} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/fwdanalysis.cpp -$(libcppdir)/importproject.o: ../lib/importproject.cpp ../externals/picojson/picojson.h ../externals/tinyxml2/tinyxml2.h ../lib/checkers.h ../lib/config.h ../lib/errortypes.h ../lib/filesettings.h ../lib/importproject.h ../lib/json.h ../lib/library.h ../lib/mathlib.h ../lib/path.h ../lib/pathmatch.h ../lib/platform.h ../lib/settings.h ../lib/smallvector.h ../lib/standards.h ../lib/suppressions.h ../lib/templatesimplifier.h ../lib/token.h ../lib/tokenlist.h ../lib/utils.h ../lib/vfvalue.h ../lib/xml.h +$(libcppdir)/importproject.o: ../lib/importproject.cpp ../externals/picojson/picojson.h ../externals/tinyxml2/tinyxml2.h ../lib/checkers.h ../lib/config.h ../lib/filesettings.h ../lib/importproject.h ../lib/json.h ../lib/library.h ../lib/mathlib.h ../lib/path.h ../lib/pathmatch.h ../lib/platform.h ../lib/settings.h ../lib/standards.h ../lib/suppressions.h ../lib/utils.h ../lib/xml.h $(CXX) ${LIB_FUZZING_ENGINE} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/importproject.cpp $(libcppdir)/infer.o: ../lib/infer.cpp ../lib/calculate.h ../lib/config.h ../lib/errortypes.h ../lib/infer.h ../lib/mathlib.h ../lib/smallvector.h ../lib/templatesimplifier.h ../lib/token.h ../lib/utils.h ../lib/valueptr.h ../lib/vfvalue.h diff --git a/test/testimportproject.cpp b/test/testimportproject.cpp index 37fdec624bb..71dabd41029 100644 --- a/test/testimportproject.cpp +++ b/test/testimportproject.cpp @@ -108,7 +108,8 @@ class TestImportProject : public TestFixture { FileSettings fs{"test.cpp", Standards::Language::CPP, 0}; std::list in(1, "../include"); VariablesMap variables; - TestImporter::fsSetIncludePaths(fs, "abc/def/", in, variables); + TestImporter importer; + importer.fsSetIncludePaths(fs, "abc/def/", in, variables); ASSERT_EQUALS(1U, fs.includePaths.size()); ASSERT_EQUALS("abc/include/", fs.includePaths.front()); } @@ -118,7 +119,8 @@ class TestImportProject : public TestFixture { std::list in(1, "$(SolutionDir)other"); VariablesMap variables; variables["SolutionDir"] = "c:/abc/"; - TestImporter::fsSetIncludePaths(fs, "/home/fred", in, variables); + TestImporter importer; + importer.fsSetIncludePaths(fs, "/home/fred", in, variables); ASSERT_EQUALS(1U, fs.includePaths.size()); ASSERT_EQUALS("c:/abc/other/", fs.includePaths.front()); } @@ -128,7 +130,8 @@ class TestImportProject : public TestFixture { std::list in(1, "$(SOLUTIONDIR)other"); VariablesMap variables; variables["SolutionDir"] = "c:/abc/"; - TestImporter::fsSetIncludePaths(fs, "/home/fred", in, variables); + TestImporter importer; + importer.fsSetIncludePaths(fs, "/home/fred", in, variables); ASSERT_EQUALS(1U, fs.includePaths.size()); ASSERT_EQUALS("c:/abc/other/", fs.includePaths.front()); } @@ -694,9 +697,8 @@ class TestImportProject : public TestFixture { ASSERT(!cppcheck::testing::evaluateVcxprojCondition("!('$(Configuration)|$(Platform)' == 'Debug|Win32' )", "Debug", "Win32")); ASSERT(cppcheck::testing::evaluateVcxprojCondition(" '$(Configuration)' == 'Debug' And '$(Platform)' == 'Win32'", "Debug", "Win32")); ASSERT(cppcheck::testing::evaluateVcxprojCondition(" '$(Configuration)' == 'Debug' Or '$(Platform)' == 'Win32'", "Release", "Win32")); - ASSERT(cppcheck::testing::evaluateVcxprojCondition(" '$(Configuration)' == '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")); @@ -718,6 +720,9 @@ class TestImportProject : public TestFixture { ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'13.0' >= '14.0'", "", "")); ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.10.0.0' > '1.9.0.0'", "", "")); ASSERT(cppcheck::testing::evaluateVcxprojCondition("'v14.0' >= '14.0'", "", "")); + // Unknown variable + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'$(DoesNotExist)' == ''", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'$(PATH)' != ''", "", "")); // Relational operators - error case ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("'14.0' >= ''", "", ""), std::runtime_error, "Cannot compare '14.0' and ''"); ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("And", "", ""), std::runtime_error, "Invalid condition: 'And'"); @@ -731,10 +736,81 @@ class TestImportProject : public TestFixture { // 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()) == 'debug'", "Debug", "Win32")); ASSERT(!cppcheck::testing::evaluateVcxprojCondition("$(Configuration.ToUpper()) == 'RELEASE'", "Debug", "Win32")); // invalid expression in => no error. We are ok with that as long as we don't crash ASSERT(!cppcheck::testing::evaluateVcxprojCondition("' ' && ' '", "", "")); + // case insensitive + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'Debug' == 'DEBUG'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'Debug' != 'DEBUG'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(configuration) == 'Debug'", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(configuration) == 'Debug'", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(CONFIGURATION) == 'Debug'", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration) == 'Debug'", "Debug", "Win32")); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition("true", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("TRUE", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("false", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("FALSE", "", "")); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition("true And true", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("true Or false", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("true And false", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("false Or false", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("!false", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("!true", "", "")); + + // HasTrailingSlash + ASSERT(cppcheck::testing::evaluateVcxprojCondition("HasTrailingSlash('foo/')", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("HasTrailingSlash('foo\\')", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("HasTrailingSlash('foo')", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("HasTrailingSlash('')", "", "")); + + // string manipulation + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Trim()) == 'Debug'", " Debug ", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.TrimStart()) == 'Debug '", " Debug ", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.TrimEnd()) == ' Debug'", " Debug ", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Substring(0, 5)) == 'Debug'", "Debug-Test", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Substring(6)) == 'Test'", "Debug-Test", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Replace('-', '_')) == 'Debug_Test'", "Debug-Test", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition( "$(Configuration.Trim().ToUpper()) == 'DEBUG'", " Debug ", "Win32")); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition("true", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("false", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("true And false", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("!false", "", "")); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition( "$(Configuration.Substring(5)) == ''", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition( "$(Configuration.Substring(5, 0)) == ''", "Debug", "Win32")); + ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Substring(-1)) == ''", "Debug", "Win32"), std::runtime_error, "Substring start index out of range"); + ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition( "$(Configuration.Substring(4, 2)) == ''", "Debug", "Win32"), std::runtime_error, "Substring length out of range"); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Trim()) == 'Debug'", " \tDebug\r\n", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.TrimStart()) == 'Debug '", " Debug ", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.TrimEnd()) == ' Debug'", " Debug ", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Trim('-')) == 'Debug'", "--Debug--", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.TrimStart('-')) == 'Debug--'", "--Debug--", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.TrimEnd('-')) == '--Debug'", "--Debug--", "Win32")); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Replace('-', '_')) == 'Debug_Test'","Debug-Test", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Replace('Debug', 'Release')) == 'Release-Test'", "Debug-Test", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Replace('x', 'y')) == 'Debug-Test'", "Debug-Test", "Win32")); + ASSERT_THROW_EQUALS(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Replace('', 'x')) == 'Debug'", "Debug", "Win32"), std::runtime_error, "Replace search string cannot be empty"); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Replace('Debug', 'Release')) == 'Release-Test'", "Debug-Test", "Win32")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("$(Configuration.Replace('debug', 'Release')) == 'Release-Test'", "Debug-Test", "Win32")); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration) == DEBUG", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(configuration) == 'Debug'", "Debug", "Win32")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'0x10' > '0x0F'", "", "")); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'0x0F' < '0x10'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'0x10' < '0x0F'", "", "")); + + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'0x10' > '0x0F'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'0x0F' < '0x10'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'0x10' < '0x0F'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'010' > '9'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'0x10' == '16'", "", "")); } // TODO: test fsParseCommand() From fd8978537f520be5acc3eb01fbeb45d8bc7e415f Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Thu, 27 Aug 2026 13:28:46 -0400 Subject: [PATCH 07/14] fix a importSln bug where filename was assumed but a directory was given --- lib/importproject.cpp | 6 +++--- lib/importproject.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/importproject.cpp b/lib/importproject.cpp index 4003c33a79f..7e88fd59819 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -299,7 +299,7 @@ ImportProject::Type ImportProject::import(const std::string &filename, Settings return ImportProject::Type::COMPILE_DB; } } else if (endsWith(filename, ".sln")) { - if (importSln(fin, mPath, fileFilters)) { + if (importSln(fin, filename, fileFilters)) { setRelativePaths(filename); return ImportProject::Type::VS_SLN; } @@ -440,7 +440,7 @@ void ImportProject::setSolution(const std::string &filename, VariablesMap &varia variables["SolutionName"] = temp; } -bool ImportProject::importSln(std::istream &istr, const std::string &path, const std::vector &fileFilters) +bool ImportProject::importSln(std::istream &istr, const std::string &filename, const std::vector &fileFilters) { std::string line; @@ -460,7 +460,7 @@ bool ImportProject::importSln(std::istream &istr, const std::string &path, const } VariablesMap solutionVariables; - setSolution(path, solutionVariables); + setSolution(filename, solutionVariables); solutionVariables["VisualStudioVersion"] = "17.0"; diff --git a/lib/importproject.h b/lib/importproject.h index 4492eab86ff..f2b0a89a6c5 100644 --- a/lib/importproject.h +++ b/lib/importproject.h @@ -154,7 +154,7 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { std::string languageStandard; }; - bool importSln(std::istream &istr, const std::string &path, const std::vector &fileFilters); + bool importSln(std::istream &istr, const std::string &filename, const std::vector &fileFilters); bool importSlnx(const std::string& filename, const std::vector& fileFilters); bool importVcxproj(const std::string &filename, VariablesMap &variables, const std::vector &fileFilters); From f1c7e24b3ac48a5025e38772f40719feae087992 Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Thu, 27 Aug 2026 16:24:01 -0400 Subject: [PATCH 08/14] add some missing files --- test/cli/props-dirs/Cpp.Build.props | 12 ++++++++++++ test/cli/props-dirs/Cpp.Build.targets | 3 +++ test/cli/props-dirs/Directory.Build.props | 6 ++++++ test/cli/props-dirs/Directory.Build.targets | 2 ++ 4 files changed, 23 insertions(+) create mode 100644 test/cli/props-dirs/Cpp.Build.props create mode 100644 test/cli/props-dirs/Cpp.Build.targets create mode 100644 test/cli/props-dirs/Directory.Build.props create mode 100644 test/cli/props-dirs/Directory.Build.targets 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 @@ + + From f3703541b78cf3d7809251b9cb5e0bcb22f93e06 Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Thu, 27 Aug 2026 17:11:43 -0400 Subject: [PATCH 09/14] fix wrong slnx default version --- lib/importproject.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/importproject.cpp b/lib/importproject.cpp index 7e88fd59819..ddbb76a05a3 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -529,7 +529,7 @@ bool ImportProject::importSlnx(const std::string& filename, const std::vector Date: Sun, 30 Aug 2026 16:01:32 -0400 Subject: [PATCH 10/14] additional refactoring and improvements This is 95% complete and I don't see any major issues. .targets files are now imported improved condition expression parser and now use it for expressions in properties treat properties and metadata separately better ordering of imports and emulation of missing system files added debug message collecting (needs integration with cppcheck debug messages) support for nested and chained properties support for ARM and ARM64 platforms (needs integration with cppcheck) import Directory.Project.props and Directory.Build.props when found --- lib/importproject.cpp | 1867 +++++++++++++++++++++++++++--------- lib/importproject.h | 49 +- lib/platform.cpp | 40 + lib/platform.h | 6 + test/testimportproject.cpp | 137 ++- 5 files changed, 1604 insertions(+), 495 deletions(-) diff --git a/lib/importproject.cpp b/lib/importproject.cpp index ddbb76a05a3..685fab19150 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -27,6 +27,7 @@ #include #include +#include #include #include #include @@ -45,6 +46,7 @@ #include "json.h" + std::string ImportProject::collectArgs(const std::string &cmd, std::vector &args) { args.clear(); @@ -247,37 +249,671 @@ void ImportProject::fsSetDefines(FileSettings& fs, std::string defs) fs.defines.swap(defs); } -static void expandMSBuildVariables(std::string &s, VariablesMap &variables) +// Find the ')' that matches the '(' at position parenPos, handling nested '$(' pairs. +static std::string::size_type findMatchingParen(const std::string &s, std::string::size_type parenPos) { - // Use multiple passes with a "no change" termination guard. - // A cap of 50 prevents infinite loops from genuinely cyclic variables (A=$(B), B=$(A)). - const int maxPasses = 50; - for (int pass = 0; pass < maxPasses; ++pass) { - bool changed = false; - std::string::size_type pos = 0; - while ((pos = s.find("$(", pos)) != std::string::npos) { - const std::string::size_type end = s.find(')', pos); - if (end == std::string::npos) - break; - const std::string var = s.substr(pos + 2, end - pos - 2); - auto it = variables.find(var); - if (it == variables.end()) { - // fall back to environment variable and cache for future passes - const char *envValue = std::getenv(var.c_str()); - if (!envValue) { - pos = end + 1; // unknown — skip and keep going + int depth = 0; + for (std::string::size_type i = parenPos; i < s.size(); ++i) { + if (s.compare(i, 2, "$(") == 0) { + ++depth; + ++i; // skip the '(' on next iteration increment + } else if (s[i] == ')') { + if (depth == 0) + return i; + --depth; + } + } + return std::string::npos; +} + +// 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.begin(), value.end(), value.begin(), [](unsigned char c) { + return std::toupper(c); + }); + return value; + } + + if (caseInsensitiveStringCompare(method, "ToLower") == 0) { + if (!args.empty()) + throw std::runtime_error("ToLower takes no arguments"); + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { + return std::tolower(c); + }); + return value; + } + + if (caseInsensitiveStringCompare(method, "Contains") == 0) { + if (args.size() != 1) + throw std::runtime_error("Contains requires one argument"); + return value.find(args[0]) != std::string::npos ? "True" : "False"; + } + + if (caseInsensitiveStringCompare(method, "StartsWith") == 0) { + if (args.size() != 1) + throw std::runtime_error("StartsWith requires one argument"); + return startsWith(value, args[0]) ? "True" : "False"; + } + + if (caseInsensitiveStringCompare(method, "EndsWith") == 0) { + if (args.size() != 1) + throw std::runtime_error("EndsWith requires one argument"); + return endsWith(value, args[0].c_str(), args[0].size()) ? "True" : "False"; + } + + if (caseInsensitiveStringCompare(method, "Trim") == 0) { + if (args.empty()) { + const std::size_t first = value.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) + return ""; + const std::size_t last = value.find_last_not_of(" \t\r\n"); + return value.substr(first, last - first + 1); + } + std::string chars; + for (const std::string &arg : args) + chars += arg; + const std::size_t first = value.find_first_not_of(chars); + if (first == std::string::npos) + return ""; + const std::size_t last = value.find_last_not_of(chars); + return value.substr(first, last - first + 1); + } + + if (caseInsensitiveStringCompare(method, "TrimStart") == 0) { + if (args.empty()) { + const std::size_t first = value.find_first_not_of(" \t\r\n"); + return first == std::string::npos ? "" : value.substr(first); + } + std::string chars; + for (const std::string &arg : args) + chars += arg; + const std::size_t first = value.find_first_not_of(chars); + return first == std::string::npos ? "" : value.substr(first); + } + + if (caseInsensitiveStringCompare(method, "TrimEnd") == 0) { + if (args.empty()) { + const std::size_t last = value.find_last_not_of(" \t\r\n"); + return last == std::string::npos ? "" : value.substr(0, last + 1); + } + std::string chars; + for (const std::string &arg : args) + chars += arg; + const std::size_t last = value.find_last_not_of(chars); + return last == std::string::npos ? "" : value.substr(0, last + 1); + } + + if (caseInsensitiveStringCompare(method, "Substring") == 0) { + if (args.size() != 1 && args.size() != 2) + throw std::runtime_error("Substring requires one or two arguments"); + char *end = nullptr; + const long start = std::strtol(args[0].c_str(), &end, 10); + if (end == args[0].c_str() || *end != '\0') + throw std::runtime_error("Invalid Substring start index"); + if (start < 0 || static_cast(start) > value.size()) + throw std::runtime_error("Substring start index out of range"); + const auto index = static_cast(start); + if (args.size() == 1) + return value.substr(index); + end = nullptr; + const long length = std::strtol(args[1].c_str(), &end, 10); + if (end == args[1].c_str() || *end != '\0') + throw std::runtime_error("Invalid Substring length"); + if (length < 0 || static_cast(length) > value.size() - index) + throw std::runtime_error("Substring length out of range"); + return value.substr(index, static_cast(length)); + } + + if (caseInsensitiveStringCompare(method, "Replace") == 0) { + if (args.size() != 2) + throw std::runtime_error("Replace requires two arguments"); + if (args[0].empty()) + throw std::runtime_error("Replace search string cannot be empty"); + std::size_t pos = 0; + while ((pos = value.find(args[0], pos)) != std::string::npos) { + value.replace(pos, args[0].size(), args[1]); + pos += args[1].size(); + } + return value; + } + + throw std::runtime_error("Unhandled method '" + method + "'"); +} + +// Evaluate a $([ClassName]::Method(args)) static property function. +// Returns an empty string for unknown or unimplementable functions rather +// than throwing, so import can continue gracefully. +static std::string applyMSBuildStaticFunction(const std::string &className, + const std::string &member, + const std::vector &args) +{ + const auto toInt = [](const std::string &s, long &out) -> bool { + if (s.empty()) return false; + char *end = nullptr; + out = std::strtol(s.c_str(), &end, 10); + return end != s.c_str() && *end == '\0'; + }; + + if (caseInsensitiveStringCompare(className, "MSBuild") == 0) { + + // $([MSBuild]::IsOSPlatform('Windows'|'Linux'|'OSX')) + if (caseInsensitiveStringCompare(member, "IsOSPlatform") == 0 && args.size() == 1) { +#if defined(_WIN32) + const bool onWindows = true, onLinux = false, onOSX = false; +#elif defined(__APPLE__) + const bool onWindows = false, onLinux = false, onOSX = true; +#else + const bool onWindows = false, onLinux = true, onOSX = false; +#endif + if (caseInsensitiveStringCompare(args[0], "Windows") == 0) + return onWindows ? "True" : "False"; + if (caseInsensitiveStringCompare(args[0], "Linux") == 0) + return onLinux ? "True" : "False"; + if (caseInsensitiveStringCompare(args[0], "OSX") == 0 || + caseInsensitiveStringCompare(args[0], "MacOS") == 0) + return onOSX ? "True" : "False"; + return "False"; + } + + // Arithmetic: Add, Subtract, Multiply, Divide, Modulo + if (args.size() == 2) { + long a = 0, b = 0; + if (toInt(args[0], a) && toInt(args[1], b)) { + if (caseInsensitiveStringCompare(member, "Add") == 0) + return std::to_string(a + b); + if (caseInsensitiveStringCompare(member, "Subtract") == 0) + return std::to_string(a - b); + if (caseInsensitiveStringCompare(member, "Multiply") == 0) + return std::to_string(a * b); + if (caseInsensitiveStringCompare(member, "Divide") == 0 && b != 0) + return std::to_string(a / b); + if (caseInsensitiveStringCompare(member, "Modulo") == 0 && b != 0) + return std::to_string(a % b); + } + // $([MSBuild]::ValueOrDefault(value, default)) + if (caseInsensitiveStringCompare(member, "ValueOrDefault") == 0) + return args[0].empty() ? args[1] : args[0]; + // $([MSBuild]::MakeRelative(base, path)) — approximate: return path unchanged + if (caseInsensitiveStringCompare(member, "MakeRelative") == 0) + return args[1]; + } + + // $([MSBuild]::NormalizePath(seg1[, seg2, ...])) — join segments (Path.Combine + // semantics: an absolute segment resets the accumulated path), normalize \ to /, + // and resolve . and .. components. The result is absolute only when the first + // evaluated segment is itself absolute; relative inputs stay relative. + if (caseInsensitiveStringCompare(member, "NormalizePath") == 0 && !args.empty()) { + // Join: an absolute segment resets the accumulated path (Path.Combine semantics). + std::string result = args[0]; + for (std::size_t i = 1; i < args.size(); ++i) { + const std::string &seg = args[i]; + const bool segAbsolute = !seg.empty() && + (seg[0] == '/' || seg[0] == '\\' || + (seg.size() >= 2 && std::isalpha(static_cast(seg[0])) && seg[1] == ':')); + if (segAbsolute) { + result = seg; + } else { + if (!result.empty() && result.back() != '/' && result.back() != '\\') + result += '/'; + result += seg; + } + } + // Unify separators. + // cppcheck-suppress useStlAlgorithm + for (char &c : result) if (c == '\\') c = '/'; + // Extract drive-letter or leading-slash prefix. + std::string prefix; + std::size_t pos = 0; + if (result.size() >= 2 && std::isalpha(static_cast(result[0])) && result[1] == ':') { + prefix = result.substr(0, 2) + '/'; + pos = (result.size() > 2 && result[2] == '/') ? 3 : 2; + } else if (!result.empty() && result[0] == '/') { + prefix = "/"; + pos = 1; + } + // Resolve . and .. components. + std::vector parts; + while (pos < result.size()) { + const std::size_t slash = result.find('/', pos); + const std::string seg = result.substr(pos, slash == std::string::npos ? std::string::npos : slash - pos); + pos = (slash == std::string::npos) ? result.size() : slash + 1; + if (seg.empty() || seg == ".") continue; + if (seg == "..") { + if (!parts.empty()) parts.pop_back(); + } else { + parts.push_back(seg); + } + } + std::string normalized = prefix; + for (std::size_t i = 0; i < parts.size(); ++i) { + if (i > 0) normalized += '/'; + normalized += parts[i]; + } + return normalized; + } + + // $([MSBuild]::NormalizeDirectory(seg1[, seg2, ...])) — same as NormalizePath + // but always returns a path with a trailing slash. + if (caseInsensitiveStringCompare(member, "NormalizeDirectory") == 0 && !args.empty()) { + // Reuse NormalizePath logic via recursive call with renamed member. + const std::string normalized = applyMSBuildStaticFunction(className, "NormalizePath", args); + if (!normalized.empty() && normalized.back() != '/') + return normalized + '/'; + return normalized; + } + + if (args.size() == 1) { + // $([MSBuild]::EnsureTrailingSlash(path)) + if (caseInsensitiveStringCompare(member, "EnsureTrailingSlash") == 0) { + std::string s = args[0]; + if (!s.empty() && s.back() != '/' && s.back() != '\\') + s += '/'; + return s; + } + // $([MSBuild]::GetTargetPlatformVersion(version)) — pass through + if (caseInsensitiveStringCompare(member, "GetTargetPlatformVersion") == 0) + return args[0]; + // filesystem searches — not feasible during import + if (caseInsensitiveStringCompare(member, "GetDirectoryNameOfFileAbove") == 0 || + caseInsensitiveStringCompare(member, "GetPathOfFileAbove") == 0) + return ""; + } + + if (args.empty()) { + if (caseInsensitiveStringCompare(member, "GetCurrentToolsVersion") == 0) + return "Current"; + } + } + + if (caseInsensitiveStringCompare(className, "System.Environment") == 0) { + // $([System.Environment]::GetEnvironmentVariable('NAME')) + if (caseInsensitiveStringCompare(member, "GetEnvironmentVariable") == 0 && args.size() == 1) { + const char *env = std::getenv(args[0].c_str()); + return env ? env : ""; + } + // $([System.Environment]::GetFolderPath(SpecialFolder.X)) + if (caseInsensitiveStringCompare(member, "GetFolderPath") == 0 && args.size() == 1) { + const char *pf = std::getenv("ProgramFiles"); + if ((caseInsensitiveStringCompare(args[0], "ProgramFiles") == 0 || + caseInsensitiveStringCompare(args[0], "ProgramFilesX86") == 0) && pf) + return pf; + return ""; + } + } + + if (caseInsensitiveStringCompare(className, "System.IO.Path") == 0) { + if (args.size() == 1) { + if (caseInsensitiveStringCompare(member, "GetFileName") == 0) { + const auto slash = args[0].find_last_of("/\\"); + return slash != std::string::npos ? args[0].substr(slash + 1) : args[0]; + } + if (caseInsensitiveStringCompare(member, "GetFileNameWithoutExtension") == 0) { + const auto slash = args[0].find_last_of("/\\"); + std::string name = slash != std::string::npos ? args[0].substr(slash + 1) : args[0]; + const auto dot = name.rfind('.'); + return dot != std::string::npos ? name.substr(0, dot) : name; + } + if (caseInsensitiveStringCompare(member, "GetDirectoryName") == 0) { + const auto slash = args[0].find_last_of("/\\"); + return slash != std::string::npos ? args[0].substr(0, slash) : ""; + } + if (caseInsensitiveStringCompare(member, "GetExtension") == 0) { + const auto dot = args[0].rfind('.'); + return dot != std::string::npos ? args[0].substr(dot) : ""; + } + if (caseInsensitiveStringCompare(member, "IsPathRooted") == 0) { + const std::string &p = args[0]; + const bool rooted = !p.empty() && + (p[0] == '/' || p[0] == '\\' || + (p.size() >= 2 && + std::isalpha(static_cast(p[0])) && + p[1] == ':')); + return rooted ? "True" : "False"; + } + } + if (args.size() == 2 && caseInsensitiveStringCompare(member, "Combine") == 0) { + if (Path::isAbsolute(args[1])) + return args[1]; + const std::string sep = + (!args[0].empty() && args[0].back() != '/' && args[0].back() != '\\') ? "/" : ""; + return args[0] + sep + args[1]; + } + } + + if (caseInsensitiveStringCompare(className, "System.String") == 0) { + if (caseInsensitiveStringCompare(member, "IsNullOrEmpty") == 0 && args.size() == 1) + return args[0].empty() ? "True" : "False"; + if (caseInsensitiveStringCompare(member, "IsNullOrWhiteSpace") == 0 && args.size() == 1) { + for (const char c : args[0]) + // cppcheck-suppress useStlAlgorithm + if (!std::isspace(static_cast(c))) return "False"; + return "True"; + } + if (caseInsensitiveStringCompare(member, "Concat") == 0) { + std::string result; + for (const std::string &a : args) result += a; + return result; + } + if (caseInsensitiveStringCompare(member, "Join") == 0 && args.size() >= 2) { + std::string result; + for (std::size_t i = 1; i < args.size(); ++i) { + if (i > 1) result += args[0]; + result += args[i]; + } + return result; + } + // Format — very rough: replace {0},{1},... with positional args + if (caseInsensitiveStringCompare(member, "Format") == 0 && !args.empty()) { + std::string result = args[0]; + for (std::size_t i = 1; i < args.size(); ++i) { + const std::string placeholder = "{" + std::to_string(i - 1) + "}"; + std::size_t pos = 0; + while ((pos = result.find(placeholder, pos)) != std::string::npos) { + result.replace(pos, placeholder.size(), args[i]); + pos += args[i].size(); + } + } + return result; + } + } + + if (caseInsensitiveStringCompare(className, "System.Math") == 0) { + const auto toDouble = [](const std::string &s, double &out) -> bool { + if (s.empty()) return false; + char *end = nullptr; + out = std::strtod(s.c_str(), &end); + return end != s.c_str() && *end == '\0'; + }; + // Format a double as an integer string when the value is whole, + // otherwise use std::to_string (which gives 6 decimal places). + const auto fmtDouble = [](double d) -> std::string { + const auto i = static_cast(d); + if (!(static_cast(i) < d) && !(static_cast(i) > d)) + return std::to_string(i); + return std::to_string(d); + }; + if (args.size() == 1) { + double x = 0; + if (toDouble(args[0], x)) { + if (caseInsensitiveStringCompare(member, "Abs") == 0) + return fmtDouble(x < 0 ? -x : x); + if (caseInsensitiveStringCompare(member, "Floor") == 0) + return std::to_string(static_cast(x >= 0 ? x : x - 1)); + if (caseInsensitiveStringCompare(member, "Ceiling") == 0) + return std::to_string(static_cast(x <= 0 ? x : x + 1)); + if (caseInsensitiveStringCompare(member, "Round") == 0) + return std::to_string(static_cast(x >= 0 ? x + 0.5 : x - 0.5)); + if (caseInsensitiveStringCompare(member, "Sqrt") == 0 && x >= 0) + return fmtDouble(std::sqrt(x)); + if (caseInsensitiveStringCompare(member, "Log") == 0 && x > 0) + return fmtDouble(std::log(x)); + if (caseInsensitiveStringCompare(member, "Log10") == 0 && x > 0) + return fmtDouble(std::log10(x)); + } + } + if (args.size() == 2) { + double a = 0, b = 0; + if (toDouble(args[0], a) && toDouble(args[1], b)) { + if (caseInsensitiveStringCompare(member, "Max") == 0) + return fmtDouble(a > b ? a : b); + if (caseInsensitiveStringCompare(member, "Min") == 0) + return fmtDouble(a < b ? a : b); + if (caseInsensitiveStringCompare(member, "Pow") == 0) + return fmtDouble(std::pow(a, b)); + } + } + } + + // $([MSBuild]::Escape / Unescape) — encode/decode MSBuild special chars as %XX + if (caseInsensitiveStringCompare(className, "MSBuild") == 0 && args.size() == 1) { + if (caseInsensitiveStringCompare(member, "Escape") == 0) { + static const char special[] = "%$@';?*!"; + std::string result; + for (const unsigned char c : args[0]) { + if (std::strchr(special, static_cast(c))) { + const char hex[] = "0123456789ABCDEF"; + result += '%'; + result += hex[(c >> 4) & 0xF]; + result += hex[c & 0xF]; + } else { + result += static_cast(c); + } + } + return result; + } + if (caseInsensitiveStringCompare(member, "Unescape") == 0) { + std::string result; + const std::string &s = args[0]; + for (std::size_t i = 0; i < s.size(); ++i) { + if (s[i] == '%' && i + 2 < s.size() && + std::isxdigit(static_cast(s[i + 1])) && + std::isxdigit(static_cast(s[i + 2]))) { + const auto nibble = [](char c) -> unsigned char { + if (c >= '0' && c <= '9') return static_cast(c - '0'); + if (c >= 'a' && c <= 'f') return static_cast(c - 'a' + 10); + return static_cast(c - 'A' + 10); + }; + result += static_cast((nibble(s[i + 1]) << 4) | nibble(s[i + 2])); + i += 2; + } else { + result += s[i]; } - variables[var] = envValue; - it = variables.find(var); } - s.replace(pos, end - pos + 1, it->second); - pos += it->second.size(); // advance past the replacement - changed = true; + return result; + } + // Bitwise operations + { + long a = 0; + if (toInt(args[0], a)) { + if (caseInsensitiveStringCompare(member, "BitwiseNot") == 0) + return std::to_string(~a); + } + } + } + + if (caseInsensitiveStringCompare(className, "MSBuild") == 0 && args.size() == 2) { + long a = 0, b = 0; + if (toInt(args[0], a) && toInt(args[1], b)) { + if (caseInsensitiveStringCompare(member, "BitwiseAnd") == 0) + return std::to_string(a & b); + if (caseInsensitiveStringCompare(member, "BitwiseOr") == 0) + return std::to_string(a | b); + if (caseInsensitiveStringCompare(member, "BitwiseXor") == 0) + return std::to_string(a ^ b); } - if (!changed) - break; // nothing left to expand — done } + + // $([MSBuild]::GetRegistryValue / GetRegistryValueFromView) + // Returns empty on non-Windows; on Windows would need registry access. + if (caseInsensitiveStringCompare(className, "MSBuild") == 0 && + (caseInsensitiveStringCompare(member, "GetRegistryValue") == 0 || + caseInsensitiveStringCompare(member, "GetRegistryValueFromView") == 0)) + return ""; + + // $([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform(...)) + // Arg is itself a static property like $([...OSPlatform]::Windows) which + // expands to the platform name string via the same mechanism. + if (caseInsensitiveStringCompare(className, "System.Runtime.InteropServices.RuntimeInformation") == 0 && + caseInsensitiveStringCompare(member, "IsOSPlatform") == 0 && args.size() == 1) + return applyMSBuildStaticFunction("MSBuild", "IsOSPlatform", args); + + // $([System.Runtime.InteropServices.OSPlatform]::Windows|Linux|OSX) — static property + if (caseInsensitiveStringCompare(className, "System.Runtime.InteropServices.OSPlatform") == 0) + return member; // return the platform name ("Windows", "Linux", "OSX") as a string + + // Unknown class or method — return empty so import continues + return ""; +} + +// Expands $(Name) and $(Name.Method(args)) references in property value strings. +// Unknown variables are left unexpanded. Use expandPropertyValue() to invoke. +struct PropertyValueExpander { + const PropertiesMap &mVars; + std::string mStr; + std::size_t mPos{0}; + bool mChanged{false}; + bool mReplaceUnknown{false}; // if true, unknown variables expand to "" + + PropertyValueExpander(const PropertiesMap &vars, std::string str) + : mVars(vars), mStr(std::move(str)) {} + + bool isKnown(const std::string &name) const { + if (mVars.count(name)) return true; + return std::getenv(name.c_str()) != nullptr; + } + + std::string lookup(const std::string &name) const { + const auto it = mVars.find(name); + if (it != mVars.end()) + return it->second; + const char *env = std::getenv(name.c_str()); + return env ? env : std::string(); + } + + // Parses an identifier, handling nested $(...) within the name. + std::string parseIdentifier() { + std::string result; + while (mPos < mStr.size()) { + if (mStr.compare(mPos, 2, "$(") == 0) { + result += tryParseExpr(); + continue; + } + const auto c = static_cast(mStr[mPos]); + if (!std::isalnum(c) && c != '_' && c != '-') break; + result += mStr[mPos++]; + } + return result; + } + + // Parses one method argument: a quoted string literal or a $(…) reference. + std::string parseArg() { + while (mPos < mStr.size() && std::isspace(static_cast(mStr[mPos]))) + ++mPos; + if (mPos < mStr.size() && mStr[mPos] == '\'') { + ++mPos; + std::string s; + while (mPos < mStr.size() && mStr[mPos] != '\'') + s += mStr[mPos++]; + if (mPos < mStr.size()) ++mPos; // consume closing '\'' + return s; + } + if (mStr.compare(mPos, 2, "$(") == 0) + return tryParseExpr(); + // Bare word — consume until ',' or ')'. + std::string s; + while (mPos < mStr.size() && mStr[mPos] != ',' && mStr[mPos] != ')') + s += mStr[mPos++]; + return s; + } + + // Parses and evaluates $(Name[.Method(args)…]) starting at mPos. + // Also handles $([ClassName]::Method(args)) static property functions. + // If the variable is unknown the token is left unchanged and mPos advances past it. + std::string tryParseExpr() { + const std::size_t start = mPos; + mPos += 2; // skip "$(" + + // $([ClassName]::Method(args)) — static property function + if (mPos < mStr.size() && mStr[mPos] == '[') { + ++mPos; // skip '[' + std::string className; + while (mPos < mStr.size() && mStr[mPos] != ']') + className += mStr[mPos++]; + if (mPos < mStr.size()) ++mPos; // skip ']' + if (mPos + 1 < mStr.size() && mStr[mPos] == ':' && mStr[mPos + 1] == ':') + mPos += 2; // skip '::' + std::string member; + while (mPos < mStr.size()) { + const auto c = static_cast(mStr[mPos]); + if (!std::isalnum(c) && c != '_') break; + member += mStr[mPos++]; + } + std::vector args; + if (mPos < mStr.size() && mStr[mPos] == '(') { + ++mPos; // skip '(' + while (mPos < mStr.size() && mStr[mPos] != ')') { + args.push_back(parseArg()); + while (mPos < mStr.size() && std::isspace(static_cast(mStr[mPos]))) + ++mPos; + if (mPos < mStr.size() && mStr[mPos] == ',') ++mPos; + } + if (mPos < mStr.size()) ++mPos; // skip inner ')' + } + if (mPos < mStr.size() && mStr[mPos] == ')') ++mPos; // skip outer ')' + mChanged = true; + return applyMSBuildStaticFunction(className, member, args); + } + + const std::string name = parseIdentifier(); + if (name.empty() || !isKnown(name)) { + const std::size_t end = findMatchingParen(mStr, start + 2); + mPos = (end != std::string::npos) ? end + 1 : mStr.size(); + if (mReplaceUnknown) { + mChanged = true; + return std::string(); + } + return mStr.substr(start, mPos - start); + } + mChanged = true; + std::string value = lookup(name); + // Parse optional .Method(args) chain. + while (mPos < mStr.size() && mStr[mPos] == '.') { + ++mPos; + std::string method; + while (mPos < mStr.size()) { + const auto c = static_cast(mStr[mPos]); + if (!std::isalnum(c) && c != '_') break; + method += mStr[mPos++]; + } + if (mPos >= mStr.size() || mStr[mPos] != '(') break; + ++mPos; // skip '(' + std::vector args; + while (mPos < mStr.size() && mStr[mPos] != ')') { + args.push_back(parseArg()); + while (mPos < mStr.size() && std::isspace(static_cast(mStr[mPos]))) + ++mPos; + if (mPos < mStr.size() && mStr[mPos] == ',') ++mPos; + } + if (mPos < mStr.size()) ++mPos; // skip ')' + try { value = applyPropertyMethod(value, method, args); } catch (...) {} + } + if (mPos < mStr.size() && mStr[mPos] == ')') ++mPos; // skip closing ')' + return value; + } + + // Expand all property expressions in mStr, multi-pass (capped at 50). + std::string expand() { + const int maxPasses = 50; + for (int pass = 0; pass < maxPasses; ++pass) { + mChanged = false; + mPos = 0; + std::string result; + result.reserve(mStr.size()); + while (mPos < mStr.size()) { + if (mStr.compare(mPos, 2, "$(") == 0) + result += tryParseExpr(); + else + result += mStr[mPos++]; + } + mStr = std::move(result); + if (!mChanged) break; + } + return mStr; + } +}; + +static void expandMSBuildVariables(std::string &s, PropertiesMap &properties) +{ + PropertyValueExpander expander{properties, s}; + s = expander.expand(); } ImportProject::Type ImportProject::import(const std::string &filename, Settings *settings, Suppressions *supprs) @@ -309,6 +945,7 @@ ImportProject::Type ImportProject::import(const std::string &filename, Settings 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; @@ -417,8 +1054,8 @@ bool ImportProject::importCompileCommands(std::istream &istr) path = Path::simplifyPath(directory + file); FileSettings fs{path, Standards::Language::None, 0}; // file will be identified later on parseArgs(fs, arguments); - VariablesMap variables; - fsSetIncludePaths(fs, directory, fs.includePaths, variables); + PropertiesMap properties; + fsSetIncludePaths(fs, directory, fs.includePaths, properties); // Assign a unique index to each file path. If the file path already exists in the map, // increment the index to handle duplicate file entries. fs.file.setFsFileId(fsFileIds[path]++); @@ -428,20 +1065,64 @@ bool ImportProject::importCompileCommands(std::istream &istr) return true; } -void ImportProject::setSolution(const std::string &filename, VariablesMap &variables) { +void ImportProject::setSolution(const std::string &filename, PropertiesMap &properties) { const std::string absolutePath = toAbsolute(filename); - variables["SolutionDir"] = Path::getPathFromFilename(absolutePath); - variables["SolutionExt"] = Path::getFilenameExtensionInLowerCase(absolutePath); - variables["SolutionPath"] = absolutePath; + properties["SolutionDir"] = Path::getPathFromFilename(absolutePath); + properties["SolutionExt"] = Path::getFilenameExtensionInLowerCase(absolutePath); + properties["SolutionPath"] = absolutePath; + // Path::stripDirectoryPart doesn't work on windows with unix paths - variables["SolutionFileName"] = absolutePath.substr(absolutePath.rfind('/') + 1, absolutePath.size()); - std::string temp = variables["SolutionFileName"]; + // absolutePath is already normalized to '/' by toAbsolute() + const auto slash = absolutePath.rfind('/'); + properties["SolutionFileName"] = (slash != std::string::npos) ? absolutePath.substr(slash + 1) : absolutePath; + + std::string temp = properties["SolutionFileName"]; findAndReplace(temp, Path::getFilenameExtension(temp), ""); - variables["SolutionName"] = temp; + properties["SolutionName"] = temp; +} + +static std::string findFile(const std::string &startDirectory, const std::string &file) +{ + // startDirectory comes from MSBuildThisFileDirectory which is already + // normalized to '/' separators by Path::simplifyPath. + std::string currentDir = startDirectory; + if (currentDir.back() == '/' && currentDir.size() > 1 && currentDir[currentDir.size() - 2] != ':') + currentDir.pop_back(); + + while (!currentDir.empty()) { + std::string targetFile = Path::join(currentDir, file); + if (Path::isFile(targetFile)) + return targetFile; + if (currentDir.back() == '/' || (currentDir.back() == ':' && currentDir.size() == 2)) + break; + size_t lastSlash = currentDir.rfind('/'); + if (lastSlash == std::string::npos) + break; + currentDir.resize(lastSlash); + } + + return ""; +} + +bool ImportProject::importDirectorySolutionProps(PropertiesMap &properties) +{ + const std::string directorySolutionProps = findFile(properties["ProjectDir"], "Directory.Solution.props"); + if (!directorySolutionProps.empty()) { + MetadataMap data; + std::unordered_set stack; + std::list projectConfigurationList; + const ImportResult result = importPropsOrTargets(directorySolutionProps, properties, data, projectConfigurationList, stack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + directorySolutionProps + "\" - " + importResultStr(result)); + return false; + } + } + return true; } bool ImportProject::importSln(std::istream &istr, const std::string &filename, const std::vector &fileFilters) { + PropertiesMap mVariables; std::string line; debugs.clear(); @@ -459,7 +1140,7 @@ bool ImportProject::importSln(std::istream &istr, const std::string &filename, c } } - VariablesMap solutionVariables; + PropertiesMap solutionVariables; setSolution(filename, solutionVariables); solutionVariables["VisualStudioVersion"] = "17.0"; @@ -475,6 +1156,13 @@ bool ImportProject::importSln(std::istream &istr, const std::string &filename, c solutionVariables["VisualStudioVersion"] = (dot2 != std::string::npos) ? ver.substr(0, dot2) : ver; continue; } + if (startsWith(line, "MinimumVisualStudioVersion = ")) { + const std::string ver = line.substr(std::strlen("MinimumVisualStudioVersion = ")); + const std::string::size_type dot = ver.find('.'); + const std::string::size_type dot2 = (dot != std::string::npos) ? ver.find('.', dot + 1) : std::string::npos; + solutionVariables["MinimumVisualStudioVersion"] = (dot2 != std::string::npos) ? ver.substr(0, dot2) : ver; + continue; + } if (!startsWith(line,"Project(")) continue; const std::string::size_type pos = line.find(".vcxproj"); @@ -501,11 +1189,12 @@ bool ImportProject::importSln(std::istream &istr, const std::string &filename, c return false; } - return true; + return importDirectorySolutionProps(mVariables); } bool ImportProject::importSlnx(const std::string& filename, const std::vector& fileFilters) { + PropertiesMap mVariables; debugs.clear(); tinyxml2::XMLDocument doc; @@ -526,14 +1215,14 @@ bool ImportProject::importSlnx(const std::string& filename, const std::vector bool { const char* pathAttribute = projectNode->Attribute("Path"); if (pathAttribute == nullptr) return true; @@ -588,7 +1277,7 @@ bool ImportProject::importSlnx(const std::string& filename, const std::vector(mCondition[mPos]))) @@ -719,18 +1419,26 @@ namespace { std::string parseOr() { std::string lhs = parseAnd(); - while (matchWord("or") || match("||")) { + while (matchWord("or")) { + const bool savedEvaluate = mEvaluate; + if (lhs == "True") mEvaluate = false; const std::string rhs = parseAnd(); - lhs = rhs == "True" ? "True" : (lhs == "True" ? "True" : "False"); + mEvaluate = savedEvaluate; + if (lhs != "True") + lhs = (rhs == "True") ? "True" : "False"; } return lhs; } std::string parseAnd() { std::string lhs = parseUnary(); - while (matchWord("and") || match("&&")) { + while (matchWord("and")) { + const bool savedEvaluate = mEvaluate; + if (lhs == "False") mEvaluate = false; const std::string rhs = parseUnary(); - lhs = (lhs == "True" && rhs == "True") ? "True" : "False"; + mEvaluate = savedEvaluate; + if (lhs != "False") + lhs = (rhs == "True") ? "True" : "False"; } return lhs; } @@ -774,9 +1482,11 @@ namespace { static constexpr const char *ops[] = { "==", "!=", "<=", ">=", "<", ">" }; for (const char *op : ops) { - // cppcheck-suppress useStlAlgorithm - if (match(op)) - return compare(lhs, op, parseValue()) ? "True" : "False"; + if (match(op)) { + const std::string rhs = parseValue(); + if (!mEvaluate) return "False"; + return compare(lhs, op, rhs) ? "True" : "False"; + } } return lhs; @@ -821,6 +1531,8 @@ namespace { 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) + "'"); } @@ -837,6 +1549,8 @@ namespace { value += c; } + if (!mEvaluate) + return std::string(); throw std::runtime_error("Can not tokenize condition"); } @@ -862,20 +1576,56 @@ namespace { std::string parseIdentifier() { skipWhitespace(); - const std::size_t begin = mPos; + 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; - ++mPos; + result += mCondition[mPos++]; } - if (begin == mPos) + if (result.empty()) throw std::runtime_error("Expected identifier in condition '" + mCondition + "'"); - return mCondition.substr(begin, mPos - begin); + return result; } std::string parsePropertyExpression() { expect("$("); + + // $([ClassName]::Method(args)) — static property function + if (mPos < mCondition.size() && mCondition[mPos] == '[') { + ++mPos; // skip '[' + std::string className; + while (mPos < mCondition.size() && mCondition[mPos] != ']') + className += mCondition[mPos++]; + if (mPos < mCondition.size()) ++mPos; // skip ']' + if (mPos + 1 < mCondition.size() && mCondition[mPos] == ':' && mCondition[mPos + 1] == ':') + mPos += 2; // skip '::' + std::string member; + while (mPos < mCondition.size()) { + const auto c = static_cast(mCondition[mPos]); + if (!std::isalnum(c) && c != '_') break; + member += mCondition[mPos++]; + } + std::vector args; + if (mPos < mCondition.size() && mCondition[mPos] == '(') { + ++mPos; // skip '(' + skipWhitespace(); + if (!match(")")) { + do { + args.push_back(parseValue()); + skipWhitespace(); + } while (match(",")); + expect(")"); + } + } + expect(")"); // outer closing paren of $(...) + return mEvaluate ? applyMSBuildStaticFunction(className, member, args) : std::string(); + } + std::string value = getPropertyValue(parseIdentifier()); while (true) { @@ -893,7 +1643,7 @@ namespace { } while (match(",")); expect(")"); } - value = applyMethod(value, method, args); + value = mEvaluate ? applyMethod(value, method, args) : std::string(); } expect(")"); @@ -937,162 +1687,17 @@ namespace { } std::string expandProperties(const std::string &input) const { - std::string result = input; - std::size_t pos = 0; - while ((pos = result.find("$(", pos)) != std::string::npos) { - const std::size_t begin = pos + 2; - const std::size_t end = result.find(')', begin); - if (end == std::string::npos) - break; - const std::string name = result.substr(begin, end - begin); - // Only expand simple property references here. Property methods - // are parsed by parsePropertyExpression(). - if (name.find_first_of("()") != std::string::npos) { - pos = end + 1; - continue; - } - const std::string value = getPropertyValue(name); - result.replace(pos, end - pos + 1, value); - pos += value.size(); - } - return result; + // Delegate to PropertyValueExpander. In condition context unknown + // variables must expand to "" (MSBuild semantics for quoted strings). + PropertyValueExpander expander{mVariables, input}; + expander.mReplaceUnknown = true; + return expander.expand(); } static std::string applyMethod(std::string value, const std::string &method, const std::vector &args) { - if (caseInsensitiveStringCompare(method, "ToUpper") == 0) { - if (!args.empty()) - throw std::runtime_error("ToUpper takes no arguments"); - std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { - return std::toupper(c); - }); - return value; - } - - if (caseInsensitiveStringCompare(method, "ToLower") == 0) { - if (!args.empty()) - throw std::runtime_error("ToLower takes no arguments"); - std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { - return std::tolower(c); - }); - return value; - } - - if (caseInsensitiveStringCompare(method, "Contains") == 0) { - if (args.size() != 1) - throw std::runtime_error("Contains requires one argument"); - return value.find(args[0]) != std::string::npos ? "True" : "False"; - } - - if (caseInsensitiveStringCompare(method, "StartsWith") == 0) { - if (args.size() != 1) - throw std::runtime_error("StartsWith requires one argument"); - return startsWith(value, args[0]) ? "True" : "False"; - } - - if (caseInsensitiveStringCompare(method, "EndsWith") == 0) { - if (args.size() != 1) - throw std::runtime_error("EndsWith requires one argument"); - return endsWith(value, args[0].c_str(), args[0].size()) ? "True" : "False"; - } - - if (caseInsensitiveStringCompare(method, "Trim") == 0) { - if (args.empty()) { - const std::size_t first = value.find_first_not_of(" \t\r\n"); - if (first == std::string::npos) - return ""; - - const std::size_t last = value.find_last_not_of(" \t\r\n"); - return value.substr(first, last - first + 1); - } - - std::string chars; - for (const std::string &arg : args) - chars += arg; - - const std::size_t first = value.find_first_not_of(chars); - if (first == std::string::npos) - return ""; - - const std::size_t last = value.find_last_not_of(chars); - return value.substr(first, last - first + 1); - } - - if (caseInsensitiveStringCompare(method, "TrimStart") == 0) { - if (args.empty()) { - const std::size_t first = value.find_first_not_of(" \t\r\n"); - return first == std::string::npos ? "" : value.substr(first); - } - - std::string chars; - for (const std::string &arg : args) - chars += arg; - - const std::size_t first = value.find_first_not_of(chars); - return first == std::string::npos ? "" : value.substr(first); - } - - if (caseInsensitiveStringCompare(method, "TrimEnd") == 0) { - if (args.empty()) { - const std::size_t last = value.find_last_not_of(" \t\r\n"); - return last == std::string::npos ? "" : value.substr(0, last + 1); - } - - std::string chars; - for (const std::string &arg : args) - chars += arg; - - const std::size_t last = value.find_last_not_of(chars); - return last == std::string::npos ? "" : value.substr(0, last + 1); - } - - if (caseInsensitiveStringCompare(method, "Substring") == 0) { - if (args.size() != 1 && args.size() != 2) - throw std::runtime_error("Substring requires one or two arguments"); - - char *end = nullptr; - const long start = std::strtol(args[0].c_str(), &end, 10); - if (end == args[0].c_str() || *end != '\0') - throw std::runtime_error("Invalid Substring start index"); - - if (start < 0 || - static_cast(start) > value.size()) - throw std::runtime_error("Substring start index out of range"); - - const auto index = static_cast(start); - - if (args.size() == 1) - return value.substr(index); - - end = nullptr; - const long length = std::strtol(args[1].c_str(), &end, 10); - if (end == args[1].c_str() || *end != '\0') - throw std::runtime_error("Invalid Substring length"); - - if (length < 0 || - static_cast(length) > value.size() - index) - throw std::runtime_error("Substring length out of range"); - - return value.substr(index, static_cast(length)); - } - - if (caseInsensitiveStringCompare(method, "Replace") == 0) { - if (args.size() != 2) - throw std::runtime_error("Replace requires two arguments"); - - if (args[0].empty()) - throw std::runtime_error("Replace search string cannot be empty"); - - std::size_t pos = 0; - while ((pos = value.find(args[0], pos)) != std::string::npos) { - value.replace(pos, args[0].size(), args[1]); - pos += args[1].size(); - } - return value; - } - - throw std::runtime_error("Unhandled method '" + method + "'"); + return applyPropertyMethod(std::move(value), method, args); } static int compareVersions(const std::vector &lhs, @@ -1100,16 +1705,13 @@ namespace { const std::size_t count = std::max(lhs.size(), rhs.size()); for (std::size_t i = 0; i < count; ++i) { - if (i >= lhs.size()) - return -1; - - if (i >= rhs.size()) - return 1; - - if (lhs[i] < rhs[i]) + // Missing trailing components are treated as 0, + // so {17} == {17, 0, 0} and {17, 1} > {17, 0, 5} is correct. + const int l = (i < lhs.size()) ? lhs[i] : 0; + const int r = (i < rhs.size()) ? rhs[i] : 0; + if (l < r) return -1; - - if (lhs[i] > rhs[i]) + if (l > r) return 1; } @@ -1161,16 +1763,25 @@ namespace { pos = dot + 1; } - if (parts.empty() || parts.size() > 4) + if (parts.empty()) return {}; return parts; }; - if (op == "==") - return caseInsensitiveStringCompare(lhs, rhs) == 0; - if (op == "!=") - return caseInsensitiveStringCompare(lhs, rhs) != 0; + if (op == "==" || op == "!=") { + const bool strEqual = caseInsensitiveStringCompare(lhs, rhs) == 0; + if (!strEqual) { + // "17" and "17.0.0.0" represent the same version; try version comparison + const auto lv = parseVersion(lhs); + const auto rv = parseVersion(rhs); + if (!lv.empty() && !rv.empty()) { + const bool verEqual = compareVersions(lv, rv) == 0; + return (op == "==") ? verEqual : !verEqual; + } + } + return (op == "==") ? strEqual : !strEqual; + } if (caseInsensitiveStringCompare(lhs, "Current") == 0) { const auto rhsVersion = parseVersion(rhs); @@ -1180,6 +1791,14 @@ namespace { } } + if (caseInsensitiveStringCompare(rhs, "Current") == 0) { + const auto lhsVersion = parseVersion(lhs); + if (!lhsVersion.empty()) { + const std::vector currentVersion{ 18 }; + return compareVersionResult(compareVersions(lhsVersion, currentVersion), op); + } + } + long lhsInt = 0; long rhsInt = 0; if (parseInteger(lhs, lhsInt) && parseInteger(rhs, rhsInt)) { @@ -1199,48 +1818,54 @@ namespace { } }; - bool evalCondition(const std::string &condition, const VariablesMap &variables) { - return ConditionParser(condition, variables).parse(); + bool evalCondition(const std::string &condition, const PropertiesMap &properties) { + try { + return ConditionParser(condition, properties).parse(); + } catch (const std::exception &) { + // malformed or unhandled condition syntax (e.g. property functions, + // unknown methods, bare .Property access) — treat as false so import continues + return false; + } } - bool conditionIsTrue(const tinyxml2::XMLElement *node, const VariablesMap &variables) { + bool conditionIsTrue(const tinyxml2::XMLElement *node, const PropertiesMap &properties) { const char *condAttr = node->Attribute("Condition"); if (!condAttr) return true; - return evalCondition(condAttr, variables); + return evalCondition(condAttr, properties); } - bool hasName(const tinyxml2::XMLElement *node, const char *nodeName, const VariablesMap &variables) { + bool hasName(const tinyxml2::XMLElement *node, const char *nodeName, const PropertiesMap &properties) { const char *name = node->Name(); if (!name || std::strcmp(nodeName, name) != 0) return false; - return conditionIsTrue(node, variables); + return conditionIsTrue(node, properties); } - bool hasNameAndAttribute(const tinyxml2::XMLElement *node, const char *nodeName, const char *attrName, const VariablesMap &variables) { + bool hasNameAndAttribute(const tinyxml2::XMLElement *node, const char *nodeName, const char *attrName, const PropertiesMap &properties) { const char *name = node->Name(); const char *attr = node->Attribute(attrName); if (!name || !attr || std::strcmp(nodeName, name) != 0) return false; - return conditionIsTrue(node, variables); + return conditionIsTrue(node, properties); } - bool hasNameAndLabel(const tinyxml2::XMLElement *node, const char *nodeName, const char *nodeAttr, const VariablesMap &variables) { + bool hasNameAndLabel(const tinyxml2::XMLElement *node, const char *nodeName, const char *nodeAttr, const PropertiesMap &properties) { const char *name = node->Name(); const char *label = node->Attribute("Label"); if (!name || !label || std::strcmp(nodeName, name) != 0 || std::strcmp(label, nodeAttr) != 0) return false; - return conditionIsTrue(node, variables); + return conditionIsTrue(node, properties); } - bool hasNameAndNotLabel(const tinyxml2::XMLElement *node, const char *nodeName, const char *nodeAttr, const VariablesMap &variables) { + bool hasNameAndNotLabel(const tinyxml2::XMLElement *node, const char *nodeName, const char *nodeAttr, const PropertiesMap &properties) { const char *name = node->Name(); if (!name || std::strcmp(nodeName, name) != 0) return false; const char *label = node->Attribute("Label"); if (label && std::strcmp(label, nodeAttr) == 0) return false; - return conditionIsTrue(node, variables); + return conditionIsTrue(node, properties); } std::list toStringList(const std::string &s) @@ -1249,7 +1874,8 @@ namespace { 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)); + if (pos2 > pos1) + ret.push_back(s.substr(pos1, pos2-pos1)); pos1 = pos2 + 1; if (pos1 >= s.size()) break; @@ -1259,29 +1885,8 @@ namespace { return ret; } - std::string findFile(const std::string &startDirectory, const std::string &file) - { - std::string currentDir = startDirectory; - if (currentDir.back() == '/' && currentDir.size() > 1 && currentDir[currentDir.size() - 2] != ':') - currentDir.pop_back(); - - while (!currentDir.empty()) { - std::string targetFile = Path::join(currentDir, file); - if (Path::isFile(targetFile)) - return targetFile; - if (currentDir.back() == '/' || (currentDir.back() == ':' && currentDir.size() == 2)) - break; - size_t lastSlash = currentDir.find_last_of('/'); - if (lastSlash == std::string::npos) - break; - currentDir.resize(lastSlash); - } - - return ""; - } - struct MSBuildThis { - VariablesMap &variablesMap; + PropertiesMap &propertiesMap; std::string thisFile; std::string thisFileName; std::string thisFileExtension; @@ -1289,38 +1894,41 @@ namespace { std::string thisFileDirectoryNoRoot; std::string thisFileFullPath; - MSBuildThis(const std::string &filename, VariablesMap &variables) - : variablesMap(variables) - , thisFile(variables.at("MSBuildThisFile")) - , thisFileName(variables.at("MSBuildThisFileName")) - , thisFileExtension(variables.at("MSBuildThisFileExtension")) - , thisFileDirectory(variables.at("MSBuildThisFileDirectory")) - , thisFileDirectoryNoRoot(variables.at("MSBuildThisFileDirectoryNoRoot")) - , thisFileFullPath(variables.at("MSBuildThisFileFullPath")) { - setMSBuildThis(filename, variables); - } - - static void setMSBuildThis(const std::string &filename, VariablesMap &variables) { - variables["MSBuildThisFileFullPath"] = filename; - std::string temp = filename.substr(filename.rfind('/') + 1, filename.size()); - variables["MSBuildThisFile"] = temp; + MSBuildThis(const std::string &filename, PropertiesMap &properties) + : propertiesMap(properties) + , thisFile(properties["MSBuildThisFile"]) + , thisFileName(properties["MSBuildThisFileName"]) + , thisFileExtension(properties["MSBuildThisFileExtension"]) + , thisFileDirectory(properties["MSBuildThisFileDirectory"]) + , thisFileDirectoryNoRoot(properties["MSBuildThisFileDirectoryNoRoot"]) + , thisFileFullPath(properties["MSBuildThisFileFullPath"]) { + setMSBuildThis(filename, properties); + } + + static void setMSBuildThis(const std::string &filename, PropertiesMap &properties) { + // Normalize once so all subsequent path ops can assume '/' separators. + const std::string nfilename = Path::simplifyPath(Path::fromNativeSeparators(filename)); + properties["MSBuildThisFileFullPath"] = nfilename; + const auto slash1 = nfilename.rfind('/'); + std::string temp = (slash1 != std::string::npos) ? nfilename.substr(slash1 + 1) : nfilename; + properties["MSBuildThisFile"] = temp; findAndReplace(temp, Path::getFilenameExtension(temp), ""); - variables["MSBuildThisFileName"] = temp; - variables["MSBuildThisFileDirectory"] = Path::simplifyPath(Path::getPathFromFilename(filename)); - temp = Path::simplifyPath(Path::getPathFromFilename(filename)); - std::string::size_type pos = filename.find('/', 0); + properties["MSBuildThisFileName"] = temp; + properties["MSBuildThisFileDirectory"] = Path::getPathFromFilename(nfilename); + temp = Path::getPathFromFilename(nfilename); + std::string::size_type pos = temp.find('/', 0); temp.erase(0, pos + 1); - variables["MSBuildThisFileDirectoryNoRoot"] = temp; - variables["MSBuildThisFileExtension"] = Path::getFilenameExtensionInLowerCase(filename); + properties["MSBuildThisFileDirectoryNoRoot"] = temp; + properties["MSBuildThisFileExtension"] = Path::getFilenameExtensionInLowerCase(nfilename); } ~MSBuildThis() { - variablesMap["MSBuildThisFile"] = thisFile; - variablesMap["MSBuildThisFileName"] = thisFileName; - variablesMap["MSBuildThisFileExtension"] = thisFileExtension; - variablesMap["MSBuildThisFileDirectory"] = thisFileDirectory; - variablesMap["MSBuildThisFileDirectoryNoRoot"] = thisFileDirectoryNoRoot; - variablesMap["MSBuildThisFileFullPath"] = thisFileFullPath; + propertiesMap["MSBuildThisFile"] = thisFile; + propertiesMap["MSBuildThisFileName"] = thisFileName; + propertiesMap["MSBuildThisFileExtension"] = thisFileExtension; + propertiesMap["MSBuildThisFileDirectory"] = thisFileDirectory; + propertiesMap["MSBuildThisFileDirectoryNoRoot"] = thisFileDirectoryNoRoot; + propertiesMap["MSBuildThisFileFullPath"] = thisFileFullPath; } }; @@ -1339,15 +1947,16 @@ namespace { std::string ImportProject::toAbsolute(const std::string &path) { - if (Path::isAbsolute(path)) - return Path::simplifyPath(path); - return Path::simplifyPath(Path::getCurrentPath() + "/" + path); + std::string internal(Path::fromNativeSeparators(path)); + if (Path::isAbsolute(internal)) + return Path::simplifyPath(internal); + return Path::simplifyPath(Path::getCurrentPath() + "/" + internal); } -std::string ImportProject::toAbsolute(const std::string &filename, const std::string &baseDir, VariablesMap &variables) +std::string ImportProject::toAbsolute(const std::string &filename, const std::string &baseDir, PropertiesMap &properties) { - std::string resolved(filename); - if (!simplifyPathWithVariables(resolved, variables)) + std::string resolved(Path::fromNativeSeparators(filename)); + if (!simplifyPathWithVariables(resolved, properties)) return resolved; if (Path::isAbsolute(resolved)) @@ -1355,17 +1964,22 @@ std::string ImportProject::toAbsolute(const std::string &filename, const std::st return Path::simplifyPath(baseDir + resolved); } -bool ImportProject::simplifyPathWithVariables(std::string &s, VariablesMap &variables) +bool ImportProject::simplifyPathWithVariables(std::string &s, PropertiesMap &properties) { - expandMSBuildVariables(s, variables); + // 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, VariablesMap &variables) +void ImportProject::fsSetIncludePaths(FileSettings &fs, const std::string &basepath, const std::list &in, PropertiesMap &properties) { std::set found; // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) @@ -1392,7 +2006,7 @@ void ImportProject::fsSetIncludePaths(FileSettings &fs, const std::string &basep if (s.find("$(") == std::string::npos) { s = Path::simplifyPath(basepath + s); } else { - if (!simplifyPathWithVariables(s, variables)) + if (!simplifyPathWithVariables(s, properties)) continue; } if (s.empty()) @@ -1401,27 +2015,67 @@ void ImportProject::fsSetIncludePaths(FileSettings &fs, const std::string &basep } } -void ImportProject::addProperty(const tinyxml2::XMLElement *node, VariablesMap &variables) { +void ImportProject::addProperty(const tinyxml2::XMLElement *node, PropertiesMap &properties) { const char *eName = node->Name(); - if (!eName || !conditionIsTrue(node, variables)) + if (!eName || !conditionIsTrue(node, properties)) return; const char *eText = node->GetText(); std::string text(eText ? eText : ""); - const std::string original = variables[eName]; + // Normalize native path separators before expansion so property values are + // stored with '/' and debug messages show normalized paths. + text = Path::fromNativeSeparators(std::move(text)); + const std::string original = properties[eName]; + findAndReplace(text, "$(" + std::string(eName) + ")", original); + expandMSBuildVariables(text, properties); + properties[eName] = text; + checkUnexpandedExpressions(text, eName); +} + +void ImportProject::addMetadata(const tinyxml2::XMLElement *node, PropertiesMap &properties, MetadataMap &metadata) { + const char *eName = node->Name(); + if (!eName || !conditionIsTrue(node, properties)) + return; + const char *eText = node->GetText(); + std::string text(eText ? eText : ""); + text = Path::fromNativeSeparators(std::move(text)); + const std::string original = metadata[eName]; findAndReplace(text, "%(" + std::string(eName) + ")", original); - expandMSBuildVariables(text, variables); - variables[eName] = text; + std::string::size_type pos = 0; + while ((pos = text.find("%(", pos)) != std::string::npos) { + const std::string::size_type end = text.find(')', pos); + if (end == std::string::npos) + break; + const std::string key = text.substr(pos + 2, end - pos - 2); + const auto it = metadata.find(key); + const std::string replacement = (it != metadata.end()) ? it->second : std::string(); + text.replace(pos, end - pos + 1, replacement); + pos += replacement.size(); + } + expandMSBuildVariables(text, properties); + metadata[eName] = text; checkUnexpandedExpressions(text, eName); } -std::string ImportProject::getProperty(const tinyxml2::XMLElement *node, VariablesMap &variables, const std::string &original) { +std::string ImportProject::getMetadata(const tinyxml2::XMLElement *node, PropertiesMap &properties, const MetadataMap &metadata, const std::string &original) { const char *eName = node->Name(); const char *eText = node->GetText(); - if (!eName || !eText || !conditionIsTrue(node, variables)) + if (!eName || !eText || !conditionIsTrue(node, properties)) return original; - std::string text(eText); + std::string text(Path::fromNativeSeparators(eText)); findAndReplace(text, "%(" + std::string(eName) + ")", original); - expandMSBuildVariables(text, variables); + { + std::string::size_type pos = 0; + while ((pos = text.find("%(", pos)) != std::string::npos) { + const std::string::size_type end = text.find(')', pos); + if (end == std::string::npos) break; + const std::string key = text.substr(pos + 2, end - pos - 2); + const auto it = metadata.find(key); + const std::string replacement = (it != metadata.end()) ? it->second : std::string(); + text.replace(pos, end - pos + 1, replacement); + pos += replacement.size(); + } + } + expandMSBuildVariables(text, properties); checkUnexpandedExpressions(text, eName); return text; } @@ -1451,22 +2105,20 @@ const std::string &ImportProject::importResultStr(ImportProject::ImportResult re ImportProject::ImportResult ImportProject::importCompile(const tinyxml2::XMLElement *node, const std::string &projectDir, - VariablesMap &variables, + PropertiesMap &properties, + const MetadataMap &metadata, std::list &compileList) { const char *include = node->Attribute("Include"); if (!include) return ImportResult::NotFound; - std::string toInclude = toAbsolute(include, projectDir, variables); + std::string toInclude = toAbsolute(include, projectDir, properties); if (!Path::acceptFile(toInclude)) return ImportResult::NotFound; ItemGroupClCompile compile(toInclude); // a file with no override of its own inherits the ItemDefinitionGroup value outright - compile.additionalIncludeDirectories = variables["AdditionalIncludeDirectories"]; - compile.forcedIncludeFiles = variables["ForcedIncludeFiles"]; - compile.preprocessorDefinitions = variables["PreprocessorDefinitions"]; - compile.languageStandard = variables["LanguageStandard"]; + compile.metadata = metadata; bool excludedFromBuild = false; for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { @@ -1474,19 +2126,103 @@ ImportProject::ImportResult ImportProject::importCompile(const tinyxml2::XMLElem if (!text) continue; - if (hasName(e1, "ExcludedFromBuild", variables)) { + if (hasName(e1, "ExcludedFromBuild", properties)) { if (caseInsensitiveStringCompare(text, "true") == 0) { excludedFromBuild = true; break; } - } else if (hasName(e1, "AdditionalIncludeDirectories", variables)) { - compile.additionalIncludeDirectories = getProperty(e1, variables, compile.additionalIncludeDirectories); - } else if (hasName(e1, "ForcedIncludeFiles", variables)) { - compile.forcedIncludeFiles = getProperty(e1, variables, compile.forcedIncludeFiles); - } else if (hasName(e1, "PreprocessorDefinitions", variables)) { - compile.preprocessorDefinitions = getProperty(e1, variables, compile.preprocessorDefinitions); - } else if (hasName(e1, "LanguageStandard", variables)) { - compile.languageStandard = getProperty(e1, variables, compile.languageStandard); + } else if (hasName(e1, "AdditionalIncludeDirectories", properties)) { + auto &v = compile.metadata["AdditionalIncludeDirectories"]; + v = getMetadata(e1, properties, compile.metadata, v); + } else if (hasName(e1, "ForcedIncludeFiles", properties)) { + auto &v = compile.metadata["ForcedIncludeFiles"]; + v = getMetadata(e1, properties, compile.metadata, v); + } else if (hasName(e1, "PreprocessorDefinitions", properties)) { + auto &v = compile.metadata["PreprocessorDefinitions"]; + v = getMetadata(e1, properties, compile.metadata, v); + } else if (hasName(e1, "LanguageStandard", properties)) { + auto &v = compile.metadata["LanguageStandard"]; + v = getMetadata(e1, properties, compile.metadata, v); + } else if (hasName(e1, "AdditionalOptions", properties)) { + auto &v = compile.metadata["AdditionalOptions"]; + v = getMetadata(e1, properties, compile.metadata, v); + } else if (hasName(e1, "AdditionalUsingDirectories", properties)) { + auto &v = compile.metadata["AdditionalUsingDirectories"]; + v = getMetadata(e1, properties, compile.metadata, v); + } + } + + if (!compile.metadata["AdditionalOptions"].empty()) { + std::vector args; + std::string arg; + bool quoted = false; + const std::string &additionalOptions = compile.metadata["AdditionalOptions"]; + + for (std::size_t i = 0; i < additionalOptions.size(); ++i) { + const char c = additionalOptions[i]; + + if (c == '"') { + quoted = !quoted; + } else if (std::isspace(static_cast(c)) && !quoted) { + if (!arg.empty()) { + args.emplace_back(std::move(arg)); + arg.clear(); + } + } else { + arg += c; + } + } + + if (!arg.empty()) + args.emplace_back(std::move(arg)); + + for (std::size_t i = 0; i < args.size(); ++i) { + const std::string &option = args[i]; + + if (option.size() >= 2 && + (option[0] == '/' || option[0] == '-') && + (option[1] == 'D' || option[1] == 'd')) { + + std::string define = option.substr(2); + + // /D NAME + if (define.empty() && i + 1 < args.size()) + define = args[++i]; + + if (!define.empty()) { + if (!compile.metadata["PreprocessorDefinitions"].empty()) + compile.metadata["PreprocessorDefinitions"] += ';'; + compile.metadata["PreprocessorDefinitions"] += define; + } + + } else if (option.size() >= 2 && + (option[0] == '/' || option[0] == '-') && + (option[1] == 'I' || option[1] == 'i')) { + + std::string path = option.substr(2); + + // /I path + if (path.empty() && i + 1 < args.size()) + path = args[++i]; + + if (!path.empty()) { + if (!compile.metadata["AdditionalIncludeDirectories"].empty()) + compile.metadata["AdditionalIncludeDirectories"] += ';'; + compile.metadata["AdditionalIncludeDirectories"] += path; + } + } else if (option == "/std:c++11" || option == "-std=c++11") { + compile.metadata["LanguageStandard"] = "stdcpp11"; + } else if (option == "/std:c++14" || option == "-std=c++14") { + compile.metadata["LanguageStandard"] = "stdcpp14"; + } else if (option == "/std:c++17" || option == "-std=c++17") { + compile.metadata["LanguageStandard"] = "stdcpp17"; + } else if (option == "/std:c++20" || option == "-std=c++20") { + compile.metadata["LanguageStandard"] = "stdcpp20"; + } else if (option == "/std:c++23" || option == "-std=c++23") { + compile.metadata["LanguageStandard"] = "stdcpp23"; + } else if (option == "/std:c++latest" || option == "-std=c++latest") { + compile.metadata["LanguageStandard"] = "stdcpplatest"; + } } } @@ -1498,19 +2234,157 @@ ImportProject::ImportResult ImportProject::importCompile(const tinyxml2::XMLElem ImportProject::ImportResult ImportProject::importProject(const tinyxml2::XMLElement *node, const std::string &projectDir, - VariablesMap &variables, + PropertiesMap &properties, + MetadataMap &metadata, std::list &projectConfigurationList, std::unordered_set &importStack) { const char *projectAttribute = node->Attribute("Project"); if (!projectAttribute) return ImportResult::Ok; - std::string file = toAbsolute(projectAttribute, projectDir, variables); + std::string file = toAbsolute(projectAttribute, projectDir, properties); std::string extension = Path::getFilenameExtensionInLowerCase(file); if (extension == ".props" || extension == ".targets") { const char *sdk = node->Attribute("Sdk"); if (sdk) return ImportResult::NotResolvable; - ImportResult result = importPropsOrTargets(file, variables, projectConfigurationList, importStack); + + if (file.find("Microsoft.Cpp.targets") != std::string::npos) { + auto it = properties.find("ForceImportBeforeCppTargets"); + if (it != properties.end()) { + ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); + return result; + } + } + + if (file.find("$(") != std::string::npos) { + std::string directoryBuildTargets = findFile(projectDir, "Directory.Build.targets"); + if (!directoryBuildTargets.empty()) { + ImportResult result = importPropsOrTargets(directoryBuildTargets, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + directoryBuildTargets + "\" - " + importResultStr(result)); + return result; + } + } + } else { + ImportResult result = importPropsOrTargets(file, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); + return result; + } + } + + it = properties.find("ForceImportAfterCppTargets"); + if (it != properties.end()) { + ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); + return result; + } + } + + return ImportResult::Ok; + } + + if (file.find("Microsoft.Cpp.Default.props") != std::string::npos) { + auto it = properties.find("ForceImportBeforeCppDefaultProps"); + if (it != properties.end()) { + ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); + return result; + } + } + + if (file.find("$(") != std::string::npos) { + // $(Configuration) = Debug, $(ConfigurationType) = Application, $(ApplicationType) + } else { + ImportResult result = importPropsOrTargets(file, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); + return result; + } + } + + it = properties.find("ForceImportAfterCppDefaultProps"); + if (it != properties.end()) { + ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); + return result; + } + } + + return ImportResult::Ok; + } + + if (file.find("Microsoft.Cpp.props") != std::string::npos) { + // If ForceImportBeforeCppProps is already set (e.g. by the vcxproj itself), + // honour it now before anything else. + const bool hadForceImportBefore = properties.count("ForceImportBeforeCppProps") > 0; + std::string forceImportBeforeCppProps; + if (hadForceImportBefore) { + auto it = properties.find("ForceImportBeforeCppProps"); + forceImportBeforeCppProps = it->second; + ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); + return result; + } + } + + if (file.find("$(") != std::string::npos) { + // $(Platform), $(PlatformToolset), $(TargetName), $(TargetExt), $(LanguageStandard) + properties["IntDir"] = "$(Platform)/$(Configuration)/"; + properties["OutDir"] = "$(SolutionDir)$(Platform)/$(Configuration)/"; + properties["GeneratedFilesDir"] = "$(IntDir)Generated Files/"; + + std::string directoryBuildProps = findFile(projectDir, "Directory.Build.props"); + if (!directoryBuildProps.empty()) { + ImportResult result = importPropsOrTargets(directoryBuildProps, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + directoryBuildProps + "\" - " + importResultStr(result)); + return result; + } + } + + // Directory.Build.props may have newly set ForceImportBeforeCppProps + // (e.g. PowerToys sets it to Cpp.Build.props which defines ProjectConfigurations). + // Real MSBuild auto-imports Directory.Build.props before evaluating + // Microsoft.Cpp.props, so ForceImportBeforeCppProps set there must be + // honoured here. + auto it = properties.find("ForceImportBeforeCppProps"); + if (it != properties.end()) { + if (it->second != forceImportBeforeCppProps) { + ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); + return result; + } + } + } + } else { + ImportResult result = importPropsOrTargets(file, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); + return result; + } + } + + auto it = properties.find("ForceImportAfterCppProps"); + if (it != properties.end()) { + ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) { + errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); + return result; + } + } + + return ImportResult::Ok; + } + + ImportResult result = importPropsOrTargets(file, properties, metadata, projectConfigurationList, importStack); if (result > ImportResult::NotResolvable) { errors.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); return result; @@ -1519,24 +2393,25 @@ ImportProject::ImportResult ImportProject::importProject(const tinyxml2::XMLElem debugs.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); } } else { - debugs.emplace_back("Could not import \"" + file + "\" - "); + debugs.emplace_back("Could not import \"" + file + "\" unsupported extension " + extension); } return ImportResult::Ok; } ImportProject::ImportResult ImportProject::importPropsOrTargets(const std::string &file, - VariablesMap &variables, + PropertiesMap &properties, + MetadataMap &metadata, std::list &projectConfigurationList, std::unordered_set &importStack) { std::string filename(file); - // variables can't be resolved - if (!simplifyPathWithVariables(filename, variables)) + // properties can't be resolved + if (!simplifyPathWithVariables(filename, properties)) return ImportResult::NotResolvable; // prepend project dir (if it exists) to transform relative paths into absolute ones - if (!Path::isAbsolute(filename) && variables.count("ProjectDir") > 0) - filename = toAbsolute(filename, variables.at("ProjectDir"), variables); + if (!Path::isAbsolute(filename) && properties.count("ProjectDir") > 0) + filename = toAbsolute(filename, properties.at("ProjectDir"), properties); // detect circular property sheet imports (A imports B, B imports A, a file importing // itself, ...) instead of recursing until the stack overflows - mirrors MSBuild's own @@ -1555,48 +2430,59 @@ ImportProject::ImportResult ImportProject::importPropsOrTargets(const std::strin if (rootnode == nullptr) return ImportResult::NotValid; - MSBuildThis msBuildThis(filename, variables); + MSBuildThis msBuildThis(filename, properties); std::string propsDir = Path::getPathFromFilename(filename); ImportResult ret = ImportResult::Ok; for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { - if (hasName(node, "ImportGroup", variables)) { + if (hasName(node, "ImportGroup", properties)) { // Accept any (PropertySheets, Shared, unlabeled) — .targets files // commonly use unlabeled or differently-labeled groups for transitive imports. const char* label = node->Attribute("Label"); const bool isPropertySheets = (label == nullptr) || (std::strcmp(label, "PropertySheets") == 0) || - (std::strcmp(label, "Shared") == 0); + (std::strcmp(label, "Shared") == 0) || + (std::strcmp(label, "ExtensionSettings") == 0) || + (std::strcmp(label, "ExtensionTargets") == 0); if (isPropertySheets) { for (const tinyxml2::XMLElement *importGroup = node->FirstChildElement(); importGroup; importGroup = importGroup->NextSiblingElement()) { - if (hasNameAndAttribute(importGroup, "Import", "Project", variables)) { - ImportResult result = importProject(importGroup, propsDir, variables, projectConfigurationList, importStack); + if (hasNameAndAttribute(importGroup, "Import", "Project", properties)) { + ImportResult result = importProject(importGroup, propsDir, properties, metadata, projectConfigurationList, importStack); if (result > ImportResult::NotResolvable) return result; } } } - } else if (hasName(node, "PropertyGroup", variables)) { + } else if (hasName(node, "PropertyGroup", properties)) { for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) - addProperty(e, variables); - } else if (hasName(node, "ItemDefinitionGroup", variables)) { + addProperty(e, properties); + } else if (hasName(node, "ItemDefinitionGroup", properties)) { for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { - if (hasName(e1, "ClCompile", variables)) { + if (hasName(e1, "ClCompile", properties)) { for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) { - addProperty(e2, variables); + addMetadata(e2, properties, metadata); } } } - } else if (hasNameAndLabel(node, "ItemGroup", "ProjectConfigurations", variables)) { + } else if (hasNameAndLabel(node, "ItemGroup", "ProjectConfigurations", properties)) { for (const tinyxml2::XMLElement *pcNode = node->FirstChildElement("ProjectConfiguration"); pcNode; pcNode = pcNode->NextSiblingElement("ProjectConfiguration")) { const ProjectConfiguration pc(pcNode); - if (pc.platform != ProjectConfiguration::Unknown) { - projectConfigurationList.emplace_back(pc); - mAllVSConfigs.insert(pc.configuration); + if (!pc.configuration.empty()) { + // Deduplicate: the same config can arrive again when Directory.Build.props / + // Cpp.Build.props is re-imported inside the per-config loop. + const bool already = std::any_of(projectConfigurationList.cbegin(), + projectConfigurationList.cend(), + [&pc](const ProjectConfiguration &existing) { + return existing.name == pc.name; + }); + if (!already) { + projectConfigurationList.emplace_back(pc); + mAllVSConfigs.insert(pc.configuration); + } } } - } else if (hasNameAndAttribute(node, "Import", "Project", variables)) { - ImportResult result = importProject(node, propsDir, variables, projectConfigurationList, importStack); + } else if (hasNameAndAttribute(node, "Import", "Project", properties)) { + ImportResult result = importProject(node, propsDir, properties, metadata, projectConfigurationList, importStack); if (result > ImportResult::NotResolvable) return result; } @@ -1606,14 +2492,15 @@ ImportProject::ImportResult ImportProject::importPropsOrTargets(const std::strin } ImportProject::ImportResult ImportProject::importVcxitems(const std::string &items, - VariablesMap &variables, + PropertiesMap &properties, + MetadataMap &metadata, std::list &compileList, std::list &projectConfigurationList, std::unordered_set &importStack) { std::string filename(items); - // variables can't be resolved - if (!simplifyPathWithVariables(filename, variables)) + // properties can't be resolved + if (!simplifyPathWithVariables(filename, properties)) return ImportResult::NotResolvable; const std::string simplifiedFilename = Path::simplifyPath(filename); @@ -1632,34 +2519,37 @@ ImportProject::ImportResult ImportProject::importVcxitems(const std::string &ite return ImportResult::NotValid; const std::string itemsDir = Path::simplifyPath(Path::getPathFromFilename(filename)); - MSBuildThis msBuildThis(filename, variables); + MSBuildThis msBuildThis(filename, properties); - ImportResult ret = ImportResult::Ok; for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { - if (hasName(node, "ItemGroup", variables)) { + if (hasName(node, "ItemGroup", properties)) { for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (hasName(e, "ClCompile", variables)) { - importCompile(e, itemsDir, variables, compileList); + if (hasName(e, "ClCompile", properties)) { + importCompile(e, itemsDir, properties, metadata, compileList); } } - } else if (hasName(node, "ItemDefinitionGroup", variables)) { + } else if (hasName(node, "PropertyGroup", properties)) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) + addProperty(e, properties); + } else if (hasName(node, "ItemDefinitionGroup", properties)) { for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { - if (hasName(e1, "ClCompile", variables)) { + if (hasName(e1, "ClCompile", properties)) { for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) - addProperty(e2, variables); + addMetadata(e2, properties, metadata); } } - } else if (hasNameAndAttribute(node, "Import", "Project", variables)) { - if (importProject(node, itemsDir, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) - break; + } else if (hasNameAndAttribute(node, "Import", "Project", properties)) { + const ImportResult result = importProject(node, itemsDir, properties, metadata,projectConfigurationList, importStack); + if (result > ImportResult::NotResolvable) + return result; } } - return ret; + return ImportResult::Ok; } bool ImportProject::importVcxproj(const std::string &filename, - VariablesMap &variables, + PropertiesMap &properties, const std::vector &fileFilters) { tinyxml2::XMLDocument doc; @@ -1668,39 +2558,46 @@ 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; } + MetadataMap metadata; + + // Normalize separators once; callers typically pass toAbsolute() results + // but normalize here as a safety net so all subsequent rfind('/') are correct. + const std::string nfilename = Path::simplifyPath(Path::fromNativeSeparators(filename)); - variables.emplace("VisualStudioVersion", "17.0"); + properties.emplace("VisualStudioVersion", "17.0"); - variables["ProjectPath"] = filename; - std::string temp = filename.substr(filename.rfind('/') + 1, filename.size()); - variables["ProjectFileName"] = temp; + properties["ProjectPath"] = nfilename; + const auto projSlash = nfilename.rfind('/'); + std::string temp = (projSlash != std::string::npos) ? nfilename.substr(projSlash + 1) : nfilename; + properties["ProjectFileName"] = temp; findAndReplace(temp, Path::getFilenameExtension(temp), ""); - variables["ProjectName"] = temp; + properties["ProjectName"] = temp; temp.resize(std::min(temp.size(), size_t(16))); - variables["ShortProjectName"] = temp; - variables["ProjectExt"] = Path::getFilenameExtensionInLowerCase(filename); - variables["ProjectDir"] = Path::simplifyPath(Path::getPathFromFilename(filename)); + properties["ShortProjectName"] = temp; + properties["ProjectExt"] = Path::getFilenameExtensionInLowerCase(nfilename); + properties["ProjectDir"] = Path::getPathFromFilename(nfilename); // importVcxproj called directly - if (variables.find("SolutionDir") == variables.end()) { + if (properties.find("SolutionDir") == properties.end()) { debugs.clear(); - variables["SolutionDir"] = variables["ProjectDir"]; + properties["SolutionDir"] = properties["ProjectDir"]; } - variables["MSBuildProjectName"] = variables["ProjectName"]; - variables["MSBuildProjectExtension"] = variables["ProjectExt"]; - variables["MSBuildProjectDirectory"] = variables["ProjectDir"]; - variables["MSBuildProjectFile"] = variables["ProjectFileName"]; - variables["MSBuildProjectFullPath"] = variables["ProjectPath"]; - - // common defaults - variables["IntDir"] = "$(Platform)/$(Configuration)/"; - variables["OutDir"] = "$(SolutionDir)$(Platform)/$(Configuration)/"; - variables["GeneratedFilesDir"] = "$(IntDir)Generated Files/"; + properties["MSBuildProjectName"] = properties["ProjectName"]; + properties["MSBuildProjectExtension"] = properties["ProjectExt"]; + properties["MSBuildProjectDirectory"] = properties["ProjectDir"]; + // remove file seperator on end of path + if (!properties["MSBuildProjectDirectory"].empty() && + (properties["MSBuildProjectDirectory"].back() == '/' || + properties["MSBuildProjectDirectory"].back() == '\\')) { + properties["MSBuildProjectDirectory"].pop_back(); + } + properties["MSBuildProjectFile"] = properties["ProjectFileName"]; + properties["MSBuildProjectFullPath"] = properties["ProjectPath"]; - MSBuildThis::setMSBuildThis(filename, variables); + MSBuildThis::setMSBuildThis(nfilename, properties); - std::string projectDir = variables["ProjectDir"]; + std::string projectDir = properties["ProjectDir"]; std::list projectConfigurationList; std::list compileList; std::unordered_set importStack; @@ -1714,11 +2611,11 @@ bool ImportProject::importVcxproj(const std::string &filename, // Read MSBuildToolsVersion directly from . // "Current" is the standard value for VS2019+ and is the correct fallback. const char *toolsVersion = rootnode->Attribute("ToolsVersion"); - variables.emplace("MSBuildToolsVersion", toolsVersion ? toolsVersion : "Current"); + 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", variables)) { + 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 @@ -1729,97 +2626,89 @@ bool ImportProject::importVcxproj(const std::string &filename, } } - // fixme how to do this right - std::string directoryBuildProps = findFile(projectDir, "Directory.Build.props"); - if (!directoryBuildProps.empty()) { - if (importPropsOrTargets(directoryBuildProps, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) { - errors.emplace_back("Could not load property sheet \"" + directoryBuildProps + "\" - it may be missing, invalid, or part of a circular import"); - } - // fixme is this right - std::string forceImportBeforeCppProps = variables["ForceImportBeforeCppProps"]; - if (!forceImportBeforeCppProps.empty()) { - if (importPropsOrTargets(forceImportBeforeCppProps, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) { - errors.emplace_back("Could not load property sheet \"" + forceImportBeforeCppProps + "\" - it may be missing, invalid, or part of a circular import"); + // Discovery pass: if no ProjectConfigurations were found inline in the vcxproj, walk + // its / nodes through importProject so that every MSBuild import + // mechanism (Directory.Build.props, ForceImportBeforeCppProps, etc.) is honoured + // generically — no special-casing of individual property names required. + // We also process nodes so that properties needed to resolve import + // paths are available. Stop as soon as configurations are found. + // Use isolated copies of properties, metadata and importStack so that side-effects + // of the discovery imports (extra properties, pre-populated import stack, etc.) do + // not bleed into the real per-configuration import pass that follows. + if (projectConfigurationList.empty()) { + PropertiesMap discoverProps = properties; + MetadataMap discoverMeta; + std::unordered_set discoverStack; + for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); + node && projectConfigurationList.empty(); + node = node->NextSiblingElement()) { + if (hasName(node, "PropertyGroup", discoverProps)) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) + addProperty(e, discoverProps); + } else if (hasName(node, "ImportGroup", discoverProps)) { + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e && projectConfigurationList.empty(); e = e->NextSiblingElement()) { + if (hasNameAndAttribute(e, "Import", "Project", discoverProps)) + importProject(e, projectDir, discoverProps, discoverMeta, projectConfigurationList, discoverStack); + } + } else if (hasNameAndAttribute(node, "Import", "Project", discoverProps)) { + importProject(node, projectDir, discoverProps, discoverMeta, projectConfigurationList, discoverStack); } } - -/* fixme are these really all of them ? - std::string forceImportAfterCppDefaultProp = variables["ForceImportAfterCppDefaultProp"]; - if (!forceImportAfterCppDefaultProp.empty()) - importPropsOrTargets(forceImportAfterCppDefaultProp, variables, projectConfigurationList, importStack); - - std::string forceImportAfterCppProps = variables["ForceImportAfterCppProps"]; - if (!forceImportAfterCppProps.empty()) - importPropsOrTargets(forceImportAfterCppProps, variables, projectConfigurationList, importStack); - */ - } - - std::string directoryBuildTargets = findFile(projectDir, "Directory.Build.targets"); - if (!directoryBuildTargets.empty()) { - if (importPropsOrTargets(directoryBuildTargets, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) { - errors.emplace_back("Could not load targets \"" + directoryBuildTargets + "\" - it may be missing, invalid, or part of a circular import"); - } -/* fixme - ForceImportBeforeCppProps - ForceImportAfterCppDefaultProps - ForceImportAfterCppProps - ForceImportBeforeCppTargets - ForceImportAfterCppTargets - */ } - VariablesMap originalVariables = variables; + PropertiesMap originalVariables = properties; bool first = true; for (const ProjectConfiguration &pc : projectConfigurationList) { if (!first) { compileList.clear(); - variables = originalVariables; + properties = originalVariables; + metadata.clear(); } else first = false; - variables["Configuration"] = pc.configuration; - variables["Platform"] = pc.platformStr; + properties["Configuration"] = pc.configuration; + properties["Platform"] = pc.platformStr; for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { - if (hasNameAndNotLabel(node, "ItemGroup", "ProjectConfigurations", variables)) { + if (hasNameAndNotLabel(node, "ItemGroup", "ProjectConfigurations", properties)) { for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (hasNameAndAttribute(e, "ClCompile", "Include", variables)) - importCompile(e, projectDir, variables, compileList); + if (hasNameAndAttribute(e, "ClCompile", "Include", properties)) + importCompile(e, projectDir, properties, metadata, compileList); } - } else if (hasName(node, "ItemDefinitionGroup", variables)) { + } else if (hasName(node, "ItemDefinitionGroup", properties)) { for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { - if (hasName(e1, "ClCompile", variables)) { + if (hasName(e1, "ClCompile", properties)) { for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) - addProperty(e2, variables); + addMetadata(e2, properties, metadata); } } - } else if (hasName(node, "PropertyGroup", variables)) { + } else if (hasName(node, "PropertyGroup", properties)) { for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) - addProperty(e, variables); - } else if (hasName(node, "ImportGroup", variables)) { + addProperty(e, properties); + } else if (hasName(node, "ImportGroup", properties)) { const char *labelAttribute = node->Attribute("Label"); if (labelAttribute && std::strcmp(labelAttribute, "PropertySheets") == 0) { for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (hasName(e, "Import", variables)) { + if (hasName(e, "Import", properties)) { const char *projectAttribute = e->Attribute("Project"); if (!projectAttribute) continue; - if (importProject(e, projectDir, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) + if (importProject(e, projectDir, properties, metadata, projectConfigurationList, importStack) > ImportResult::NotResolvable) return false; } } } else if (labelAttribute && std::strcmp(labelAttribute, "Shared") == 0) { for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (hasName(e, "Import", variables)) { + if (hasName(e, "Import", properties)) { const char *projectAttribute = e->Attribute("Project"); if (!projectAttribute) continue; - std::string file = toAbsolute(projectAttribute, projectDir, variables); + std::string file = toAbsolute(projectAttribute, projectDir, properties); std::string extension = Path::getFilenameExtensionInLowerCase(file); if (extension == ".vcxitems") { - ImportResult result = importVcxitems(file, variables, compileList, projectConfigurationList, importStack); + ImportResult result = importVcxitems(file, properties, metadata, compileList, projectConfigurationList, importStack); if (result > ImportResult::NotResolvable) { errors.emplace_back("Could not import items \"" + file + "\" - " + importResultStr(result)); return false; @@ -1828,13 +2717,25 @@ bool ImportProject::importVcxproj(const std::string &filename, debugs.emplace_back("Could not import items \"" + file + "\" - " + importResultStr(result)); } } else { - debugs.emplace_back("Could not import unknown file type \"" + file + "\""); + debugs.emplace_back("Could not import \"" + file + "\" unsupported extension " + extension); } } } + } else { + // Unlabeled or other-labeled ImportGroup (e.g. ExtensionSettings, + // ExtensionTargets) — process children like PropertySheets. + for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { + if (hasName(e, "Import", properties)) { + const char *projectAttribute = e->Attribute("Project"); + if (!projectAttribute) + continue; + if (importProject(e, projectDir, properties, metadata, projectConfigurationList, importStack) > ImportResult::NotResolvable) + return false; + } + } } - } else if (hasNameAndAttribute(node, "Import", "Project", variables)) { - if (importProject(node, projectDir, variables, projectConfigurationList, importStack) > ImportResult::NotResolvable) + } else if (hasNameAndAttribute(node, "Import", "Project", properties)) { + if (importProject(node, projectDir, properties, metadata, projectConfigurationList, importStack) > ImportResult::NotResolvable) return false; } } @@ -1866,10 +2767,16 @@ bool ImportProject::importVcxproj(const std::string &filename, else if (pc.platform == ProjectConfiguration::x64) { fs.platformType = Platform::Type::Win64; fs.defines += ";_WIN64=1"; + } else if (pc.platform == ProjectConfiguration::ARM64) { + fs.platformType = Platform::Type::WinARM64; + fs.defines += ";_M_ARM64=1"; + } else if (pc.platform == ProjectConfiguration::ARM) { + fs.platformType = Platform::Type::WinARM; + fs.defines += ";_M_ARM=1"; } Standards::cppstd_t cppstd = Standards::CPPLatest; - const std::string &languageStandard = compile.languageStandard; + const std::string &languageStandard = compile.get("LanguageStandard"); if (languageStandard == "stdcpp11") cppstd = Standards::CPP11; else if (languageStandard == "stdcpp14") @@ -1884,7 +2791,7 @@ bool ImportProject::importVcxproj(const std::string &filename, cppstd = Standards::CPPLatest; fs.standard = Standards::getCPP(cppstd); - std::string enableEnhancedInstructionSet = variables["EnableEnhancedInstructionSet"]; + std::string enableEnhancedInstructionSet = compile.get("EnableEnhancedInstructionSet"); if (enableEnhancedInstructionSet == "StreamingSIMDExtensions") fs.defines += ";__SSE__"; else if (enableEnhancedInstructionSet == "StreamingSIMDExtensions2") @@ -1896,11 +2803,11 @@ bool ImportProject::importVcxproj(const std::string &filename, else if (enableEnhancedInstructionSet == "AdvancedVectorExtensions512") fs.defines += ";__AVX512F__"; - const auto charSetIt = variables.find("CharacterSet"); - const std::string charSet = (charSetIt != variables.end()) ? charSetIt->second : std::string(); + const auto charSetIt = properties.find("CharacterSet"); + const std::string charSet = (charSetIt != properties.end()) ? charSetIt->second : std::string(); - const auto useOfMfcIt = variables.find("UseOfMfc"); - fs.useMfc = useOfMfcIt != variables.end() && !useOfMfcIt->second.empty() && + const auto useOfMfcIt = properties.find("UseOfMfc"); + fs.useMfc = useOfMfcIt != properties.end() && !useOfMfcIt->second.empty() && caseInsensitiveStringCompare(useOfMfcIt->second, "false") != 0; if (charSet == "Unicode") { @@ -1910,15 +2817,18 @@ bool ImportProject::importVcxproj(const std::string &filename, } std::string defines = fs.defines; - if (!compile.preprocessorDefinitions.empty()) - defines += (";" + compile.preprocessorDefinitions); + if (!compile.get("PreprocessorDefinitions").empty()) + defines += (";" + compile.get("PreprocessorDefinitions")); fsSetDefines(fs, defines); - fsSetIncludePaths(fs, projectDir, toStringList(variables["IncludePath"]), variables); + { + const auto includePathIt = properties.find("IncludePath"); + fsSetIncludePaths(fs, projectDir, toStringList(includePathIt != properties.end() ? includePathIt->second : std::string()), properties); + } fs.systemIncludePaths = std::move(fs.includePaths); - fsSetIncludePaths(fs, projectDir, toStringList(compile.additionalIncludeDirectories), variables); - fs.forcedIncludes = toStringList(compile.forcedIncludeFiles); + fsSetIncludePaths(fs, projectDir, toStringList(compile.get("AdditionalIncludeDirectories")), properties); + fs.forcedIncludes = toStringList(compile.get("ForcedIncludeFiles")); for (auto &forcedInclude : fs.forcedIncludes) - forcedInclude = toAbsolute(forcedInclude, projectDir, variables); + forcedInclude = toAbsolute(forcedInclude, projectDir, properties); fileSettings.push_back(std::move(fs)); } @@ -2161,15 +3071,15 @@ bool ImportProject::importBcb6Prj(const std::string &projectFilename) predefines += ";__WIN32__=1"; } - // Include paths may contain variables like "$(BCB)\include" or "$(BCB)\include\vcl". + // Include paths may contain properties like "$(BCB)\include" or "$(BCB)\include\vcl". // Those get resolved by ImportProject::FileSettings::setIncludePaths by - // 1. checking the provided variables map ("BCB" => "C:\\Program Files (x86)\\Borland\\CBuilder6") - // 2. checking env variables as a fallback - // Setting env is always possible. Configuring the variables via cli might be an addition. + // 1. checking the provided properties map ("BCB" => "C:\\Program Files (x86)\\Borland\\CBuilder6") + // 2. checking env properties as a fallback + // Setting env is always possible. Configuring the properties via cli might be an addition. // Reading the BCB6 install location from registry in windows environments would also be possible, // but I didn't see any such functionality around the source. Not in favor of adding it only // for the BCB6 project loading. - VariablesMap variables; + PropertiesMap properties; const std::string defines = predefines + ";" + sysdefines + ";" + userdefines; const std::string cppDefines = cppPredefines + ";" + defines; const bool forceCppMode = (cflags.find("-P") != cflags.end()); @@ -2187,7 +3097,7 @@ bool ImportProject::importBcb6Prj(const std::string &projectFilename) const bool cppMode = forceCppMode || Path::getFilenameExtensionInLowerCase(c) == ".cpp"; // TODO: needs to set language and ignore later identification and language enforcement FileSettings fs{Path::simplifyPath(Path::isAbsolute(c) ? c : projectDir + c), Standards::Language::None, 0}; // file will be identified later on - fsSetIncludePaths(fs, projectDir, toStringList(includePath), variables); + fsSetIncludePaths(fs, projectDir, toStringList(includePath), properties); fsSetDefines(fs, cppMode ? cppDefines : defines); fileSettings.push_back(std::move(fs)); } @@ -2457,11 +3367,20 @@ void ImportProject::selectOneVsConfig(Platform::Type platform) } const FileSettings &fs = *it; bool remove = false; - if (!startsWith(fs.cfg,"Debug")) + const std::string cfgName = fs.cfg.substr(0, fs.cfg.find('|')); + if (cfgName.size() < 5 || caseInsensitiveStringCompare(cfgName.substr(0, 5), "Debug") != 0) + remove = true; + + if (platform == Platform::Type::Win64 && fs.platformType != Platform::Type::Win64) + remove = true; + else if (platform == Platform::Type::WinARM64 && fs.platformType != Platform::Type::WinARM64) remove = true; - if (platform == Platform::Type::Win64 && fs.platformType != platform) + 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::WinARM)) remove = true; else if (filenames.find(fs.filename()) != filenames.end()) remove = true; @@ -2486,9 +3405,16 @@ void ImportProject::selectVsConfigurations(Platform::Type platform, const std::v bool remove = false; if (std::find(configurations.begin(), configurations.end(), config) == configurations.end()) remove = true; - if (platform == Platform::Type::Win64 && fs.platformType != platform) + if (platform == Platform::Type::Win64 && fs.platformType != Platform::Type::Win64) + remove = true; + else if (platform == Platform::Type::WinARM64 && fs.platformType != Platform::Type::WinARM64) remove = true; - else if ((platform == Platform::Type::Win32A || platform == Platform::Type::Win32W) && fs.platformType == Platform::Type::Win64) + else if (platform == Platform::Type::WinARM && fs.platformType != Platform::Type::WinARM) + remove = true; + else if ((platform == Platform::Type::Win32A || platform == Platform::Type::Win32W) && + (fs.platformType == Platform::Type::Win64 || + fs.platformType == Platform::Type::WinARM64 || + fs.platformType == Platform::Type::WinARM)) remove = true; if (remove) { it = fileSettings.erase(it); @@ -2529,8 +3455,19 @@ bool cppcheck::testing::evaluateVcxprojCondition(const std::string& condition, const std::string& configuration, const std::string& platform) { - VariablesMap variables; - variables["Platform"] = platform; - variables["Configuration"] = configuration; - return evalCondition(condition, variables); + PropertiesMap properties; + properties["Platform"] = platform; + properties["Configuration"] = configuration; + // Use ConditionParser directly so exceptions propagate to the caller; + // evalCondition swallows them (by design for production use). + return ConditionParser(condition, properties).parse(); +} + +// cppcheck-suppress unusedFunction +std::string cppcheck::testing::expandMSBuildExpression(const std::string& expr) +{ + PropertiesMap properties; + std::string s = expr; + expandMSBuildVariables(s, properties); + return s; } diff --git a/lib/importproject.h b/lib/importproject.h index f2b0a89a6c5..93dc7e11c87 100644 --- a/lib/importproject.h +++ b/lib/importproject.h @@ -55,10 +55,14 @@ namespace cppcheck { namespace testing { CPPCHECKLIB bool evaluateVcxprojCondition(const std::string& condition, const std::string& configuration, const std::string& platform); + /** Expand MSBuild property expressions ($(Name), $([Class]::Method(args))) in \p expr + * against an empty property map and return the result. Intended for unit tests. */ + CPPCHECKLIB std::string expandMSBuildExpression(const std::string& expr); } } -using VariablesMap = std::map; +using PropertiesMap = std::map; +using MetadataMap = std::map; /** * @brief Importing project settings. @@ -88,7 +92,7 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { protected: static void fsSetDefines(FileSettings& fs, std::string defs); - void fsSetIncludePaths(FileSettings& fs, const std::string &basepath, const std::list &in, VariablesMap &variables); + void fsSetIncludePaths(FileSettings& fs, const std::string &basepath, const std::list &in, PropertiesMap &properties); public: std::list fileSettings; @@ -128,9 +132,6 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { static std::string collectArgs(const std::string &cmd, std::vector &args); void setRelativePaths(const std::string &filename); - VariablesMap mVariables; - - private: static void parseArgs(FileSettings &fs, const std::vector &args); @@ -141,48 +142,56 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { std::string name; std::string configuration; - enum : std::uint8_t { Win32, x64, Unknown } platform = Unknown; + enum : std::uint8_t { Win32, x64, ARM64, ARM, Unknown } platform = Unknown; std::string platformStr; }; struct ItemGroupClCompile { explicit ItemGroupClCompile(std::string filename) : filename(std::move(filename)) {} std::string filename; - std::string additionalIncludeDirectories; - std::string forcedIncludeFiles; - std::string preprocessorDefinitions; - std::string languageStandard; + 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, VariablesMap &variables, const std::vector &fileFilters); + bool importDirectorySolutionProps(PropertiesMap &properties); + bool importVcxproj(const std::string &filename, PropertiesMap &properties, const std::vector &fileFilters); ImportResult importPropsOrTargets(const std::string &file, - VariablesMap &variables, + PropertiesMap &properties, + MetadataMap &metadata, std::list &projectConfigurationList, std::unordered_set &importStack); ImportResult importVcxitems(const std::string &items, - VariablesMap &variables, + PropertiesMap &properties, + MetadataMap &metadata, std::list &compileList, std::list &projectConfigurationList, std::unordered_set &importStack); ImportResult importProject(const tinyxml2::XMLElement *node, const std::string &projectDir, - VariablesMap &variables, + PropertiesMap &properties, + MetadataMap &metadata, std::list &projectConfigurationList, std::unordered_set &importStack); ImportResult importCompile(const tinyxml2::XMLElement *node, const std::string &projectDir, - VariablesMap &variables, + PropertiesMap &properties, + const MetadataMap &metadata, std::list &compileList); void checkUnexpandedExpressions(const std::string &text, const char *context); - bool simplifyPathWithVariables(std::string &s, VariablesMap &variables); - void addProperty(const tinyxml2::XMLElement *node, VariablesMap &variables); - std::string getProperty(const tinyxml2::XMLElement *node, VariablesMap &variables, const std::string &original); - std::string toAbsolute(const std::string &filename, const std::string &baseDir, VariablesMap &variables); + bool simplifyPathWithVariables(std::string &s, PropertiesMap &properties); + void addProperty(const tinyxml2::XMLElement *node, PropertiesMap &properties); + void addMetadata(const tinyxml2::XMLElement *node, PropertiesMap &properties, MetadataMap &metadata); + std::string getMetadata(const tinyxml2::XMLElement *node, PropertiesMap &properties, const MetadataMap &metadata, const std::string &original); + std::string toAbsolute(const std::string &filename, const std::string &baseDir, PropertiesMap &properties); static std::string toAbsolute(const std::string &path); - static void setSolution(const std::string &filename, VariablesMap &variables); + static void setSolution(const std::string &filename, PropertiesMap &properties); std::string mPath; diff --git a/lib/platform.cpp b/lib/platform.cpp index 0e202b9f49a..3a960404cd7 100644 --- a/lib/platform.cpp +++ b/lib/platform.cpp @@ -97,6 +97,42 @@ bool Platform::set(Type t) char_bit = 8; calculateBitMembers(); return true; + case Type::WinARM64: + type = t; + windows = true; + sizeof_bool = 1; + sizeof_short = 2; + sizeof_int = 4; + sizeof_long = 4; + sizeof_long_long = 8; + sizeof_float = 4; + sizeof_double = 8; + sizeof_long_double = 8; + sizeof_wchar_t = 2; + sizeof_size_t = 8; + sizeof_pointer = 8; + defaultSign = 's'; + char_bit = 8; + calculateBitMembers(); + return true; + case Type::WinARM: + type = t; + windows = true; + sizeof_bool = 1; + sizeof_short = 2; + sizeof_int = 4; + sizeof_long = 4; + sizeof_long_long = 8; + sizeof_float = 4; + sizeof_double = 8; + sizeof_long_double = 8; + sizeof_wchar_t = 2; + sizeof_size_t = 4; + sizeof_pointer = 4; + defaultSign = 's'; + char_bit = 8; + calculateBitMembers(); + return true; case Type::Unix32: type = t; windows = false; @@ -150,6 +186,10 @@ bool Platform::set(const std::string& platformstr, std::string& errstr, const st set(Type::Win32W); else if (platformstr == "win64") set(Type::Win64); + else if (platformstr == "winARM64") + set(Type::WinARM64); + else if (platformstr == "winARM") + set(Type::WinARM); else if (platformstr == "unix32") set(Type::Unix32); else if (platformstr == "unix64") diff --git a/lib/platform.h b/lib/platform.h index 97e7d296fba..cd1601df6bb 100644 --- a/lib/platform.h +++ b/lib/platform.h @@ -140,6 +140,8 @@ class CPPCHECKLIB Platform { Win32A, Win32W, Win64, + WinARM64, + WinARM, Unix32, Unix64, File @@ -188,6 +190,10 @@ class CPPCHECKLIB Platform { return "win32W"; case Type::Win64: return "win64"; + case Type::WinARM64: + return "winARM64"; + case Type::WinARM: + return "winARM"; case Type::Unix32: return "unix32"; case Type::Unix64: diff --git a/test/testimportproject.cpp b/test/testimportproject.cpp index 71dabd41029..84dfa73224a 100644 --- a/test/testimportproject.cpp +++ b/test/testimportproject.cpp @@ -86,6 +86,7 @@ class TestImportProject : public TestFixture { TEST_CASE(testCollectArgs6); TEST_CASE(testCollectArgs7); TEST_CASE(testVcxprojConditions); + TEST_CASE(testMSBuildStaticFunctions); } void setDefines() const { @@ -107,9 +108,9 @@ class TestImportProject : public TestFixture { void setIncludePaths1() const { FileSettings fs{"test.cpp", Standards::Language::CPP, 0}; std::list in(1, "../include"); - VariablesMap variables; + PropertiesMap properties; TestImporter importer; - importer.fsSetIncludePaths(fs, "abc/def/", in, variables); + importer.fsSetIncludePaths(fs, "abc/def/", in, properties); ASSERT_EQUALS(1U, fs.includePaths.size()); ASSERT_EQUALS("abc/include/", fs.includePaths.front()); } @@ -117,10 +118,10 @@ class TestImportProject : public TestFixture { void setIncludePaths2() const { FileSettings fs{"test.cpp", Standards::Language::CPP, 0}; std::list in(1, "$(SolutionDir)other"); - VariablesMap variables; - variables["SolutionDir"] = "c:/abc/"; + PropertiesMap properties; + properties["SolutionDir"] = "c:/abc/"; TestImporter importer; - importer.fsSetIncludePaths(fs, "/home/fred", in, variables); + importer.fsSetIncludePaths(fs, "/home/fred", in, properties); ASSERT_EQUALS(1U, fs.includePaths.size()); ASSERT_EQUALS("c:/abc/other/", fs.includePaths.front()); } @@ -128,10 +129,10 @@ class TestImportProject : public TestFixture { void setIncludePaths3() const { // macro names are case insensitive FileSettings fs{"test.cpp", Standards::Language::CPP, 0}; std::list in(1, "$(SOLUTIONDIR)other"); - VariablesMap variables; - variables["SolutionDir"] = "c:/abc/"; + PropertiesMap properties; + properties["SolutionDir"] = "c:/abc/"; TestImporter importer; - importer.fsSetIncludePaths(fs, "/home/fred", in, variables); + importer.fsSetIncludePaths(fs, "/home/fred", in, properties); ASSERT_EQUALS(1U, fs.includePaths.size()); ASSERT_EQUALS("c:/abc/other/", fs.includePaths.front()); } @@ -720,6 +721,50 @@ class TestImportProject : public TestFixture { ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'13.0' >= '14.0'", "", "")); ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.10.0.0' > '1.9.0.0'", "", "")); ASSERT(cppcheck::testing::evaluateVcxprojCondition("'v14.0' >= '14.0'", "", "")); + // Version comparison: full 4-part #.#.#.# + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4' == '1.2.3.4'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4' == '1.2.3.5'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.5' > '1.2.3.4'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4' < '1.2.3.5'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'2.0.0.0' > '1.9.9.9'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1.9.9.9' > '2.0.0.0'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4' >= '1.2.3.4'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4' <= '1.2.3.4'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4' > '1.2.3.4'", "", "")); + // Version comparison: more than 4 parts (no truncation) + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4.5' > '1.2.3.4.4'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4.4' > '1.2.3.4.5'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4.0' == '1.2.3.4'", "", "")); + // Version comparison: missing trailing components treated as 0 + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17' == '17.0.0.0'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'17' != '17.0.0.0'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17' >= '17.0.0.0'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17' <= '17.0.0.0'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'17' > '17.0.0.0'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'17' < '17.0.0.0'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17.0' == '17'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17.0' == '17.0.0.0'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17.0.0' == '17'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17.1' > '17'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17.1' > '17.0.0.0'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'16.9' > '17'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1' < '1.0.0.1'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3' < '1.2.3.1'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3' > '1.2.2.9'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'2' > '1.9.9.9'", "", "")); + // 'Current' on LHS + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'Current' > '14'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'Current' >= '18'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'Current' < '14'", "", "")); + // 'Current' on RHS + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'14' < 'Current'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'18' <= 'Current'", "", "")); + ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'19' < 'Current'", "", "")); + // Static property functions in conditions + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$([MSBuild]::Add(1, 2)) == '3'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$([MSBuild]::EnsureTrailingSlash('foo')) == 'foo/'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$([System.String]::IsNullOrEmpty('')) == 'True'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$([System.Math]::Max(1, 2)) == '2'", "", "")); // Unknown variable ASSERT(cppcheck::testing::evaluateVcxprojCondition("'$(DoesNotExist)' == ''", "", "")); ASSERT(cppcheck::testing::evaluateVcxprojCondition("'$(PATH)' != ''", "", "")); @@ -738,8 +783,8 @@ class TestImportProject : public TestFixture { ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.ToLower()) == 'debug'", "Debug", "Win32")); ASSERT(cppcheck::testing::evaluateVcxprojCondition("$(Configuration.ToUpper()) == 'debug'", "Debug", "Win32")); ASSERT(!cppcheck::testing::evaluateVcxprojCondition("$(Configuration.ToUpper()) == 'RELEASE'", "Debug", "Win32")); - // invalid expression in => no error. We are ok with that as long as we don't crash - ASSERT(!cppcheck::testing::evaluateVcxprojCondition("' ' && ' '", "", "")); + // 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'", "", "")); @@ -813,6 +858,78 @@ class TestImportProject : public TestFixture { ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'0x10' == '16'", "", "")); } + void testMSBuildStaticFunctions() const { + // --- $([MSBuild]::...) arithmetic --- + ASSERT_EQUALS("3", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Add(1, 2))")); + ASSERT_EQUALS("5", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Subtract(8, 3))")); + ASSERT_EQUALS("12", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Multiply(3, 4))")); + ASSERT_EQUALS("3", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Divide(9, 3))")); + ASSERT_EQUALS("2", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Modulo(5, 3))")); + + // --- $([MSBuild]::...) path helpers --- + ASSERT_EQUALS("foo/", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::EnsureTrailingSlash('foo'))")); + ASSERT_EQUALS("foo/", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::EnsureTrailingSlash('foo/'))")); + // NormalizePath: join segments, normalise separators, resolve . and .. + // Use absolute first segments so results are deterministic (CWD-independent). + ASSERT_EQUALS("/a/b/c", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizePath('/a', 'b', 'c'))")); + ASSERT_EQUALS("/a/b/c", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizePath('/a\\b\\c'))")); + ASSERT_EQUALS("/a/c", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizePath('/a/b/../c'))")); + ASSERT_EQUALS("/a/b/c", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizePath('/a/b/./c'))")); + ASSERT_EQUALS("C:/a/c", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizePath('C:\\a\\b\\..\\c'))")); + ASSERT_EQUALS("C:/a/b/c", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizePath('C:\\a', 'b', 'c'))")); + // NormalizeDirectory: same as NormalizePath but always has a trailing slash + ASSERT_EQUALS("/a/b/c/", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizeDirectory('/a', 'b', 'c'))")); + ASSERT_EQUALS("/a/b/c/", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizeDirectory('/a\\b\\c'))")); + ASSERT_EQUALS("C:/a/b/c/", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::NormalizeDirectory('C:\\a', 'b', 'c'))")); + // ValueOrDefault: return first arg when non-empty, else second + ASSERT_EQUALS("x", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::ValueOrDefault('x', 'y'))")); + ASSERT_EQUALS("y", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::ValueOrDefault('', 'y'))")); + // GetCurrentToolsVersion + ASSERT_EQUALS("Current", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::GetCurrentToolsVersion())")); + + // --- $([MSBuild]::...) bitwise --- + ASSERT_EQUALS("2", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::BitwiseAnd(6, 3))")); + ASSERT_EQUALS("7", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::BitwiseOr(5, 3))")); + ASSERT_EQUALS("6", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::BitwiseXor(5, 3))")); + + // --- $([MSBuild]::...) Escape / Unescape --- + ASSERT_EQUALS("%3B", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Escape(';'))")); + ASSERT_EQUALS(";", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Unescape('%3B'))")); + ASSERT_EQUALS("%24", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Escape('$'))")); + ASSERT_EQUALS("$", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Unescape('%24'))")); + + // --- $([System.String]::...) --- + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([System.String]::IsNullOrEmpty(''))")); + ASSERT_EQUALS("False", cppcheck::testing::expandMSBuildExpression("$([System.String]::IsNullOrEmpty('x'))")); + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([System.String]::IsNullOrWhiteSpace(' '))")); + ASSERT_EQUALS("False", cppcheck::testing::expandMSBuildExpression("$([System.String]::IsNullOrWhiteSpace('x'))")); + ASSERT_EQUALS("ab", cppcheck::testing::expandMSBuildExpression("$([System.String]::Concat('a', 'b'))")); + ASSERT_EQUALS("a,b", cppcheck::testing::expandMSBuildExpression("$([System.String]::Join(',', 'a', 'b'))")); + + // --- $([System.Math]::...) --- + ASSERT_EQUALS("10", cppcheck::testing::expandMSBuildExpression("$([System.Math]::Max(5, 10))")); + ASSERT_EQUALS("5", cppcheck::testing::expandMSBuildExpression("$([System.Math]::Min(5, 10))")); + ASSERT_EQUALS("5", cppcheck::testing::expandMSBuildExpression("$([System.Math]::Abs(-5))")); + ASSERT_EQUALS("5", cppcheck::testing::expandMSBuildExpression("$([System.Math]::Abs(5))")); + ASSERT_EQUALS("2", cppcheck::testing::expandMSBuildExpression("$([System.Math]::Floor(2.9))")); + ASSERT_EQUALS("3", cppcheck::testing::expandMSBuildExpression("$([System.Math]::Ceiling(2.1))")); + + // --- $([System.IO.Path]::...) --- + ASSERT_EQUALS("bar.cpp", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::GetFileName('C:/foo/bar.cpp'))")); + ASSERT_EQUALS("bar", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::GetFileNameWithoutExtension('C:/foo/bar.cpp'))")); + ASSERT_EQUALS("C:/foo", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::GetDirectoryName('C:/foo/bar.cpp'))")); + ASSERT_EQUALS(".cpp", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::GetExtension('bar.cpp'))")); + ASSERT_EQUALS("True", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::IsPathRooted('C:/foo'))")); + ASSERT_EQUALS("False", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::IsPathRooted('foo'))")); + ASSERT_EQUALS("a/b", cppcheck::testing::expandMSBuildExpression("$([System.IO.Path]::Combine('a', 'b'))")); + + // --- Composite / nesting --- + ASSERT_EQUALS("6", cppcheck::testing::expandMSBuildExpression("$([MSBuild]::Add($([MSBuild]::Multiply(2, 2)), 2))")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$([MSBuild]::Add(1, 2)) == '3'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$([System.String]::IsNullOrEmpty('')) == 'True'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("$([System.Math]::Max(10, 5)) == '10'", "", "")); + } + // TODO: test fsParseCommand() }; From 588f9b30c051aced8e8b2457c1d5562130766591 Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Wed, 9 Sep 2026 10:48:36 -0400 Subject: [PATCH 11/14] use 3 passes to parse vcxproj files --- lib/importproject.cpp | 5089 ++++++++++++++++++++++++++---------- lib/importproject.h | 264 +- lib/platform.cpp | 22 + lib/platform.h | 3 + test/testimportproject.cpp | 706 ++++- 5 files changed, 4668 insertions(+), 1416 deletions(-) diff --git a/lib/importproject.cpp b/lib/importproject.cpp index 685fab19150..892601aa35e 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -38,10 +38,25 @@ #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" @@ -115,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; } } @@ -218,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); @@ -246,23 +317,57 @@ void ImportProject::fsSetDefines(FileSettings& fs, std::string defs) } if (!eq && !defs.empty()) defs += "=1"; + defs = msbuildUnescape(defs); fs.defines.swap(defs); } -// Find the ')' that matches the '(' at position parenPos, handling nested '$(' pairs. -static std::string::size_type findMatchingParen(const std::string &s, std::string::size_type parenPos) -{ +void ImportProject::addDebug(const std::string &msg) { + for (const auto &debug : debugs) { + // cppcheck-suppress useStlAlgorithm + if (debug == msg) + return; + } + debugs.emplace_back(msg); +} + +// 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) { - if (s.compare(i, 2, "$(") == 0) { + 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 (c == '\'' || c == '"') { + quote = c; + } else if (c == '(') { ++depth; - ++i; // skip the '(' on next iteration increment - } else if (s[i] == ')') { + } else if (c == ')') { + --depth; if (depth == 0) return i; - --depth; } } + return std::string::npos; } @@ -270,42 +375,44 @@ static std::string::size_type findMatchingParen(const std::string &s, std::strin // 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) -{ + const std::vector &args) { if (caseInsensitiveStringCompare(method, "ToUpper") == 0) { if (!args.empty()) throw std::runtime_error("ToUpper takes no arguments"); - std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { - return std::toupper(c); - }); + std::transform(value.cbegin(), value.cend(), value.begin(), static_cast(std::toupper)); return value; } if (caseInsensitiveStringCompare(method, "ToLower") == 0) { if (!args.empty()) throw std::runtime_error("ToLower takes no arguments"); - std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { - return std::tolower(c); - }); + std::transform(value.cbegin(), value.cend(), value.begin(), static_cast(std::tolower)); return value; } 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 (caseInsensitiveStringCompare(method, "StartsWith") == 0) { if (args.size() != 1) throw std::runtime_error("StartsWith requires one argument"); - return startsWith(value, args[0]) ? "True" : "False"; + // .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"); - return endsWith(value, args[0].c_str(), args[0].size()) ? "True" : "False"; + // .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) { @@ -384,20 +491,382 @@ static std::string applyPropertyMethod(std::string value, 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); + } + 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); + } + const std::size_t found = value.rfind(args[0], from); + return std::to_string(found == std::string::npos ? -1L : static_cast(found)); + } + + 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 + "'"); } +static std::string findFileAbove(const std::string &startDirectory, const std::string &file) +{ + // startDirectory comes from MSBuildThisFileDirectory which is already + // normalized to '/' separators by Path::simplifyPath. + std::string currentDir = startDirectory; + if (currentDir.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); + } + + return ""; +} + +/// 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; +} + +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 +} + +// 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; +} + +static std::string stripDirectoryPart(const std::string &filename) { + const auto slash = filename.rfind('/'); + return (slash != std::string::npos) ? filename.substr(slash + 1) : filename; +} + +/// 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()); +} + + +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; + } +} + +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; + } + 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; + }; + + for (const std::string &bp : basePaths) { + if (absolutePath == bp || bp.empty()) // Seems to be a file, or path is empty + continue; + + const std::string normBase = Path::fromNativeSeparators(bp); + + // 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); + } + + // 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; + } + // 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; +} + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wmissing-format-attribute" +#pragma clang diagnostic ignored "-Wformat-nonliteral" +#endif + +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 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. -static std::string applyMSBuildStaticFunction(const std::string &className, - const std::string &member, - const std::vector &args) -{ - const auto toInt = [](const std::string &s, long &out) -> bool { - if (s.empty()) return false; +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::strtol(s.c_str(), &end, 10); + out = std::strtoll(s.c_str(), &end, 10); return end != s.c_str() && *end == '\0'; }; @@ -424,7 +893,7 @@ static std::string applyMSBuildStaticFunction(const std::string &className, // Arithmetic: Add, Subtract, Multiply, Divide, Modulo if (args.size() == 2) { - long a = 0, b = 0; + 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); @@ -432,77 +901,109 @@ static std::string applyMSBuildStaticFunction(const std::string &className, return std::to_string(a - b); if (caseInsensitiveStringCompare(member, "Multiply") == 0) return std::to_string(a * b); - if (caseInsensitiveStringCompare(member, "Divide") == 0 && b != 0) + 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 && b != 0) + } + 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(base, path)) — approximate: return path unchanged + // $([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 args[1]; + 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]::NormalizePath(seg1[, seg2, ...])) — join segments (Path.Combine - // semantics: an absolute segment resets the accumulated path), normalize \ to /, - // and resolve . and .. components. The result is absolute only when the first - // evaluated segment is itself absolute; relative inputs stay relative. - if (caseInsensitiveStringCompare(member, "NormalizePath") == 0 && !args.empty()) { - // Join: an absolute segment resets the accumulated path (Path.Combine semantics). - std::string result = args[0]; - for (std::size_t i = 1; i < args.size(); ++i) { - const std::string &seg = args[i]; - const bool segAbsolute = !seg.empty() && - (seg[0] == '/' || seg[0] == '\\' || - (seg.size() >= 2 && std::isalpha(static_cast(seg[0])) && seg[1] == ':')); - if (segAbsolute) { - result = seg; - } else { - if (!result.empty() && result.back() != '/' && result.back() != '\\') - result += '/'; - result += seg; + // $([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 ""; } } - // Unify separators. - // cppcheck-suppress useStlAlgorithm - for (char &c : result) if (c == '\\') c = '/'; - // Extract drive-letter or leading-slash prefix. - std::string prefix; - std::size_t pos = 0; - if (result.size() >= 2 && std::isalpha(static_cast(result[0])) && result[1] == ':') { - prefix = result.substr(0, 2) + '/'; - pos = (result.size() > 2 && result[2] == '/') ? 3 : 2; - } else if (!result.empty() && result[0] == '/') { - prefix = "/"; - pos = 1; - } - // Resolve . and .. components. - std::vector parts; - while (pos < result.size()) { - const std::size_t slash = result.find('/', pos); - const std::string seg = result.substr(pos, slash == std::string::npos ? std::string::npos : slash - pos); - pos = (slash == std::string::npos) ? result.size() : slash + 1; - if (seg.empty() || seg == ".") - continue; - if (seg == "..") { - if (!parts.empty()) parts.pop_back(); - } else { - parts.push_back(seg); + std::string 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 ""; } } - std::string normalized = prefix; - for (std::size_t i = 0; i < parts.size(); ++i) { - if (i > 0) normalized += '/'; - normalized += parts[i]; + const PathKind resultKind = classifyPath(result); + if (resultKind == PathKind::DriveRelative) { + addDebug("NormalizePath: drive-relative path cannot be resolved: " + result); + return ""; } - return normalized; + // 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 + // $([MSBuild]::NormalizeDirectory(seg1[, seg2, ...])) -- same as NormalizePath // but always returns a path with a trailing slash. - if (caseInsensitiveStringCompare(member, "NormalizeDirectory") == 0 && !args.empty()) { + 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() != '/') @@ -518,13 +1019,67 @@ static std::string applyMSBuildStaticFunction(const std::string &className, s += '/'; return s; } - // $([MSBuild]::GetTargetPlatformVersion(version)) — pass through + // $([MSBuild]::GetTargetPlatformVersion(version)) -- pass through if (caseInsensitiveStringCompare(member, "GetTargetPlatformVersion") == 0) return args[0]; - // filesystem searches — not feasible during import - if (caseInsensitiveStringCompare(member, "GetDirectoryNameOfFileAbove") == 0 || - caseInsensitiveStringCompare(member, "GetPathOfFileAbove") == 0) - return ""; + } + + // $([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()) { @@ -541,50 +1096,124 @@ static std::string applyMSBuildStaticFunction(const std::string &className, } // $([System.Environment]::GetFolderPath(SpecialFolder.X)) if (caseInsensitiveStringCompare(member, "GetFolderPath") == 0 && args.size() == 1) { - const char *pf = std::getenv("ProgramFiles"); - if ((caseInsensitiveStringCompare(args[0], "ProgramFiles") == 0 || - caseInsensitiveStringCompare(args[0], "ProgramFilesX86") == 0) && pf) - return pf; + 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) { - if (caseInsensitiveStringCompare(member, "GetFileName") == 0) { - const auto slash = args[0].find_last_of("/\\"); - return slash != std::string::npos ? args[0].substr(slash + 1) : args[0]; - } - if (caseInsensitiveStringCompare(member, "GetFileNameWithoutExtension") == 0) { - const auto slash = args[0].find_last_of("/\\"); - std::string name = slash != std::string::npos ? args[0].substr(slash + 1) : args[0]; - const auto dot = name.rfind('.'); - return dot != std::string::npos ? name.substr(0, dot) : name; - } + 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) { - const auto slash = args[0].find_last_of("/\\"); - return slash != std::string::npos ? args[0].substr(0, slash) : ""; + std::string path = Path::getPathFromFilename(filename); + if (!path.empty() && path.back() == '/') + path.pop_back(); + return path; } - if (caseInsensitiveStringCompare(member, "GetExtension") == 0) { - const auto dot = args[0].rfind('.'); - return dot != std::string::npos ? args[0].substr(dot) : ""; + 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 (caseInsensitiveStringCompare(member, "IsPathRooted") == 0) { - const std::string &p = args[0]; - const bool rooted = !p.empty() && - (p[0] == '/' || p[0] == '\\' || - (p.size() >= 2 && - std::isalpha(static_cast(p[0])) && - p[1] == ':')); - return rooted ? "True" : "False"; + } + if (!args.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 && caseInsensitiveStringCompare(member, "Combine") == 0) { - if (Path::isAbsolute(args[1])) - return args[1]; - const std::string sep = - (!args[0].empty() && args[0].back() != '/' && args[0].back() != '\\') ? "/" : ""; - return args[0] + sep + args[1]; + if (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); + } + } + } } } @@ -592,33 +1221,151 @@ static std::string applyMSBuildStaticFunction(const std::string &className, 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]) + for (const char c : args[0]) { // cppcheck-suppress useStlAlgorithm - if (!std::isspace(static_cast(c))) return "False"; + 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; + 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]; + if (i > 1) + result += args[0]; result += args[i]; } return result; } - // Format — very rough: replace {0},{1},... with positional args + // Format: replace {n} and {n:spec} placeholders; {{ and }} are literal braces. if (caseInsensitiveStringCompare(member, "Format") == 0 && !args.empty()) { - std::string result = args[0]; - for (std::size_t i = 1; i < args.size(); ++i) { - const std::string placeholder = "{" + std::to_string(i - 1) + "}"; - std::size_t pos = 0; - while ((pos = result.find(placeholder, pos)) != std::string::npos) { - result.replace(pos, placeholder.size(), args[i]); - pos += args[i].size(); + // 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; @@ -627,7 +1374,8 @@ static std::string applyMSBuildStaticFunction(const std::string &className, if (caseInsensitiveStringCompare(className, "System.Math") == 0) { const auto toDouble = [](const std::string &s, double &out) -> bool { - if (s.empty()) return false; + if (s.empty()) + return false; char *end = nullptr; out = std::strtod(s.c_str(), &end); return end != s.c_str() && *end == '\0'; @@ -646,11 +1394,25 @@ static std::string applyMSBuildStaticFunction(const std::string &className, if (caseInsensitiveStringCompare(member, "Abs") == 0) return fmtDouble(x < 0 ? -x : x); if (caseInsensitiveStringCompare(member, "Floor") == 0) - return std::to_string(static_cast(x >= 0 ? x : x - 1)); + return fmtDouble(std::floor(x)); if (caseInsensitiveStringCompare(member, "Ceiling") == 0) - return std::to_string(static_cast(x <= 0 ? x : x + 1)); - if (caseInsensitiveStringCompare(member, "Round") == 0) - return std::to_string(static_cast(x >= 0 ? x + 0.5 : x - 0.5)); + 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) @@ -672,46 +1434,15 @@ static std::string applyMSBuildStaticFunction(const std::string &className, } } - // $([MSBuild]::Escape / Unescape) — encode/decode MSBuild special chars as %XX + // $([MSBuild]::Escape / Unescape) -- encode/decode MSBuild special chars as %XX if (caseInsensitiveStringCompare(className, "MSBuild") == 0 && args.size() == 1) { - if (caseInsensitiveStringCompare(member, "Escape") == 0) { - static const char special[] = "%$@';?*!"; - std::string result; - for (const unsigned char c : args[0]) { - if (std::strchr(special, static_cast(c))) { - const char hex[] = "0123456789ABCDEF"; - result += '%'; - result += hex[(c >> 4) & 0xF]; - result += hex[c & 0xF]; - } else { - result += static_cast(c); - } - } - return result; - } - if (caseInsensitiveStringCompare(member, "Unescape") == 0) { - std::string result; - const std::string &s = args[0]; - for (std::size_t i = 0; i < s.size(); ++i) { - if (s[i] == '%' && i + 2 < s.size() && - std::isxdigit(static_cast(s[i + 1])) && - std::isxdigit(static_cast(s[i + 2]))) { - const auto nibble = [](char c) -> unsigned char { - if (c >= '0' && c <= '9') return static_cast(c - '0'); - if (c >= 'a' && c <= 'f') return static_cast(c - 'a' + 10); - return static_cast(c - 'A' + 10); - }; - result += static_cast((nibble(s[i + 1]) << 4) | nibble(s[i + 2])); - i += 2; - } else { - result += s[i]; - } - } - return result; - } + if (caseInsensitiveStringCompare(member, "Escape") == 0) + return msbuildEscape(args[0]); + if (caseInsensitiveStringCompare(member, "Unescape") == 0) + return msbuildUnescape(args[0]); // Bitwise operations { - long a = 0; + long long a = 0; if (toInt(args[0], a)) { if (caseInsensitiveStringCompare(member, "BitwiseNot") == 0) return std::to_string(~a); @@ -720,7 +1451,7 @@ static std::string applyMSBuildStaticFunction(const std::string &className, } if (caseInsensitiveStringCompare(className, "MSBuild") == 0 && args.size() == 2) { - long a = 0, b = 0; + 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); @@ -745,28 +1476,64 @@ static std::string applyMSBuildStaticFunction(const std::string &className, caseInsensitiveStringCompare(member, "IsOSPlatform") == 0 && args.size() == 1) return applyMSBuildStaticFunction("MSBuild", "IsOSPlatform", args); - // $([System.Runtime.InteropServices.OSPlatform]::Windows|Linux|OSX) — static property + // $([System.Runtime.InteropServices.OSPlatform]::Windows|Linux|OSX) -- static property if (caseInsensitiveStringCompare(className, "System.Runtime.InteropServices.OSPlatform") == 0) return member; // return the platform name ("Windows", "Linux", "OSX") as a string - // Unknown class or method — return empty so import continues + // $([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 ""; } -// Expands $(Name) and $(Name.Method(args)) references in property value strings. -// Unknown variables are left unexpanded. Use expandPropertyValue() to invoke. -struct PropertyValueExpander { - const PropertiesMap &mVars; +// 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 mChanged{false}; - bool mReplaceUnknown{false}; // if true, unknown variables expand to "" + bool mUnknownAsEmpty{ false }; - PropertyValueExpander(const PropertiesMap &vars, std::string str) - : mVars(vars), mStr(std::move(str)) {} + PropertyValueExpander(ImportProject &project, const PropertiesMap &vars, std::string str, bool unknownAsEmpty = false) + : mProject(project), mVars(vars), mStr(std::move(str)), mUnknownAsEmpty(unknownAsEmpty) {} - bool isKnown(const std::string &name) const { - if (mVars.count(name)) return true; + bool hasValue(const std::string &name) const { + if (mVars.count(name)) + return true; return std::getenv(name.c_str()) != nullptr; } @@ -778,7 +1545,15 @@ struct PropertyValueExpander { return env ? env : std::string(); } - // Parses an identifier, handling nested $(...) within the name. + // 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()) { @@ -787,82 +1562,140 @@ struct PropertyValueExpander { continue; } const auto c = static_cast(mStr[mPos]); - if (!std::isalnum(c) && c != '_' && c != '-') break; + if (!std::isalnum(c) && c != '_' && c != '-') + break; result += mStr[mPos++]; } return result; } - // Parses one method argument: a quoted string literal or a $(…) reference. + // 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; - while (mPos < mStr.size() && mStr[mPos] != '\'') + // 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++]; - if (mPos < mStr.size()) ++mPos; // consume closing '\'' return s; } - if (mStr.compare(mPos, 2, "$(") == 0) - return tryParseExpr(); - // Bare word — consume until ',' or ')'. + // Bare word -- consume until ',' or ')'. std::string s; while (mPos < mStr.size() && mStr[mPos] != ',' && mStr[mPos] != ')') s += mStr[mPos++]; return s; } - // Parses and evaluates $(Name[.Method(args)…]) starting at mPos. + // 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. - // If the variable is unknown the token is left unchanged and mPos advances past it. + // 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 + // $([ClassName]::Method(args)) -- static property function if (mPos < mStr.size() && mStr[mPos] == '[') { - ++mPos; // skip '[' - std::string className; - while (mPos < mStr.size() && mStr[mPos] != ']') - className += mStr[mPos++]; - if (mPos < mStr.size()) ++mPos; // skip ']' - if (mPos + 1 < mStr.size() && mStr[mPos] == ':' && mStr[mPos + 1] == ':') - mPos += 2; // skip '::' - std::string member; - while (mPos < mStr.size()) { - const auto c = static_cast(mStr[mPos]); - if (!std::isalnum(c) && c != '_') break; - member += mStr[mPos++]; - } + std::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 '(' - 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; + 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()) ++mPos; // skip inner ')' } - if (mPos < mStr.size() && mStr[mPos] == ')') ++mPos; // skip outer ')' - mChanged = true; - return applyMSBuildStaticFunction(className, member, args); + if (mPos < mStr.size() && mStr[mPos] == ')') // skip outer ')' + ++mPos; + return value; } const std::string name = parseIdentifier(); - if (name.empty() || !isKnown(name)) { + if (name.empty() || !hasValue(name)) { const std::size_t end = findMatchingParen(mStr, start + 2); mPos = (end != std::string::npos) ? end + 1 : mStr.size(); - if (mReplaceUnknown) { - mChanged = true; - return std::string(); - } - return mStr.substr(start, mPos - start); + return mUnknownAsEmpty ? std::string() : mStr.substr(start, mPos - start); } - mChanged = true; + std::string value = lookup(name); // Parse optional .Method(args) chain. while (mPos < mStr.size() && mStr[mPos] == '.') { @@ -870,49 +1703,50 @@ struct PropertyValueExpander { std::string method; while (mPos < mStr.size()) { const auto c = static_cast(mStr[mPos]); - if (!std::isalnum(c) && c != '_') break; + if (!std::isalnum(c) && c != '_') + break; method += mStr[mPos++]; } - if (mPos >= mStr.size() || mStr[mPos] != '(') break; + 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; - 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; + 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()) ++mPos; // skip ')' - try { value = applyPropertyMethod(value, method, args); } catch (...) {} } - if (mPos < mStr.size() && mStr[mPos] == ')') ++mPos; // skip closing ')' + if (mPos < mStr.size() && mStr[mPos] == ')') // skip closing ')' + ++mPos; return value; } - // Expand all property expressions in mStr, multi-pass (capped at 50). + // 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() { - const int maxPasses = 50; - for (int pass = 0; pass < maxPasses; ++pass) { - mChanged = false; - mPos = 0; - std::string result; - result.reserve(mStr.size()); - while (mPos < mStr.size()) { - if (mStr.compare(mPos, 2, "$(") == 0) - result += tryParseExpr(); - else - result += mStr[mPos++]; - } - mStr = std::move(result); - if (!mChanged) break; + 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 mStr; + return result; } }; -static void expandMSBuildVariables(std::string &s, PropertiesMap &properties) +void ImportProject::expandMSBuildVariables(std::string &s, const PropertiesMap &properties) { - PropertyValueExpander expander{properties, s}; + PropertyValueExpander expander{*this, properties, s}; s = expander.expand(); } @@ -930,7 +1764,7 @@ ImportProject::Type ImportProject::import(const std::string &filename, Settings settings ? settings->fileFilters : std::vector(); if (endsWith(filename, ".json")) { - if (importCompileCommands(fin)) { + if (processCompileCommands(fin)) { setRelativePaths(filename); return ImportProject::Type::COMPILE_DB; } @@ -966,7 +1800,7 @@ ImportProject::Type ImportProject::import(const std::string &filename, Settings return ImportProject::Type::FAILURE; } -bool ImportProject::importCompileCommands(std::istream &istr) +bool ImportProject::processCompileCommands(std::istream &istr) { picojson::value compileCommands; istr >> compileCommands; @@ -1070,74 +1904,48 @@ void ImportProject::setSolution(const std::string &filename, PropertiesMap &prop properties["SolutionDir"] = Path::getPathFromFilename(absolutePath); properties["SolutionExt"] = Path::getFilenameExtensionInLowerCase(absolutePath); properties["SolutionPath"] = absolutePath; - - // Path::stripDirectoryPart doesn't work on windows with unix paths - // absolutePath is already normalized to '/' by toAbsolute() - const auto slash = absolutePath.rfind('/'); - properties["SolutionFileName"] = (slash != std::string::npos) ? absolutePath.substr(slash + 1) : absolutePath; - - std::string temp = properties["SolutionFileName"]; - findAndReplace(temp, Path::getFilenameExtension(temp), ""); - properties["SolutionName"] = temp; -} - -static std::string findFile(const std::string &startDirectory, const std::string &file) -{ - // startDirectory comes from MSBuildThisFileDirectory which is already - // normalized to '/' separators by Path::simplifyPath. - std::string currentDir = startDirectory; - if (currentDir.back() == '/' && currentDir.size() > 1 && currentDir[currentDir.size() - 2] != ':') - currentDir.pop_back(); - - while (!currentDir.empty()) { - std::string targetFile = Path::join(currentDir, file); - if (Path::isFile(targetFile)) - return targetFile; - if (currentDir.back() == '/' || (currentDir.back() == ':' && currentDir.size() == 2)) - break; - size_t lastSlash = currentDir.rfind('/'); - if (lastSlash == std::string::npos) - break; - currentDir.resize(lastSlash); - } - - return ""; -} - -bool ImportProject::importDirectorySolutionProps(PropertiesMap &properties) -{ - const std::string directorySolutionProps = findFile(properties["ProjectDir"], "Directory.Solution.props"); - if (!directorySolutionProps.empty()) { - MetadataMap data; - std::unordered_set stack; - std::list projectConfigurationList; - const ImportResult result = importPropsOrTargets(directorySolutionProps, properties, data, projectConfigurationList, stack); - if (result > ImportResult::NotResolvable) { - errors.emplace_back("Could not import \"" + directorySolutionProps + "\" - " + importResultStr(result)); - return false; - } - } - return true; + properties["SolutionFileName"] = stripDirectoryPart(absolutePath); + properties["SolutionName"] = fileStem(properties["SolutionFileName"]); } bool ImportProject::importSln(std::istream &istr, const std::string &filename, const std::vector &fileFilters) { - PropertiesMap mVariables; 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; } - - if (!startsWith(line, "Microsoft Visual Studio Solution File")) { - // Skip BOM - if (!std::getline(istr, line) || !startsWith(line, "Microsoft Visual Studio Solution File")) { + 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; @@ -1147,54 +1955,63 @@ bool ImportProject::importSln(std::istream &istr, const std::string &filename, c const std::string solutionDir = solutionVariables["SolutionDir"]; - bool found = false; + // 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 = ")) { - const std::string ver = line.substr(std::strlen("VisualStudioVersion = ")); - const std::string::size_type dot = ver.find('.'); - const std::string::size_type dot2 = (dot != std::string::npos) ? ver.find('.', dot + 1) : std::string::npos; - solutionVariables["VisualStudioVersion"] = (dot2 != std::string::npos) ? ver.substr(0, dot2) : ver; + solutionVariables["VisualStudioVersion"] = line.substr(std::strlen("VisualStudioVersion = ")); continue; } if (startsWith(line, "MinimumVisualStudioVersion = ")) { - const std::string ver = line.substr(std::strlen("MinimumVisualStudioVersion = ")); - const std::string::size_type dot = ver.find('.'); - const std::string::size_type dot2 = (dot != std::string::npos) ? ver.find('.', dot + 1) : std::string::npos; - solutionVariables["MinimumVisualStudioVersion"] = (dot2 != std::string::npos) ? ver.substr(0, dot2) : ver; + solutionVariables["MinimumVisualStudioVersion"] = line.substr(std::strlen("MinimumVisualStudioVersion = ")); continue; } if (!startsWith(line,"Project(")) continue; - const std::string::size_type pos = line.find(".vcxproj"); - if (pos == std::string::npos) + // 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; - const std::string::size_type pos1 = line.rfind('\"',pos); - if (pos1 == std::string::npos) + std::string vcxproj = line.substr(searchStart + 1, pathEnd - searchStart - 1); + if (Path::getFilenameExtensionInLowerCase(vcxproj) != ".vcxproj") continue; - std::string vcxproj(line.substr(pos1+1, pos-pos1+7)); 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; + } - mVariables = solutionVariables; + 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; } - found = true; - } - - if (!found) { - errors.emplace_back("no projects found in Visual Studio solution file"); - return false; } - return importDirectorySolutionProps(mVariables); + return true; } bool ImportProject::importSlnx(const std::string& filename, const std::vector& fileFilters) { - PropertiesMap mVariables; debugs.clear(); tinyxml2::XMLDocument doc; @@ -1237,7 +2054,7 @@ bool ImportProject::importSlnx(const std::string& filename, const std::vector limit) ? limit : dot; - throw std::runtime_error("Invalid condition: '" + mCondition + "'"); - } + // Rule 4a: reject empty components (e.g. "17..0" or trailing dot). + if (end == pos) + return MSBuildVersion(); - if (value != "True" && value != "False") - throw std::runtime_error("Invalid condition: '" + mCondition + "'"); + // Rule 4b: reject leading whitespace -- strtol silently skips it. + if (std::isspace(static_cast(s[pos]))) + return MSBuildVersion(); - return value == "True"; - } + // 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); - private: - const std::string &mCondition; - const PropertiesMap &mVariables; - std::size_t mPos = 0; - bool mEvaluate = true; // false while parsing a short-circuited operand + // Rule 4c: all characters must have been consumed and the value non-negative. + if (endPtr == part.c_str() || *endPtr != '\0' || value < 0) + return MSBuildVersion(); - void skipWhitespace() { - while (mPos < mCondition.size() && std::isspace(static_cast(mCondition[mPos]))) - ++mPos; - } + ver.mComponents.push_back(static_cast(value)); - bool match(const std::string &text) { - skipWhitespace(); - if (mCondition.compare(mPos, text.size(), text) != 0) - return false; - mPos += text.size(); - return true; + if (dot == std::string::npos || dot >= limit) + break; + pos = dot + 1; } - bool matchWord(const std::string &word) { - skipWhitespace(); - if (mCondition.size() - mPos < word.size()) - return false; - if (caseInsensitiveStringCompare(mCondition.substr(mPos, word.size()), word) != 0) - return false; + if (ver.mComponents.empty()) + return MSBuildVersion(); - const std::size_t end = mPos + word.size(); - if (end < mCondition.size() && - (std::isalnum(static_cast(mCondition[end])) || mCondition[end] == '_')) - return false; + return ver; + } - mPos = end; - return true; - } + /** True when this instance could not be parsed or was default-constructed. */ + bool empty() const { + return mComponents.empty(); + } - void expect(const std::string &text) { - if (match(text)) - return; + /** 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; + } - if (text == ")") - throw std::runtime_error("'(' without closing ')'!"); + 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; + } - throw std::runtime_error("Expected '" + text + "' in condition '" + mCondition + "'"); - } + // 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()); - 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"; + // 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 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; + return op == "==" ? equal : !equal; } - std::string parseUnary() { - if (match("!")) { - skipWhitespace(); - if (mPos == mCondition.size()) - throw std::runtime_error("Invalid condition: '" + mCondition + "'"); + if (op == "<") + return *this < rhs; + if (op == ">") + return *this > rhs; + if (op == "<=") + return *this <= rhs; + if (op == ">=") + return *this >= rhs; - return parseUnary() == "False" ? "True" : "False"; - } + return false; + } - return parsePrimary(); + 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; + } - std::string parsePrimary() { - skipWhitespace(); +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; + } +}; - if (match("(")) { - std::string value = parseOr(); - expect(")"); - return value; - } +// 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) {} - if (matchWord("Exists")) - return parseExists(); + bool parse() { + const std::string value = parseOr(); - if (matchWord("And") || matchWord("Or") || match("!")) - throw std::runtime_error("Invalid condition: '" + mCondition + "'"); + skipWhitespace(); - if (matchWord("HasTrailingSlash")) - return parseHasTrailingSlash(); + if (mPos != mCondition.size()) { + if (mCondition[mPos] == ')') + throw std::runtime_error("unmatched ')' in condition " + mCondition); - return parseComparison(); + throw std::runtime_error("Invalid condition: '" + mCondition + "'"); } - std::string parseComparison() { - const std::string lhs = parseValue(); - skipWhitespace(); - - static constexpr const char *ops[] = { "==", "!=", "<=", ">=", "<", ">" }; - for (const char *op : ops) { - if (match(op)) { - const std::string rhs = parseValue(); - if (!mEvaluate) return "False"; - return compare(lhs, op, rhs) ? "True" : "False"; - } - } - - return lhs; - } + if (value != "True" && value != "False") + throw std::runtime_error("Invalid condition: '" + mCondition + "'"); - std::string parseValue() { - skipWhitespace(); + return value == "True"; + } - if (mPos >= mCondition.size()) - throw std::runtime_error("Missing operator"); +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 - if (matchWord("true")) - return "True"; + void skipWhitespace() { + while (mPos < mCondition.size() && std::isspace(static_cast(mCondition[mPos]))) + ++mPos; + } - if (matchWord("false")) - return "False"; + bool match(const std::string &text) { + skipWhitespace(); + if (mCondition.compare(mPos, text.size(), text) != 0) + return false; + mPos += text.size(); + return true; + } - if (mCondition[mPos] == '\'') - return parseString(); + 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; + } - if (mCondition.compare(mPos, 2, "$(") == 0) - return parsePropertyExpression(); + const std::size_t end = mPos + word.size(); + if (end < mCondition.size() && + (std::isalnum(static_cast(mCondition[end])) || mCondition[end] == '_')) + return false; - 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++; + mPos = end; + return true; + } - while (mPos < mCondition.size() && std::isdigit(static_cast(mCondition[mPos]))) - ++mPos; + void expect(const std::string &text) { + if (match(text)) + return; - return mCondition.substr(begin, mPos - begin); - } + if (text == ")") + throw std::runtime_error("'(' without closing ')'!"); - 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; - } + throw std::runtime_error("Expected '" + text + "' in condition '" + mCondition + "'"); + } - 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 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 parseString() { - ++mPos; - std::string value; + 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; + } - while (mPos < mCondition.size()) { - const char c = mCondition[mPos++]; + std::string parseUnary() { + if (match("!") || matchWord("not")) { + if (mPos == mCondition.size()) + throw std::runtime_error("Invalid condition: '" + mCondition + "'"); + return parseUnary() == "False" ? "True" : "False"; + } - if (c == '\'') - return expandProperties(value); + return parsePrimary(); + } - value += c; - } + std::string parsePrimary() { + skipWhitespace(); - if (!mEvaluate) - return std::string(); - throw std::runtime_error("Can not tokenize condition"); + if (match("(")) { + std::string value = parseOr(); + expect(")"); + return value; } - static bool parseInteger(const std::string &s, long &value) - { - if (s.empty()) - return false; + if (matchWord("Exists")) + return parseExists(); - const char *begin = s.c_str(); - char *end = nullptr; - int base = 10; + if (matchWord("And") || matchWord("Or")) + throw std::runtime_error("Invalid condition: '" + mCondition + "'"); - if (s.size() > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { - begin += 2; - if (*begin == '\0') - return false; - base = 16; - } + if (matchWord("HasTrailingSlash")) + return parseHasTrailingSlash(); - value = std::strtol(begin, &end, base); - return end != begin && *end == '\0'; - } + return parseComparison(); + } - std::string parseIdentifier() { - skipWhitespace(); - std::string result; - while (mPos < mCondition.size()) { - if (mCondition.compare(mPos, 2, "$(") == 0) { - result += parsePropertyExpression(); - continue; - } - const auto c = static_cast(mCondition[mPos]); - if (!std::isalnum(c) && c != '_' && c != '-') - break; - result += mCondition[mPos++]; + 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"; } - if (result.empty()) - throw std::runtime_error("Expected identifier in condition '" + mCondition + "'"); - return result; } - std::string parsePropertyExpression() { - expect("$("); - - // $([ClassName]::Method(args)) — static property function - if (mPos < mCondition.size() && mCondition[mPos] == '[') { - ++mPos; // skip '[' - std::string className; - while (mPos < mCondition.size() && mCondition[mPos] != ']') - className += mCondition[mPos++]; - if (mPos < mCondition.size()) ++mPos; // skip ']' - if (mPos + 1 < mCondition.size() && mCondition[mPos] == ':' && mCondition[mPos + 1] == ':') - mPos += 2; // skip '::' - std::string member; - while (mPos < mCondition.size()) { - const auto c = static_cast(mCondition[mPos]); - if (!std::isalnum(c) && c != '_') break; - member += mCondition[mPos++]; - } - std::vector args; - if (mPos < mCondition.size() && mCondition[mPos] == '(') { - ++mPos; // skip '(' - skipWhitespace(); - if (!match(")")) { - do { - args.push_back(parseValue()); - skipWhitespace(); - } while (match(",")); - expect(")"); - } - } - expect(")"); // outer closing paren of $(...) - return mEvaluate ? applyMSBuildStaticFunction(className, member, args) : std::string(); - } - - std::string value = getPropertyValue(parseIdentifier()); + // 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; + } - while (true) { - skipWhitespace(); - if (!match(".")) - break; + std::string parseValue() { + skipWhitespace(); - const std::string method = parseIdentifier(); - expect("("); - std::vector args; - skipWhitespace(); - if (!match(")")) { - do { - args.push_back(parseValue()); - } while (match(",")); - expect(")"); - } - value = mEvaluate ? applyMethod(value, method, args) : std::string(); - } + if (mPos >= mCondition.size()) + throw std::runtime_error("Missing operator"); - expect(")"); - return value; - } + if (matchWord("true")) + return "True"; - std::string parseExists() { - expect("("); - const std::string filename = parseValue(); - expect(")"); + if (matchWord("false")) + return "False"; - std::string path = filename; - if (!Path::isAbsolute(path)) { - auto it = mVariables.find("MSBuildThisFileDirectory"); - if (it == mVariables.end()) - it = mVariables.find("ProjectDir"); - if (it != mVariables.end()) - path = it->second + path; + if (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 (Path::isFile(path) || Path::isDirectory(path)) ? "True" : "False"; + return mCondition.substr(begin, mPos - begin); } - std::string parseHasTrailingSlash() { - expect("("); - const std::string value = parseValue(); - expect(")"); - - return (!value.empty() && (value.back() == '/' || value.back() == '\\')) - ? "True" - : "False"; + 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; } - std::string getPropertyValue(const std::string &name) const { - const auto it = mVariables.find(name); - if (it != mVariables.end()) - return it->second; + 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) + "'"); + } - const char *envValue = std::getenv(name.c_str()); - return envValue ? envValue : ""; - } + std::string parseString() { + ++mPos; + std::string value; + + while (mPos < mCondition.size()) { + const char c = mCondition[mPos++]; + + if (c == '\'') + return expandProperties(value); - std::string expandProperties(const std::string &input) const { - // Delegate to PropertyValueExpander. In condition context unknown - // variables must expand to "" (MSBuild semantics for quoted strings). - PropertyValueExpander expander{mVariables, input}; - expander.mReplaceUnknown = true; - return expander.expand(); + value += c; } - static std::string applyMethod(std::string value, - const std::string &method, - const std::vector &args) { - return applyPropertyMethod(std::move(value), method, args); + 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; } - static int compareVersions(const std::vector &lhs, - const std::vector &rhs) { - const std::size_t count = std::max(lhs.size(), rhs.size()); + value = std::strtol(begin, &end, base); + return end != begin && *end == '\0'; + } - for (std::size_t i = 0; i < count; ++i) { - // Missing trailing components are treated as 0, - // so {17} == {17, 0, 0} and {17, 1} > {17, 0, 5} is correct. - const int l = (i < lhs.size()) ? lhs[i] : 0; - const int r = (i < rhs.size()) ? rhs[i] : 0; - if (l < r) - return -1; - if (l > r) - return 1; + // 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; + } - return 0; + // 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; + } - static bool compareVersionResult(int result, const std::string &op) { - if (op == "<") - return result < 0; - if (op == ">") - return result > 0; - if (op == "<=") - return result <= 0; - if (op == ">=") - return result >= 0; - return false; + std::string 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; } - static bool compare(const std::string &lhs, const std::string &op, const std::string &rhs) { - const auto parseVersion = [](const std::string &s) -> std::vector { - if (s.empty()) - return {}; + std::string value = getPropertyValue(parsePropertyName()); - std::size_t pos = (s[0] == 'v' || s[0] == 'V') ? 1 : 0; - if (pos == s.size()) - return {}; + while (true) { + skipWhitespace(); + if (!match(".")) + break; - std::vector parts; - while (pos < s.size()) { - const std::size_t dot = s.find('.', pos); - const std::size_t end = - dot == std::string::npos ? s.size() : dot; + 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(); + } - if (end == pos) - return {}; + expect(")"); + return value; + } - const std::string part = s.substr(pos, end - pos); - char *endPtr = nullptr; - const long value = std::strtol(part.c_str(), &endPtr, 10); + 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)); - if (endPtr != part.c_str() && *endPtr == '\0') - parts.push_back(static_cast(value)); - else - return {}; + return (Path::isFile(path) || Path::isDirectory(path)) ? "True" : "False"; + } - if (dot == std::string::npos) - break; + std::string parseHasTrailingSlash() { + expect("("); + const std::string value = parseValue(); + expect(")"); - pos = dot + 1; - } + return (!value.empty() && (value.back() == '/' || value.back() == '\\')) + ? "True" + : "False"; + } - if (parts.empty()) - return {}; + std::string getPropertyValue(const std::string &name) const { + const auto it = mVariables.find(name); + if (it != mVariables.end()) + return it->second; - return parts; - }; + const char *envValue = std::getenv(name.c_str()); + return envValue ? envValue : ""; + } - if (op == "==" || op == "!=") { - const bool strEqual = caseInsensitiveStringCompare(lhs, rhs) == 0; - if (!strEqual) { - // "17" and "17.0.0.0" represent the same version; try version comparison - const auto lv = parseVersion(lhs); - const auto rv = parseVersion(rhs); - if (!lv.empty() && !rv.empty()) { - const bool verEqual = compareVersions(lv, rv) == 0; - return (op == "==") ? verEqual : !verEqual; - } - } - return (op == "==") ? strEqual : !strEqual; - } + 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 auto rhsVersion = parseVersion(rhs); - if (!rhsVersion.empty()) { - const std::vector currentVersion{ 18 }; - return compareVersionResult(compareVersions(currentVersion, rhsVersion), op); - } + const MSBuildVersion rhsVersion = MSBuildVersion::parse(rhs); + if (!rhsVersion.empty()) + return currentVersion.compareOp(op, rhsVersion); } if (caseInsensitiveStringCompare(rhs, "Current") == 0) { - const auto lhsVersion = parseVersion(lhs); - if (!lhsVersion.empty()) { - const std::vector currentVersion{ 18 }; - return compareVersionResult(compareVersions(lhsVersion, currentVersion), op); - } + const MSBuildVersion lhsVersion = MSBuildVersion::parse(lhs); + if (!lhsVersion.empty()) + return lhsVersion.compareOp(op, currentVersion); } + } - long lhsInt = 0; - long rhsInt = 0; - if (parseInteger(lhs, lhsInt) && parseInteger(rhs, rhsInt)) { - if (op == "<") return lhsInt < rhsInt; - if (op == ">") return lhsInt > rhsInt; - if (op == "<=") return lhsInt <= rhsInt; - if (op == ">=") return lhsInt >= rhsInt; + // 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; + }; - const std::vector lhsVersion = parseVersion(lhs); - const std::vector rhsVersion = parseVersion(rhs); + 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 compareVersionResult(compareVersions(lhsVersion, rhsVersion), op); - - throw std::runtime_error("Cannot compare '" + lhs + "' and '" + rhs + "'"); + return lhsVersion.compareOp(op, rhsVersion); } - }; - bool evalCondition(const std::string &condition, const PropertiesMap &properties) { - try { - return ConditionParser(condition, properties).parse(); - } catch (const std::exception &) { - // malformed or unhandled condition syntax (e.g. property functions, - // unknown methods, bare .Property access) — treat as false so import continues - return false; + // Finally, == and != fall back to case-insensitive string comparison. + if (op == "==" || op == "!=") { + const bool equal = caseInsensitiveStringCompare(lhs, rhs) == 0; + return op == "==" ? equal : !equal; } - } - bool conditionIsTrue(const tinyxml2::XMLElement *node, const PropertiesMap &properties) { - const char *condAttr = node->Attribute("Condition"); - if (!condAttr) - return true; - return evalCondition(condAttr, properties); + throw std::runtime_error("Cannot compare '" + lhs + "' and '" + rhs + "'"); } +}; - bool hasName(const tinyxml2::XMLElement *node, const char *nodeName, const PropertiesMap &properties) { - const char *name = node->Name(); - if (!name || std::strcmp(nodeName, name) != 0) - return false; - return conditionIsTrue(node, properties); +bool 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 hasNameAndAttribute(const tinyxml2::XMLElement *node, const char *nodeName, const char *attrName, const PropertiesMap &properties) { - const char *name = node->Name(); - const char *attr = node->Attribute(attrName); - if (!name || !attr || std::strcmp(nodeName, name) != 0) +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; - return conditionIsTrue(node, properties); + } + const ImportGraph::Decision &decision = decisions[pos++]; + file = decision.file; + return decision.taken; } - bool hasNameAndLabel(const tinyxml2::XMLElement *node, const char *nodeName, const char *nodeAttr, const PropertiesMap &properties) { - const char *name = node->Name(); - const char *label = node->Attribute("Label"); - if (!name || !label || std::strcmp(nodeName, name) != 0 || std::strcmp(label, nodeAttr) != 0) + 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; - return conditionIsTrue(node, properties); + } + const ImportGraph::Decision &decision = decisions[pos++]; + branch = decision.branch; + return decision.taken; } - bool hasNameAndNotLabel(const tinyxml2::XMLElement *node, const char *nodeName, const char *nodeAttr, const PropertiesMap &properties) { - const char *name = node->Name(); - if (!name || std::strcmp(nodeName, name) != 0) - return false; - const char *label = node->Attribute("Label"); - if (label && std::strcmp(label, nodeAttr) == 0) - return false; - return conditionIsTrue(node, properties); + ImportGraph::Decision decision; + decision.taken = matched; + decision.branch = matched ? branch : 0; + decisions.push_back(std::move(decision)); + return matched; +} + +ImportProject::ImportResult ImportProject::attemptSyntheticImport(std::string file, + PropertiesMap &properties, + MetadataMap &metadata, + std::list &compileList, + std::list &projectConfigurationList, + std::unordered_set &importStack, + EvalPhase phase) +{ + 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); } - std::list toStringList(const std::string &s) - { + std::list toStringList(const std::string &s) { std::list ret; std::string::size_type pos1 = 0; std::string::size_type pos2; - while ((pos2 = s.find(';',pos1)) != std::string::npos) { - if (pos2 > pos1) - ret.push_back(s.substr(pos1, pos2-pos1)); + + 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()) - break; } - if (pos1 < s.size()) - ret.push_back(s.substr(pos1)); + + if (pos1 < s.size()) { + std::string piece = s.substr(pos1); + trimWhitespace(piece); + if (!piece.empty()) + ret.push_back(msbuildUnescape(piece)); + } + return ret; } + /// 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; + } + struct MSBuildThis { PropertiesMap &propertiesMap; std::string thisFile; @@ -1909,16 +3122,11 @@ namespace { // Normalize once so all subsequent path ops can assume '/' separators. const std::string nfilename = Path::simplifyPath(Path::fromNativeSeparators(filename)); properties["MSBuildThisFileFullPath"] = nfilename; - const auto slash1 = nfilename.rfind('/'); - std::string temp = (slash1 != std::string::npos) ? nfilename.substr(slash1 + 1) : nfilename; - properties["MSBuildThisFile"] = temp; - findAndReplace(temp, Path::getFilenameExtension(temp), ""); - properties["MSBuildThisFileName"] = temp; + const std::string thisFile = stripDirectoryPart(nfilename); + properties["MSBuildThisFile"] = thisFile; + properties["MSBuildThisFileName"] = fileStem(thisFile); properties["MSBuildThisFileDirectory"] = Path::getPathFromFilename(nfilename); - temp = Path::getPathFromFilename(nfilename); - std::string::size_type pos = temp.find('/', 0); - temp.erase(0, pos + 1); - properties["MSBuildThisFileDirectoryNoRoot"] = temp; + properties["MSBuildThisFileDirectoryNoRoot"] = stripPathRoot(Path::getPathFromFilename(nfilename)); properties["MSBuildThisFileExtension"] = Path::getFilenameExtensionInLowerCase(nfilename); } @@ -1932,6 +3140,22 @@ namespace { } }; + // 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; + + explicit DiscoveringGuard(bool &flag) : mFlag(flag) { + mFlag = true; + } + + ~DiscoveringGuard() { + mFlag = false; + } + }; + struct ImportStackGuard { std::unordered_set &mStack; std::string mKey; @@ -1943,28 +3167,107 @@ namespace { mStack.erase(mKey); } }; + + // 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)); - if (Path::isAbsolute(internal)) + switch (classifyPath(internal)) { + case PathKind::UNC: + case PathKind::DriveAbsolute: return Path::simplifyPath(internal); - return Path::simplifyPath(Path::getCurrentPath() + "/" + 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, PropertiesMap &properties) +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; - if (Path::isAbsolute(resolved)) + switch (classifyPath(resolved)) { + case PathKind::UNC: + case PathKind::DriveAbsolute: return Path::simplifyPath(resolved); - return Path::simplifyPath(baseDir + 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, PropertiesMap &properties) +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 '\\'. @@ -1979,7 +3282,7 @@ bool ImportProject::simplifyPathWithVariables(std::string &s, PropertiesMap &pro return true; } -void ImportProject::fsSetIncludePaths(FileSettings &fs, const std::string &basepath, const std::list &in, PropertiesMap &properties) +void ImportProject::fsSetIncludePaths(FileSettings &fs, const std::string &basepath, const std::list &in, const PropertiesMap &properties) { std::set found; // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) @@ -2005,9 +3308,22 @@ void ImportProject::fsSetIncludePaths(FileSettings &fs, const std::string &basep if (s.find("$(") == std::string::npos) { s = Path::simplifyPath(basepath + s); - } else { - if (!simplifyPathWithVariables(s, properties)) - continue; + } 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; @@ -2015,34 +3331,128 @@ void ImportProject::fsSetIncludePaths(FileSettings &fs, const std::string &basep } } +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 original = properties[eName]; - findAndReplace(text, "$(" + std::string(eName) + ")", original); + 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, PropertiesMap &properties, MetadataMap &metadata) { +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 original = metadata[eName]; - findAndReplace(text, "%(" + std::string(eName) + ")", original); + 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) { - const std::string::size_type end = text.find(')', pos); + 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); @@ -2051,42 +3461,100 @@ void ImportProject::addMetadata(const tinyxml2::XMLElement *node, PropertiesMap 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, PropertiesMap &properties, const MetadataMap &metadata, const std::string &original) { +std::string ImportProject::getMetadata(const tinyxml2::XMLElement *node, const PropertiesMap &properties, const MetadataMap &metadata, const std::string &original) { const char *eName = node->Name(); - const char *eText = node->GetText(); - if (!eName || !eText || !conditionIsTrue(node, properties)) + if (!eName || !conditionIsTrue(node, properties)) return original; - std::string text(Path::fromNativeSeparators(eText)); - findAndReplace(text, "%(" + std::string(eName) + ")", original); - { - std::string::size_type pos = 0; - while ((pos = text.find("%(", pos)) != std::string::npos) { - const std::string::size_type end = text.find(')', pos); - if (end == std::string::npos) break; - const std::string key = text.substr(pos + 2, end - pos - 2); - const auto it = metadata.find(key); - const std::string replacement = (it != metadata.end()) ? it->second : std::string(); - text.replace(pos, end - pos + 1, replacement); - pos += replacement.size(); + // 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 std::string ok("ok"); - static std::string notResolvable("Not Resolvable"); - static std::string notFound("Not Found"); - static std::string notValid("Not Valid"); - static std::string cycle("Cycle"); - static std::string unknown("Unknown"); + 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: @@ -2103,449 +3571,1008 @@ const std::string &ImportProject::importResultStr(ImportProject::ImportResult re return unknown; } -ImportProject::ImportResult ImportProject::importCompile(const tinyxml2::XMLElement *node, - const std::string &projectDir, - PropertiesMap &properties, - const MetadataMap &metadata, - std::list &compileList) { - const char *include = node->Attribute("Include"); - if (!include) - return ImportResult::NotFound; +void ImportProject::applyClCompileChild(const tinyxml2::XMLElement *e1, + const PropertiesMap &properties, + MetadataMap &metadata) +{ + const char *eName = e1->Name(); + if (!eName || !conditionIsTrue(e1, properties)) + return; - std::string toInclude = toAbsolute(include, projectDir, properties); - if (!Path::acceptFile(toInclude)) - return ImportResult::NotFound; + // 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); +} - ItemGroupClCompile compile(toInclude); - // a file with no override of its own inherits the ItemDefinitionGroup value outright - compile.metadata = metadata; - bool excludedFromBuild = false; +static void applyAdditionalOptions(MetadataMap &metadata) +{ + const std::string &additionalOptions = metadata["AdditionalOptions"]; + if (additionalOptions.empty()) + return; - for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { - const char *text = e1->GetText(); - if (!text) + 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; - if (hasName(e1, "ExcludedFromBuild", properties)) { - if (caseInsensitiveStringCompare(text, "true") == 0) { - excludedFromBuild = true; - break; + 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; } - } else if (hasName(e1, "AdditionalIncludeDirectories", properties)) { - auto &v = compile.metadata["AdditionalIncludeDirectories"]; - v = getMetadata(e1, properties, compile.metadata, v); - } else if (hasName(e1, "ForcedIncludeFiles", properties)) { - auto &v = compile.metadata["ForcedIncludeFiles"]; - v = getMetadata(e1, properties, compile.metadata, v); - } else if (hasName(e1, "PreprocessorDefinitions", properties)) { - auto &v = compile.metadata["PreprocessorDefinitions"]; - v = getMetadata(e1, properties, compile.metadata, v); - } else if (hasName(e1, "LanguageStandard", properties)) { - auto &v = compile.metadata["LanguageStandard"]; - v = getMetadata(e1, properties, compile.metadata, v); - } else if (hasName(e1, "AdditionalOptions", properties)) { - auto &v = compile.metadata["AdditionalOptions"]; - v = getMetadata(e1, properties, compile.metadata, v); - } else if (hasName(e1, "AdditionalUsingDirectories", properties)) { - auto &v = compile.metadata["AdditionalUsingDirectories"]; - v = getMetadata(e1, properties, compile.metadata, v); - } - } - - if (!compile.metadata["AdditionalOptions"].empty()) { - std::vector args; - std::string arg; - bool quoted = false; - const std::string &additionalOptions = compile.metadata["AdditionalOptions"]; - - for (std::size_t i = 0; i < additionalOptions.size(); ++i) { - const char c = additionalOptions[i]; - - if (c == '"') { - quoted = !quoted; - } else if (std::isspace(static_cast(c)) && !quoted) { - if (!arg.empty()) { - args.emplace_back(std::move(arg)); - arg.clear(); + } + + // 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); } - } else { - arg += c; + + if (!path.empty()) { + if (!metadata["AdditionalIncludeDirectories"].empty()) + metadata["AdditionalIncludeDirectories"] += ';'; + metadata["AdditionalIncludeDirectories"] += path; + } + continue; } } - if (!arg.empty()) - args.emplace_back(std::move(arg)); + // 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"; + } + } +} - for (std::size_t i = 0; i < args.size(); ++i) { - const std::string &option = args[i]; +// 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; +} - if (option.size() >= 2 && - (option[0] == '/' || option[0] == '-') && - (option[1] == 'D' || option[1] == 'd')) { +// 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::string define = option.substr(2); +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 + "'"); + } - // /D NAME - if (define.empty() && i + 1 < args.size()) - define = args[++i]; + 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()); + } - if (!define.empty()) { - if (!compile.metadata["PreprocessorDefinitions"].empty()) - compile.metadata["PreprocessorDefinitions"] += ';'; - compile.metadata["PreprocessorDefinitions"] += define; - } + // 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; - } else if (option.size() >= 2 && - (option[0] == '/' || option[0] == '-') && - (option[1] == 'I' || option[1] == 'i')) { + if (lo < hi) { + std::string trimmed = expandedSpec.substr(lo, hi - lo); - std::string path = option.substr(2); + // 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); + } - // /I path - if (path.empty() && i + 1 < args.size()) - path = args[++i]; + const std::string decoded = msbuildUnescape(trimmed); - if (!path.empty()) { - if (!compile.metadata["AdditionalIncludeDirectories"].empty()) - compile.metadata["AdditionalIncludeDirectories"] += ';'; - compile.metadata["AdditionalIncludeDirectories"] += path; - } - } else if (option == "/std:c++11" || option == "-std=c++11") { - compile.metadata["LanguageStandard"] = "stdcpp11"; - } else if (option == "/std:c++14" || option == "-std=c++14") { - compile.metadata["LanguageStandard"] = "stdcpp14"; - } else if (option == "/std:c++17" || option == "-std=c++17") { - compile.metadata["LanguageStandard"] = "stdcpp17"; - } else if (option == "/std:c++20" || option == "-std=c++20") { - compile.metadata["LanguageStandard"] = "stdcpp20"; - } else if (option == "/std:c++23" || option == "-std=c++23") { - compile.metadata["LanguageStandard"] = "stdcpp23"; - } else if (option == "/std:c++latest" || option == "-std=c++latest") { - compile.metadata["LanguageStandard"] = "stdcpplatest"; + // 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; - if (!excludedFromBuild) - compileList.emplace_back(compile); + for (auto it = compileList.begin(); it != compileList.end();) { + if (Path::sameFileName(removeItem.second, it->filename)) + it = compileList.erase(it); + else + ++it; + } +} + +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; } -ImportProject::ImportResult ImportProject::importProject(const tinyxml2::XMLElement *node, - const std::string &projectDir, - PropertiesMap &properties, - MetadataMap &metadata, - std::list &projectConfigurationList, - std::unordered_set &importStack) { +// 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(); +} + +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); - std::string extension = Path::getFilenameExtensionInLowerCase(file); - if (extension == ".props" || extension == ".targets") { + 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) + if (sdk) { + addDebug("Could not import \"" + file + "\" - " + " (Sdk not supported)"); return ImportResult::NotResolvable; + } - if (file.find("Microsoft.Cpp.targets") != std::string::npos) { - auto it = properties.find("ForceImportBeforeCppTargets"); - if (it != properties.end()) { - ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) { - errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); - return result; - } - } + // 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; + }; - if (file.find("$(") != std::string::npos) { - std::string directoryBuildTargets = findFile(projectDir, "Directory.Build.targets"); - if (!directoryBuildTargets.empty()) { - ImportResult result = importPropsOrTargets(directoryBuildTargets, properties, metadata, projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) { - errors.emplace_back("Could not import \"" + directoryBuildTargets + "\" - " + importResultStr(result)); - return result; - } - } - } else { - ImportResult result = importPropsOrTargets(file, properties, metadata, projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) { - errors.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); - return result; - } + 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; } - it = properties.find("ForceImportAfterCppTargets"); - if (it != properties.end()) { - ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) { - errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); - return result; - } + // 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); } + attemptSyntheticImport(propertyOrEmpty(properties, "ForceImportAfterCppTargets"), + properties, metadata, compileList, projectConfigurationList, importStack, phase); + return ImportResult::Ok; } - if (file.find("Microsoft.Cpp.Default.props") != std::string::npos) { - auto it = properties.find("ForceImportBeforeCppDefaultProps"); - if (it != properties.end()) { - ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) { - errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); - return result; - } + 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); } - if (file.find("$(") != std::string::npos) { - // $(Configuration) = Debug, $(ConfigurationType) = Application, $(ApplicationType) - } else { - ImportResult result = importPropsOrTargets(file, properties, metadata, projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) { - errors.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); - return result; + // 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; } } - it = properties.find("ForceImportAfterCppDefaultProps"); - if (it != properties.end()) { - ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) { - errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); - return result; - } - } + attemptSyntheticImport(propertyOrEmpty(properties, "ForceImportAfterCppDefaultProps"), + properties, metadata, compileList, projectConfigurationList, importStack, phase); return ImportResult::Ok; } - if (file.find("Microsoft.Cpp.props") != std::string::npos) { - // If ForceImportBeforeCppProps is already set (e.g. by the vcxproj itself), - // honour it now before anything else. - const bool hadForceImportBefore = properties.count("ForceImportBeforeCppProps") > 0; - std::string forceImportBeforeCppProps; - if (hadForceImportBefore) { - auto it = properties.find("ForceImportBeforeCppProps"); - forceImportBeforeCppProps = it->second; - ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) { - errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); - return result; - } - } - - if (file.find("$(") != std::string::npos) { - // $(Platform), $(PlatformToolset), $(TargetName), $(TargetExt), $(LanguageStandard) - properties["IntDir"] = "$(Platform)/$(Configuration)/"; - properties["OutDir"] = "$(SolutionDir)$(Platform)/$(Configuration)/"; - properties["GeneratedFilesDir"] = "$(IntDir)Generated Files/"; - - std::string directoryBuildProps = findFile(projectDir, "Directory.Build.props"); - if (!directoryBuildProps.empty()) { - ImportResult result = importPropsOrTargets(directoryBuildProps, properties, metadata, projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) { - errors.emplace_back("Could not import \"" + directoryBuildProps + "\" - " + importResultStr(result)); - return result; - } - } - - // Directory.Build.props may have newly set ForceImportBeforeCppProps - // (e.g. PowerToys sets it to Cpp.Build.props which defines ProjectConfigurations). - // Real MSBuild auto-imports Directory.Build.props before evaluating - // Microsoft.Cpp.props, so ForceImportBeforeCppProps set there must be - // honoured here. - auto it = properties.find("ForceImportBeforeCppProps"); - if (it != properties.end()) { - if (it->second != forceImportBeforeCppProps) { - ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) { - errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); - return result; - } - } - } - } else { - ImportResult result = importPropsOrTargets(file, properties, metadata, projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) { - errors.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); - return result; - } + 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/"); } - auto it = properties.find("ForceImportAfterCppProps"); - if (it != properties.end()) { - ImportResult result = importPropsOrTargets(it->second, properties, metadata, projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) { - errors.emplace_back("Could not import \"" + it->second + "\" - " + importResultStr(result)); - return result; - } - } + attemptSyntheticImport(propertyOrEmpty(properties, "ForceImportAfterCppProps"), + properties, metadata, compileList, projectConfigurationList, importStack, phase); return ImportResult::Ok; } - ImportResult result = importPropsOrTargets(file, properties, metadata, projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) { - errors.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); - return result; - } + 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) { - debugs.emplace_back("Could not import \"" + file + "\" - " + importResultStr(result)); + addDebug("Could not import \"" + file + "\" - " + importResultStr(result)); } } else { - debugs.emplace_back("Could not import \"" + file + "\" unsupported extension " + extension); + addDebug("Could not import \"" + file + "\" unsupported extension " + extension); } return ImportResult::Ok; } -ImportProject::ImportResult ImportProject::importPropsOrTargets(const std::string &file, - PropertiesMap &properties, - MetadataMap &metadata, - std::list &projectConfigurationList, - std::unordered_set &importStack) -{ - std::string filename(file); - // properties can't be resolved - if (!simplifyPathWithVariables(filename, properties)) - return ImportResult::NotResolvable; - - // prepend project dir (if it exists) to transform relative paths into absolute ones - if (!Path::isAbsolute(filename) && properties.count("ProjectDir") > 0) - filename = toAbsolute(filename, properties.at("ProjectDir"), properties); - - // detect circular property sheet imports (A imports B, B imports A, a file importing - // itself, ...) instead of recursing until the stack overflows - mirrors MSBuild's own - // import-cycle detection, which errors out rather than looping forever - const std::string simplifiedFilename = Path::simplifyPath(filename); - if (!importStack.insert(simplifiedFilename).second) - return ImportResult::Cycle; +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; +} - ImportStackGuard guard(importStack, simplifiedFilename); // erases on any exit from here +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; + } - tinyxml2::XMLDocument doc; - if (doc.LoadFile(filename.c_str()) != tinyxml2::XML_SUCCESS) - return ImportResult::NotFound; + 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; + } + } - const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement(); - if (rootnode == nullptr) - return ImportResult::NotValid; + if (!importGraphChooseDecision(matched, branch)) + return ImportResult::Ok; - MSBuildThis msBuildThis(filename, properties); - std::string propsDir = Path::getPathFromFilename(filename); + // 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; +} - ImportResult ret = ImportResult::Ok; - for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { - if (hasName(node, "ImportGroup", properties)) { - // Accept any (PropertySheets, Shared, unlabeled) — .targets files - // commonly use unlabeled or differently-labeled groups for transitive imports. - const char* label = node->Attribute("Label"); - const bool isPropertySheets = (label == nullptr) || - (std::strcmp(label, "PropertySheets") == 0) || - (std::strcmp(label, "Shared") == 0) || - (std::strcmp(label, "ExtensionSettings") == 0) || - (std::strcmp(label, "ExtensionTargets") == 0); - if (isPropertySheets) { - for (const tinyxml2::XMLElement *importGroup = node->FirstChildElement(); importGroup; importGroup = importGroup->NextSiblingElement()) { - if (hasNameAndAttribute(importGroup, "Import", "Project", properties)) { - ImportResult result = importProject(importGroup, propsDir, properties, metadata, projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) - return result; - } - } +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, "PropertyGroup", properties)) { - for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) - addProperty(e, properties); } else if (hasName(node, "ItemDefinitionGroup", properties)) { - for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { - if (hasName(e1, "ClCompile", properties)) { - for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) { - addMetadata(e2, properties, metadata); - } + // 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)) { - for (const tinyxml2::XMLElement *pcNode = node->FirstChildElement("ProjectConfiguration"); pcNode; pcNode = pcNode->NextSiblingElement("ProjectConfiguration")) { - const ProjectConfiguration pc(pcNode); - if (!pc.configuration.empty()) { - // Deduplicate: the same config can arrive again when Directory.Build.props / - // Cpp.Build.props is re-imported inside the per-config loop. - const bool already = std::any_of(projectConfigurationList.cbegin(), - projectConfigurationList.cend(), - [&pc](const ProjectConfiguration &existing) { - return existing.name == pc.name; - }); - if (!already) { - projectConfigurationList.emplace_back(pc); - mAllVSConfigs.insert(pc.configuration); - } + // 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; + + const bool alreadyPresent = std::any_of(projectConfigurationList.cbegin(), + projectConfigurationList.cend(), + [&pc](const ProjectConfiguration &existing) { + return existing.name == pc.name; + }); + + if (!alreadyPresent) { + projectConfigurationList.emplace_back(pc); + mAllVSConfigs.insert(pc.configuration); } } - } else if (hasNameAndAttribute(node, "Import", "Project", properties)) { - ImportResult result = importProject(node, propsDir, properties, metadata, projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) - return result; + } 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; + + 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); + } + } + } 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); + } + } 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); } } - return ret; + return result; } -ImportProject::ImportResult ImportProject::importVcxitems(const std::string &items, - PropertiesMap &properties, - MetadataMap &metadata, - std::list &compileList, - std::list &projectConfigurationList, - std::unordered_set &importStack) +// 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::string filename(items); - // properties can't be resolved + 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; + } + } + while (p < pattern.size() && pattern[p] == '*') + ++p; + return p == pattern.size(); +} + +// 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) +{ + 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 + +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; - const std::string simplifiedFilename = Path::simplifyPath(filename); - if (!importStack.insert(simplifiedFilename).second) + // prepend project dir (if it exists) to transform relative paths into absolute ones + // 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, simplifiedFilename); // erases on any exit from here + ImportStackGuard guard(importStack, key); + // A previously completed import is ignored. + if (mImportGraph.active && !mImportGraph.imported.insert(key).second) + return ImportResult::Ok; tinyxml2::XMLDocument doc; - const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); - if (error != tinyxml2::XML_SUCCESS) - return ImportResult::NotFound; - const tinyxml2::XMLElement *const rootnode = doc.FirstChildElement(); + 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 ImportResult::NotValid; - const std::string itemsDir = Path::simplifyPath(Path::getPathFromFilename(filename)); - MSBuildThis msBuildThis(filename, properties); + // 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); - for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { - if (hasName(node, "ItemGroup", properties)) { - for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (hasName(e, "ClCompile", properties)) { - importCompile(e, itemsDir, properties, metadata, compileList); - } - } - } else if (hasName(node, "PropertyGroup", properties)) { - for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) - addProperty(e, properties); - } else if (hasName(node, "ItemDefinitionGroup", properties)) { - for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { - if (hasName(e1, "ClCompile", properties)) { - for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) - addMetadata(e2, properties, metadata); - } - } - } else if (hasNameAndAttribute(node, "Import", "Project", properties)) { - const ImportResult result = importProject(node, itemsDir, properties, metadata,projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) - return result; - } - } + MSBuildThis msBuildThis(filename, properties); + std::string propsDir = Path::getPathFromFilename(filename); - return ImportResult::Ok; + return processElementChildren(rootnode, propsDir, properties, metadata, compileList, + projectConfigurationList, importStack, phase); } bool ImportProject::importVcxproj(const std::string &filename, @@ -2558,22 +4585,29 @@ 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; } - MetadataMap metadata; // Normalize separators once; callers typically pass toAbsolute() results // but normalize here as a safety net so all subsequent rfind('/') are correct. const std::string nfilename = Path::simplifyPath(Path::fromNativeSeparators(filename)); - properties.emplace("VisualStudioVersion", "17.0"); + // 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 auto projSlash = nfilename.rfind('/'); - std::string temp = (projSlash != std::string::npos) ? nfilename.substr(projSlash + 1) : nfilename; - properties["ProjectFileName"] = temp; - findAndReplace(temp, Path::getFilenameExtension(temp), ""); - properties["ProjectName"] = temp; - temp.resize(std::min(temp.size(), size_t(16))); - properties["ShortProjectName"] = temp; + 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); @@ -2595,12 +4629,27 @@ bool ImportProject::importVcxproj(const std::string &filename, 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::unordered_set importStack; + MetadataMap metadata; const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement(); if (rootnode == nullptr) { @@ -2627,37 +4676,169 @@ bool ImportProject::importVcxproj(const std::string &filename, } // Discovery pass: if no ProjectConfigurations were found inline in the vcxproj, walk - // its / nodes through importProject so that every MSBuild import - // mechanism (Directory.Build.props, ForceImportBeforeCppProps, etc.) is honoured - // generically — no special-casing of individual property names required. - // We also process nodes so that properties needed to resolve import - // paths are available. Stop as soon as configurations are found. + // 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. + // 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; - for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); - node && projectConfigurationList.empty(); - node = node->NextSiblingElement()) { - if (hasName(node, "PropertyGroup", discoverProps)) { - for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) - addProperty(e, discoverProps); - } else if (hasName(node, "ImportGroup", discoverProps)) { - for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e && projectConfigurationList.empty(); e = e->NextSiblingElement()) { - if (hasNameAndAttribute(e, "Import", "Project", discoverProps)) - importProject(e, projectDir, discoverProps, discoverMeta, projectConfigurationList, discoverStack); - } - } else if (hasNameAndAttribute(node, "Import", "Project", discoverProps)) { - importProject(node, projectDir, discoverProps, discoverMeta, projectConfigurationList, discoverStack); - } - } + + 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 &prop : valuesByProperty) { + if (prop.second.size() == 1) + mConfigInvariantProperties.insert(prop.first); + } + } + bool first = true; for (const ProjectConfiguration &pc : projectConfigurationList) { @@ -2665,80 +4846,56 @@ bool ImportProject::importVcxproj(const std::string &filename, compileList.clear(); properties = originalVariables; metadata.clear(); + importStack.clear(); } else first = false; properties["Configuration"] = pc.configuration; properties["Platform"] = pc.platformStr; - for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { - if (hasNameAndNotLabel(node, "ItemGroup", "ProjectConfigurations", properties)) { - for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (hasNameAndAttribute(e, "ClCompile", "Include", properties)) - importCompile(e, projectDir, properties, metadata, compileList); - } - } else if (hasName(node, "ItemDefinitionGroup", properties)) { - for (const tinyxml2::XMLElement *e1 = node->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { - if (hasName(e1, "ClCompile", properties)) { - for (const tinyxml2::XMLElement *e2 = e1->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) - addMetadata(e2, properties, metadata); - } - } - } else if (hasName(node, "PropertyGroup", properties)) { - for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) - addProperty(e, properties); - } else if (hasName(node, "ImportGroup", properties)) { - const char *labelAttribute = node->Attribute("Label"); - if (labelAttribute && std::strcmp(labelAttribute, "PropertySheets") == 0) { - for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (hasName(e, "Import", properties)) { - const char *projectAttribute = e->Attribute("Project"); - if (!projectAttribute) - continue; - if (importProject(e, projectDir, properties, metadata, projectConfigurationList, importStack) > ImportResult::NotResolvable) - return false; - } - } - } else if (labelAttribute && std::strcmp(labelAttribute, "Shared") == 0) { - for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (hasName(e, "Import", properties)) { - const char *projectAttribute = e->Attribute("Project"); - if (!projectAttribute) - continue; - std::string file = toAbsolute(projectAttribute, projectDir, properties); - std::string extension = Path::getFilenameExtensionInLowerCase(file); - if (extension == ".vcxitems") { - ImportResult result = importVcxitems(file, properties, metadata, compileList, projectConfigurationList, importStack); - if (result > ImportResult::NotResolvable) { - errors.emplace_back("Could not import items \"" + file + "\" - " + importResultStr(result)); - return false; - } - if (result == ImportResult::NotResolvable) { - debugs.emplace_back("Could not import items \"" + file + "\" - " + importResultStr(result)); - } - } else { - debugs.emplace_back("Could not import \"" + file + "\" unsupported extension " + extension); - } - } - } - } else { - // Unlabeled or other-labeled ImportGroup (e.g. ExtensionSettings, - // ExtensionTargets) — process children like PropertySheets. - for (const tinyxml2::XMLElement *e = node->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (hasName(e, "Import", properties)) { - const char *projectAttribute = e->Attribute("Project"); - if (!projectAttribute) - continue; - if (importProject(e, projectDir, properties, metadata, projectConfigurationList, importStack) > ImportResult::NotResolvable) - return false; - } - } - } - } else if (hasNameAndAttribute(node, "Import", "Project", properties)) { - if (importProject(node, projectDir, properties, metadata, projectConfigurationList, importStack) > ImportResult::NotResolvable) - return false; - } - } + // 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 @@ -2749,6 +4906,10 @@ bool ImportProject::importVcxproj(const std::string &filename, if (!fileFilters.empty() && !filtermatcher.match(compile.filename)) continue; + 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 == pc.configuration; @@ -2759,48 +4920,171 @@ bool ImportProject::importVcxproj(const std::string &filename, FileSettings fs{ compile.filename, Standards::Language::None, 0 }; // file will be identified later on fs.cfg = pc.name; - // TODO: detect actual MSC version fs.msc = true; fs.defines = "_WIN32=1"; - if (pc.platform == ProjectConfiguration::Win32) + if (pc.platform == ProjectConfiguration::Win32) { fs.platformType = Platform::Type::Win32W; - else if (pc.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; - fs.defines += ";_M_ARM=1"; - } - - Standards::cppstd_t cppstd = Standards::CPPLatest; - const std::string &languageStandard = compile.get("LanguageStandard"); - if (languageStandard == "stdcpp11") - cppstd = Standards::CPP11; - else if (languageStandard == "stdcpp14") - cppstd = Standards::CPP14; - else if (languageStandard == "stdcpp17") - cppstd = Standards::CPP17; - else if (languageStandard == "stdcpp20") - cppstd = Standards::CPP20; - else if (languageStandard == "stdcpp23") - cppstd = Standards::CPP23; - else if (languageStandard == "stdcpplatest") - cppstd = Standards::CPPLatest; - fs.standard = Standards::getCPP(cppstd); - - std::string enableEnhancedInstructionSet = compile.get("EnableEnhancedInstructionSet"); - if (enableEnhancedInstructionSet == "StreamingSIMDExtensions") - fs.defines += ";__SSE__"; - else if (enableEnhancedInstructionSet == "StreamingSIMDExtensions2") - fs.defines += ";__SSE2__"; - else if (enableEnhancedInstructionSet == "AdvancedVectorExtensions") + // MSVC defines _M_ARM=7 (Thumb-2 instruction set) for ARM targets. + fs.defines += ";_M_ARM=7"; + } + + // 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); + } + + // 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; + } + } + + 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 (...) {} + } + } + + 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; + } + + 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 (enableEnhancedInstructionSet == "AdvancedVectorExtensions2") + else if (caseInsensitiveStringCompare(enableEnhancedInstructionSet, "AdvancedVectorExtensions2") == 0) fs.defines += ";__AVX2__"; - else if (enableEnhancedInstructionSet == "AdvancedVectorExtensions512") + else if (caseInsensitiveStringCompare(enableEnhancedInstructionSet, "AdvancedVectorExtensions512") == 0) fs.defines += ";__AVX512F__"; const auto charSetIt = properties.find("CharacterSet"); @@ -2810,30 +5094,146 @@ bool ImportProject::importVcxproj(const std::string &filename, fs.useMfc = useOfMfcIt != properties.end() && !useOfMfcIt->second.empty() && caseInsensitiveStringCompare(useOfMfcIt->second, "false") != 0; - if (charSet == "Unicode") { + if (caseInsensitiveStringCompare(charSet, "Unicode") == 0) fs.defines += ";UNICODE=1;_UNICODE=1"; - } else if (charSet == "MultiByte") { + else if (caseInsensitiveStringCompare(charSet, "MultiByte") == 0) fs.defines += ";_MBCS=1"; + + const auto configurationTypeIt = properties.find("ConfigurationType"); + if (configurationTypeIt != properties.end() && + caseInsensitiveStringCompare(configurationTypeIt->second, "DynamicLibrary") == 0) { + fs.defines += ";_WINDLL"; + } + + if (useOfMfcIt != properties.end() && + caseInsensitiveStringCompare(useOfMfcIt->second, "Dynamic") == 0) { + fs.defines += ";_AFXDLL"; + } + + 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")); - fsSetDefines(fs, defines); - { - const auto includePathIt = properties.find("IncludePath"); - fsSetIncludePaths(fs, projectDir, toStringList(includePathIt != properties.end() ? includePathIt->second : std::string()), properties); + 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 { + seg += c; + } + } + defines = std::move(filtered); } + 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(compile.get("AdditionalIncludeDirectories")), properties); - fs.forcedIncludes = toStringList(compile.get("ForcedIncludeFiles")); - for (auto &forcedInclude : fs.forcedIncludes) - forcedInclude = toAbsolute(forcedInclude, projectDir, properties); + 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; + + // 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)); } } + mImportGraph = ImportGraph(); // deactivate: later imports evaluate conditions live + return true; } @@ -3081,7 +5481,7 @@ bool ImportProject::importBcb6Prj(const std::string &projectFilename) // for the BCB6 project loading. 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) { @@ -3096,7 +5496,10 @@ 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 + // 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)); @@ -3107,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; } @@ -3375,11 +5790,14 @@ void ImportProject::selectOneVsConfig(Platform::Type platform) 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 || 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()) @@ -3409,11 +5827,14 @@ void ImportProject::selectVsConfigurations(Platform::Type platform, const std::v 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 || fs.platformType == Platform::Type::WinARM64 || + fs.platformType == Platform::Type::WinARM64EC || fs.platformType == Platform::Type::WinARM)) remove = true; if (remove) { @@ -3455,19 +5876,57 @@ bool cppcheck::testing::evaluateVcxprojCondition(const std::string& condition, const std::string& configuration, const std::string& platform) { + 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). - return ConditionParser(condition, properties).parse(); + 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; - expandMSBuildVariables(s, properties); + 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 93dc7e11c87..a258336a551 100644 --- a/lib/importproject.h +++ b/lib/importproject.h @@ -26,6 +26,7 @@ #include "platform.h" #include "utils.h" +#include #include #include #include @@ -33,6 +34,7 @@ #include #include #include +#include #include class Settings; @@ -55,9 +57,9 @@ namespace cppcheck { namespace testing { CPPCHECKLIB bool evaluateVcxprojCondition(const std::string& condition, const std::string& configuration, const std::string& platform); - /** Expand MSBuild property expressions ($(Name), $([Class]::Method(args))) in \p expr - * against an empty property map and return the result. Intended for unit tests. */ CPPCHECKLIB std::string expandMSBuildExpression(const std::string& expr); + 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); } } @@ -69,6 +71,10 @@ using MetadataMap = std::map; */ 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, @@ -84,15 +90,39 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { }; enum class ImportResult : std::uint8_t { Ok, + Cycle, // Visual Studio/MSBuild reports a circular import; continue safely NotResolvable, NotFound, NotValid, - Cycle + }; + + /// 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); - void fsSetIncludePaths(FileSettings& fs, const std::string &basepath, const std::list &in, PropertiesMap &properties); + 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; @@ -127,12 +157,15 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { 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); private: + struct PropertyValueExpander; + class ConditionParser; + static void parseArgs(FileSettings &fs, const std::vector &args); bool importBcb6Prj(const std::string &projectFilename); @@ -142,7 +175,7 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { std::string name; std::string configuration; - enum : std::uint8_t { Win32, x64, ARM64, ARM, Unknown } platform = Unknown; + enum : std::uint8_t { Win32, x64, ARM64, ARM64EC, ARM, Unknown } platform = Unknown; std::string platformStr; }; @@ -159,43 +192,218 @@ class CPPCHECKLIB WARN_UNUSED ImportProject { bool importSln(std::istream &istr, const std::string &filename, const std::vector &fileFilters); bool importSlnx(const std::string& filename, const std::vector& fileFilters); - bool importDirectorySolutionProps(PropertiesMap &properties); bool importVcxproj(const std::string &filename, PropertiesMap &properties, const std::vector &fileFilters); - ImportResult importPropsOrTargets(const std::string &file, + 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); - ImportResult importVcxitems(const std::string &items, - PropertiesMap &properties, - MetadataMap &metadata, - std::list &compileList, - std::list &projectConfigurationList, - std::unordered_set &importStack); - ImportResult importProject(const tinyxml2::XMLElement *node, - const std::string &projectDir, + 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); - ImportResult importCompile(const tinyxml2::XMLElement *node, - const std::string &projectDir, - PropertiesMap &properties, - const MetadataMap &metadata, - std::list &compileList); + 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, PropertiesMap &properties); + bool simplifyPathWithVariables(std::string &s, const PropertiesMap &properties); void addProperty(const tinyxml2::XMLElement *node, PropertiesMap &properties); - void addMetadata(const tinyxml2::XMLElement *node, PropertiesMap &properties, MetadataMap &metadata); - std::string getMetadata(const tinyxml2::XMLElement *node, PropertiesMap &properties, const MetadataMap &metadata, const std::string &original); - std::string toAbsolute(const std::string &filename, const std::string &baseDir, PropertiesMap &properties); + 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; }; diff --git a/lib/platform.cpp b/lib/platform.cpp index 3a960404cd7..b956b687419 100644 --- a/lib/platform.cpp +++ b/lib/platform.cpp @@ -115,6 +115,26 @@ bool Platform::set(Type t) 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; @@ -188,6 +208,8 @@ bool Platform::set(const std::string& platformstr, std::string& errstr, const st 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") diff --git a/lib/platform.h b/lib/platform.h index cd1601df6bb..f630df9cbd7 100644 --- a/lib/platform.h +++ b/lib/platform.h @@ -141,6 +141,7 @@ class CPPCHECKLIB Platform { Win32W, Win64, WinARM64, + WinARM64EC, WinARM, Unix32, Unix64, @@ -192,6 +193,8 @@ class CPPCHECKLIB Platform { return "win64"; case Type::WinARM64: return "winARM64"; + case Type::WinARM64EC: + return "winARM64EC"; case Type::WinARM: return "winARM"; case Type::Unix32: diff --git a/test/testimportproject.cpp b/test/testimportproject.cpp index 84dfa73224a..c7cd3000fc2 100644 --- a/test/testimportproject.cpp +++ b/test/testimportproject.cpp @@ -18,12 +18,15 @@ #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 +#include #include #include #include @@ -33,12 +36,13 @@ class TestImporter final : public ImportProject { public: - using ImportProject::importCompileCommands; + using ImportProject::processCompileCommands; using ImportProject::importCppcheckGuiProject; using ImportProject::collectArgs; using ImportProject::fsSetDefines; using ImportProject::fsSetIncludePaths; using ImportProject::setRelativePaths; + using ImportProject::setProjectPath; }; @@ -54,29 +58,31 @@ 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(testCollectArgs1); TEST_CASE(testCollectArgs2); @@ -86,7 +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 { @@ -103,6 +115,20 @@ 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 { @@ -149,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", @@ -158,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 @@ -174,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 @@ -185,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/", @@ -200,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/", @@ -214,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"([{ @@ -233,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"([{ @@ -253,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[] = @@ -271,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()); @@ -281,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[] = @@ -292,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[] = @@ -309,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"([{ @@ -326,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"([{ @@ -348,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"([{ @@ -365,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"([{ @@ -386,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(); @@ -394,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"([{ @@ -411,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"([ @@ -430,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"([{ @@ -447,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()); @@ -455,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]); @@ -577,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}; @@ -734,18 +805,38 @@ class TestImportProject : public TestFixture { // Version comparison: more than 4 parts (no truncation) ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4.5' > '1.2.3.4.4'", "", "")); ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4.4' > '1.2.3.4.5'", "", "")); - ASSERT(cppcheck::testing::evaluateVcxprojCondition("'1.2.3.4.0' == '1.2.3.4'", "", "")); - // Version comparison: missing trailing components treated as 0 - ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17' == '17.0.0.0'", "", "")); - ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'17' != '17.0.0.0'", "", "")); - ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17' >= '17.0.0.0'", "", "")); + // == 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'", "", "")); - ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17.0' == '17'", "", "")); - ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17.0' == '17.0.0.0'", "", "")); - ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17.0.0' == '17'", "", "")); - ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17.1' > '17'", "", "")); + ASSERT(cppcheck::testing::evaluateVcxprojCondition("'17' < '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'", "", "")); @@ -844,6 +935,51 @@ class TestImportProject : public TestFixture { 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'", "", "")); @@ -855,7 +991,172 @@ class TestImportProject : public TestFixture { ASSERT(cppcheck::testing::evaluateVcxprojCondition("'0x0F' < '0x10'", "", "")); ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'0x10' < '0x0F'", "", "")); ASSERT(cppcheck::testing::evaluateVcxprojCondition("'010' > '9'", "", "")); - ASSERT(!cppcheck::testing::evaluateVcxprojCondition("'0x10' == '16'", "", "")); + // 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 { @@ -881,6 +1182,50 @@ class TestImportProject : public TestFixture { 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'))")); @@ -905,6 +1250,23 @@ class TestImportProject : public TestFixture { 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))")); @@ -920,8 +1282,27 @@ class TestImportProject : public TestFixture { 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))")); @@ -930,6 +1311,185 @@ class TestImportProject : public TestFixture { 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() }; From 12f8efa349e2fa16e071cc4fa98a1b8458099b22 Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Wed, 9 Sep 2026 10:53:44 -0400 Subject: [PATCH 12/14] fix makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9b245226036..84699a0244a 100644 --- a/Makefile +++ b/Makefile @@ -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 lib/check.h lib/checkers.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/filesettings.h lib/importproject.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/utils.h test/fixture.h test/redirect.h +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 From fa4a368c37cffe28e34033aadfe3810e96c76b94 Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Wed, 9 Sep 2026 11:15:38 -0400 Subject: [PATCH 13/14] forgot tests --- test/cli/props-dirs/ProjA/ProjA.vcxproj | 7 +- test/cli/props-dirs/ProjA/a.cpp | 4 + test/cli/props-dirs/ProjB/ProjB.vcxproj | 7 +- test/cli/props-dirs/ProjB/b.cpp | 4 + test/cli/props-dirs/common/common2.h | 3 + test/cli/props-dirs/common/common2.props | 13 +++ test/cli/props-dirs/shared/shared2.props | 14 +++ test/cli/props_dirs_test.py | 31 +++-- test/cli/slnx-folders/slnx-folders.slnx | 14 ++- .../vcxproj-unicode/vcxproj_unicode.vcxproj | 10 ++ test/cli/vcxproj_choose/main.cpp | 1 + .../cli/vcxproj_choose/vcxproj_choose.vcxproj | 61 ++++++++++ test/cli/vcxproj_choose_test.py | 44 +++++++ .../Directory.Build.props | 8 ++ .../Directory.Build.targets | 8 ++ .../vcxproj_directory_build_disabled/main.cpp | 15 +++ .../vcxproj_directory_build_disabled.vcxproj | 31 +++++ .../vcxproj_directory_build_disabled_test.py | 33 ++++++ .../CustomBuild.props | 8 ++ .../CustomBuild.targets | 8 ++ .../Directory.Build.props | 8 ++ .../Directory.Build.targets | 8 ++ .../cli/vcxproj_directory_build_path/main.cpp | 21 ++++ .../vcxproj_directory_build_path.vcxproj | 31 +++++ test/cli/vcxproj_directory_build_path_test.py | 32 ++++++ .../cli/vcxproj_discover_choose/Configs.props | 9 ++ test/cli/vcxproj_discover_choose/main.cpp | 1 + .../vcxproj_discover_choose.vcxproj | 34 ++++++ test/cli/vcxproj_discover_choose_test.py | 43 +++++++ .../Debug/Configs.props | 9 ++ .../vcxproj_discover_property_path/main.cpp | 1 + .../vcxproj_discover_property_path.vcxproj | 28 +++++ .../vcxproj_discover_property_path_test.py | 44 +++++++ .../cli/vcxproj_duplicate_import/Common.props | 6 + test/cli/vcxproj_duplicate_import/main.cpp | 1 + .../vcxproj_duplicate_import.vcxproj | 41 +++++++ test/cli/vcxproj_duplicate_import_test.py | 40 +++++++ .../conditional.props | 8 ++ test/cli/vcxproj_import_condition/main.cpp | 1 + .../vcxproj_import_condition.vcxproj | 37 ++++++ test/cli/vcxproj_import_condition_test.py | 43 +++++++ .../Directory.Build.props | 8 ++ test/cli/vcxproj_import_graph/dup.props | 12 ++ test/cli/vcxproj_import_graph/guarded.props | 11 ++ test/cli/vcxproj_import_graph/main.cpp | 20 ++++ .../vcxproj_import_graph.slnx | 6 + .../vcxproj_import_graph.vcxproj | 55 +++++++++ test/cli/vcxproj_import_graph_test.py | 32 ++++++ .../ImportBefore/a.props | 8 ++ .../ImportBefore/b.props | 8 ++ .../ImportBefore/decoy.txt | 2 + test/cli/vcxproj_import_wildcard/main.cpp | 18 +++ .../vcxproj_import_wildcard.vcxproj | 36 ++++++ test/cli/vcxproj_import_wildcard_test.py | 43 +++++++ .../RealInc/myheader.h | 1 + test/cli/vcxproj_include_dir_macro/main.cpp | 5 + .../vcxproj_include_dir_macro.vcxproj | 39 +++++++ test/cli/vcxproj_include_dir_macro_test.py | 59 ++++++++++ .../vcxproj_item_absolute_drive_path.vcxproj | 36 ++++++ .../vcxproj_item_absolute_drive_path_test.py | 49 ++++++++ test/cli/vcxproj_item_condition/foo.cpp | 1 + .../vcxproj_item_condition.vcxproj | 39 +++++++ test/cli/vcxproj_item_condition_test.py | 47 ++++++++ .../vcxproj_item_macro_path/SomeDir/foo.cpp | 1 + .../vcxproj_item_macro_path.vcxproj | 52 +++++++++ .../foo.cpp | 8 ++ ...xproj_item_macro_path_case_builtin.vcxproj | 35 ++++++ .../SameCaseDir/mathplot.cpp | 8 ++ ...roj_item_macro_path_case_invariant.vcxproj | 49 ++++++++ test/cli/vcxproj_item_macro_path_case_test.py | 107 ++++++++++++++++++ .../DebugCaseDir/foo.cpp | 2 + .../ReleaseCaseDir/foo.cpp | 2 + ...xproj_item_macro_path_case_variant.vcxproj | 53 +++++++++ .../EnvLibRoot/mathplot.cpp | 8 ++ .../vcxproj_item_macro_path_env.vcxproj | 45 ++++++++ test/cli/vcxproj_item_macro_path_env_test.py | 74 ++++++++++++ .../HyphenDir/foo.cpp | 10 ++ .../vcxproj_item_macro_path_hyphen.vcxproj | 45 ++++++++ .../vcxproj_item_macro_path_hyphen_test.py | 49 ++++++++ .../ExternalLibRoot/mathplot.cpp | 7 ++ .../vcxproj_item_macro_path_invariant.vcxproj | 51 +++++++++ .../vcxproj_item_macro_path_invariant_test.py | 52 +++++++++ .../DebugDir/foo.cpp | 2 + ...roj_item_macro_path_missing_config.vcxproj | 50 ++++++++ ...roj_item_macro_path_missing_config_test.py | 55 +++++++++ test/cli/vcxproj_item_macro_path_test.py | 69 +++++++++++ .../foo.cpp | 21 ++++ ...xproj_metadata_self_reference_case.vcxproj | 48 ++++++++ ...xproj_metadata_self_reference_case_test.py | 42 +++++++ test/cli/vcxproj_property_order/main.cpp | 4 + .../vcxproj_property_order.vcxproj | 53 +++++++++ test/cli/vcxproj_property_order_test.py | 39 +++++++ .../vcxproj_split_configs/ConfigsDebug.props | 9 ++ .../ConfigsRelease.props | 9 ++ test/cli/vcxproj_split_configs/main.cpp | 1 + .../vcxproj_split_configs.vcxproj | 36 ++++++ test/cli/vcxproj_split_configs_test.py | 49 ++++++++ test/cli/vcxproj_unicode_test.py | 27 ++++- 98 files changed, 2448 insertions(+), 29 deletions(-) create mode 100644 test/cli/props-dirs/common/common2.h create mode 100644 test/cli/props-dirs/common/common2.props create mode 100644 test/cli/props-dirs/shared/shared2.props create mode 100644 test/cli/vcxproj_choose/main.cpp create mode 100644 test/cli/vcxproj_choose/vcxproj_choose.vcxproj create mode 100644 test/cli/vcxproj_choose_test.py create mode 100644 test/cli/vcxproj_directory_build_disabled/Directory.Build.props create mode 100644 test/cli/vcxproj_directory_build_disabled/Directory.Build.targets create mode 100644 test/cli/vcxproj_directory_build_disabled/main.cpp create mode 100644 test/cli/vcxproj_directory_build_disabled/vcxproj_directory_build_disabled.vcxproj create mode 100644 test/cli/vcxproj_directory_build_disabled_test.py create mode 100644 test/cli/vcxproj_directory_build_path/CustomBuild.props create mode 100644 test/cli/vcxproj_directory_build_path/CustomBuild.targets create mode 100644 test/cli/vcxproj_directory_build_path/Directory.Build.props create mode 100644 test/cli/vcxproj_directory_build_path/Directory.Build.targets create mode 100644 test/cli/vcxproj_directory_build_path/main.cpp create mode 100644 test/cli/vcxproj_directory_build_path/vcxproj_directory_build_path.vcxproj create mode 100644 test/cli/vcxproj_directory_build_path_test.py create mode 100644 test/cli/vcxproj_discover_choose/Configs.props create mode 100644 test/cli/vcxproj_discover_choose/main.cpp create mode 100644 test/cli/vcxproj_discover_choose/vcxproj_discover_choose.vcxproj create mode 100644 test/cli/vcxproj_discover_choose_test.py create mode 100644 test/cli/vcxproj_discover_property_path/Debug/Configs.props create mode 100644 test/cli/vcxproj_discover_property_path/main.cpp create mode 100644 test/cli/vcxproj_discover_property_path/vcxproj_discover_property_path.vcxproj create mode 100644 test/cli/vcxproj_discover_property_path_test.py create mode 100644 test/cli/vcxproj_duplicate_import/Common.props create mode 100644 test/cli/vcxproj_duplicate_import/main.cpp create mode 100644 test/cli/vcxproj_duplicate_import/vcxproj_duplicate_import.vcxproj create mode 100644 test/cli/vcxproj_duplicate_import_test.py create mode 100644 test/cli/vcxproj_import_condition/conditional.props create mode 100644 test/cli/vcxproj_import_condition/main.cpp create mode 100644 test/cli/vcxproj_import_condition/vcxproj_import_condition.vcxproj create mode 100644 test/cli/vcxproj_import_condition_test.py create mode 100644 test/cli/vcxproj_import_graph/Directory.Build.props create mode 100644 test/cli/vcxproj_import_graph/dup.props create mode 100644 test/cli/vcxproj_import_graph/guarded.props create mode 100644 test/cli/vcxproj_import_graph/main.cpp create mode 100644 test/cli/vcxproj_import_graph/vcxproj_import_graph.slnx create mode 100644 test/cli/vcxproj_import_graph/vcxproj_import_graph.vcxproj create mode 100644 test/cli/vcxproj_import_graph_test.py create mode 100644 test/cli/vcxproj_import_wildcard/ImportBefore/a.props create mode 100644 test/cli/vcxproj_import_wildcard/ImportBefore/b.props create mode 100644 test/cli/vcxproj_import_wildcard/ImportBefore/decoy.txt create mode 100644 test/cli/vcxproj_import_wildcard/main.cpp create mode 100644 test/cli/vcxproj_import_wildcard/vcxproj_import_wildcard.vcxproj create mode 100644 test/cli/vcxproj_import_wildcard_test.py create mode 100644 test/cli/vcxproj_include_dir_macro/RealInc/myheader.h create mode 100644 test/cli/vcxproj_include_dir_macro/main.cpp create mode 100644 test/cli/vcxproj_include_dir_macro/vcxproj_include_dir_macro.vcxproj create mode 100644 test/cli/vcxproj_include_dir_macro_test.py create mode 100644 test/cli/vcxproj_item_absolute_drive_path/vcxproj_item_absolute_drive_path.vcxproj create mode 100644 test/cli/vcxproj_item_absolute_drive_path_test.py create mode 100644 test/cli/vcxproj_item_condition/foo.cpp create mode 100644 test/cli/vcxproj_item_condition/vcxproj_item_condition.vcxproj create mode 100644 test/cli/vcxproj_item_condition_test.py create mode 100644 test/cli/vcxproj_item_macro_path/SomeDir/foo.cpp create mode 100644 test/cli/vcxproj_item_macro_path/vcxproj_item_macro_path.vcxproj create mode 100644 test/cli/vcxproj_item_macro_path_case_builtin/foo.cpp create mode 100644 test/cli/vcxproj_item_macro_path_case_builtin/vcxproj_item_macro_path_case_builtin.vcxproj create mode 100644 test/cli/vcxproj_item_macro_path_case_invariant/SameCaseDir/mathplot.cpp create mode 100644 test/cli/vcxproj_item_macro_path_case_invariant/vcxproj_item_macro_path_case_invariant.vcxproj create mode 100644 test/cli/vcxproj_item_macro_path_case_test.py create mode 100644 test/cli/vcxproj_item_macro_path_case_variant/DebugCaseDir/foo.cpp create mode 100644 test/cli/vcxproj_item_macro_path_case_variant/ReleaseCaseDir/foo.cpp create mode 100644 test/cli/vcxproj_item_macro_path_case_variant/vcxproj_item_macro_path_case_variant.vcxproj create mode 100644 test/cli/vcxproj_item_macro_path_env/EnvLibRoot/mathplot.cpp create mode 100644 test/cli/vcxproj_item_macro_path_env/vcxproj_item_macro_path_env.vcxproj create mode 100644 test/cli/vcxproj_item_macro_path_env_test.py create mode 100644 test/cli/vcxproj_item_macro_path_hyphen/HyphenDir/foo.cpp create mode 100644 test/cli/vcxproj_item_macro_path_hyphen/vcxproj_item_macro_path_hyphen.vcxproj create mode 100644 test/cli/vcxproj_item_macro_path_hyphen_test.py create mode 100644 test/cli/vcxproj_item_macro_path_invariant/ExternalLibRoot/mathplot.cpp create mode 100644 test/cli/vcxproj_item_macro_path_invariant/vcxproj_item_macro_path_invariant.vcxproj create mode 100644 test/cli/vcxproj_item_macro_path_invariant_test.py create mode 100644 test/cli/vcxproj_item_macro_path_missing_config/DebugDir/foo.cpp create mode 100644 test/cli/vcxproj_item_macro_path_missing_config/vcxproj_item_macro_path_missing_config.vcxproj create mode 100644 test/cli/vcxproj_item_macro_path_missing_config_test.py create mode 100644 test/cli/vcxproj_item_macro_path_test.py create mode 100644 test/cli/vcxproj_metadata_self_reference_case/foo.cpp create mode 100644 test/cli/vcxproj_metadata_self_reference_case/vcxproj_metadata_self_reference_case.vcxproj create mode 100644 test/cli/vcxproj_metadata_self_reference_case_test.py create mode 100644 test/cli/vcxproj_property_order/main.cpp create mode 100644 test/cli/vcxproj_property_order/vcxproj_property_order.vcxproj create mode 100644 test/cli/vcxproj_property_order_test.py create mode 100644 test/cli/vcxproj_split_configs/ConfigsDebug.props create mode 100644 test/cli/vcxproj_split_configs/ConfigsRelease.props create mode 100644 test/cli/vcxproj_split_configs/main.cpp create mode 100644 test/cli/vcxproj_split_configs/vcxproj_split_configs.vcxproj create mode 100644 test/cli/vcxproj_split_configs_test.py diff --git a/test/cli/props-dirs/ProjA/ProjA.vcxproj b/test/cli/props-dirs/ProjA/ProjA.vcxproj index 5ce4fbd069d..9ce10068a8b 100644 --- a/test/cli/props-dirs/ProjA/ProjA.vcxproj +++ b/test/cli/props-dirs/ProjA/ProjA.vcxproj @@ -1,11 +1,5 @@ - - - Debug - x64 - - {a1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1} ProjA @@ -19,6 +13,7 @@ + diff --git a/test/cli/props-dirs/ProjA/a.cpp b/test/cli/props-dirs/ProjA/a.cpp index 4eb775af468..aab1237748c 100644 --- a/test/cli/props-dirs/ProjA/a.cpp +++ b/test/cli/props-dirs/ProjA/a.cpp @@ -1,8 +1,12 @@ #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() { diff --git a/test/cli/props-dirs/ProjB/ProjB.vcxproj b/test/cli/props-dirs/ProjB/ProjB.vcxproj index 047f5d4b8fa..3daf8907924 100644 --- a/test/cli/props-dirs/ProjB/ProjB.vcxproj +++ b/test/cli/props-dirs/ProjB/ProjB.vcxproj @@ -1,11 +1,5 @@ - - - Debug - x64 - - {b2b2b2b2-b2b2-b2b2-b2b2-b2b2b2b2b2b2} ProjB @@ -21,6 +15,7 @@ in the same solution (regression coverage for the mVariables cross-project leak). --> + diff --git a/test/cli/props-dirs/ProjB/b.cpp b/test/cli/props-dirs/ProjB/b.cpp index c24977c13ae..8c3e4016529 100644 --- a/test/cli/props-dirs/ProjB/b.cpp +++ b/test/cli/props-dirs/ProjB/b.cpp @@ -1,8 +1,12 @@ #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() { 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/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 index 123cc010b5e..bccfe3cf2bc 100644 --- a/test/cli/props_dirs_test.py +++ b/test/cli/props_dirs_test.py @@ -18,10 +18,10 @@ __script_dir = os.path.dirname(os.path.abspath(__file__)) -__ERR_A = ('%s:10:14: error: Division by zero. [zerodiv]\n' +__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:10:14: error: Division by zero. [zerodiv]\n' +__ERR_B = ('%s:14:14: error: Division by zero. [zerodiv]\n' ' return y / 0;\n' ' ^\n') % os.path.join('props-dirs', 'ProjB', 'b.cpp') @@ -65,14 +65,29 @@ def test_props_dirs_defines_and_standard(): with open(dump_b, 'rt') as f: dump_b_content = f.read() - # ProjA imports shared/shared.props (which itself imports common/common.props), and - # also sets its own PROJA_DEFINE - all three must be present, most specific first - assert 'cfg="_WIN32=1;_WIN64=1;PROJA_DEFINE=1;SHARED_DEFINE=1;COMMON_DEFINE=1;_MSC_VER=1900"' in dump_a_content + # 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 directly - it must see COMMON_DEFINE, but neither - # PROJA_DEFINE nor SHARED_DEFINE, which only ever applied to ProjA - assert 'cfg="_WIN32=1;_WIN64=1;COMMON_DEFINE=1;_MSC_VER=1900"' in dump_b_content + # 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/vcxproj_unicode.vcxproj b/test/cli/vcxproj-unicode/vcxproj_unicode.vcxproj index e85592a647e..cc7549a0e06 100644 --- a/test/cli/vcxproj-unicode/vcxproj_unicode.vcxproj +++ b/test/cli/vcxproj-unicode/vcxproj_unicode.vcxproj @@ -9,6 +9,10 @@ Release Win32 + + MultiByte + Win32 + @@ -27,6 +31,12 @@ 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_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 index 7e1dd22954e..b59a4c4ad4e 100644 --- a/test/cli/vcxproj_unicode_test.py +++ b/test/cli/vcxproj_unicode_test.py @@ -33,10 +33,31 @@ def _get_dump_for_configuration(tmp_path, configuration): def test_vcxproj_unicode_debug(tmp_path): dump_content = _get_dump_for_configuration(tmp_path, 'Debug|Win32') - # the resolved defines are recorded in the attribute - assert 'cfg="_WIN32=1;UNICODE=1;_UNICODE=1;_MSC_VER=1900"' in dump_content + # 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') - assert 'cfg="_WIN32=1;_MSC_VER=1900;__AFXWIN_H__=1"' in dump_content + # 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 From 84dee749d48bfc09ee220b0178e147e0a928ab3a Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Wed, 9 Sep 2026 11:28:06 -0400 Subject: [PATCH 14/14] missed a test --- test/cli/project_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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" \