From c790d437e2e930f355728fdc6f3d264bf141bab7 Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Wed, 19 Aug 2026 18:37:20 -0400 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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