From c790d437e2e930f355728fdc6f3d264bf141bab7 Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Wed, 19 Aug 2026 18:37:20 -0400 Subject: [PATCH 1/2] 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/2] Force GitHub Action run