From 7afa955cbb8c35ab25626f37ad51894e09a2d6db Mon Sep 17 00:00:00 2001 From: Robert Reif Date: Sun, 9 Aug 2026 09:27:18 -0400 Subject: [PATCH] added support for reading vcxproj ForcedIncludeFiles and support for reading more information from props files --- lib/importproject.cpp | 550 ++++++++++++++---- 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.cppcheck | 16 + .../vcxproj_forced_includes.props | 8 + .../vcxproj_forced_includes.slnx | 6 + .../vcxproj_forced_includes.vcxproj | 103 ++++ test/cli/vcxproj_forced_includes_test.py | 34 ++ test/testimportproject.cpp | 197 +++++++ 17 files changed, 849 insertions(+), 124 deletions(-) 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.cppcheck 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 diff --git a/lib/importproject.cpp b/lib/importproject.cpp index 5c9bcaadd3f..49c96e66376 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -594,13 +594,13 @@ namespace { if (a) name = a; for (const tinyxml2::XMLElement *e = cfg->FirstChildElement(); e; e = e->NextSiblingElement()) { - const char * const text = e->GetText(); + const char *const text = e->GetText(); if (!text) continue; - const char * ename = e->Name(); - if (std::strcmp(ename,"Configuration")==0) + const char *ename = e->Name(); + if (std::strcmp(ename, "Configuration") == 0) configuration = text; - else if (std::strcmp(ename,"Platform")==0) { + else if (std::strcmp(ename, "Platform") == 0) { platformStr = text; if (platformStr == "Win32") platform = Win32; @@ -618,7 +618,7 @@ namespace { }; struct Conditional { - explicit Conditional(const tinyxml2::XMLElement *idg){ + explicit Conditional(const tinyxml2::XMLElement *idg) { const char *condAttr = idg->Attribute("Condition"); if (condAttr) mCondition = condAttr; @@ -628,8 +628,8 @@ namespace { 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); + c.erase(pos, from.size()); + c.insert(pos, to); } } @@ -641,14 +641,14 @@ namespace { try { return evalCondition(mCondition, p); } - catch (const std::runtime_error& r) + 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) { + static bool evalCondition(const std::string &condition, const ProjectConfiguration &p) { std::string c = '(' + condition + ")"; replaceAll(c, "$(Configuration)", p.configuration); replaceAll(c, "$(Platform)", p.platformStr); @@ -661,8 +661,8 @@ namespace { // generate links { - std::stack lpar; - for (Token* tok2 = tokenlist.front(); tok2; tok2 = tok2->next()) { + std::stack lpar; + for (Token *tok2 = tokenlist.front(); tok2; tok2 = tok2->next()) { if (tok2->str() == "(") lpar.push(tok2); else if (tok2->str() == ")") { @@ -698,15 +698,15 @@ namespace { private: - static std::string executeOp1(const Token* tok, const ProjectConfiguration &p) { + 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) { + 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) { + static std::string execute(const Token *tok, const ProjectConfiguration &p) { if (!tok) throw std::runtime_error("Missing operator"); auto boolResult = [](bool b) -> std::string { @@ -723,7 +723,7 @@ namespace { 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); + const std::string &propertyName = tok->strAt(1); std::string propertyValue; if (propertyName == "Configuration") propertyValue = p.configuration; @@ -731,16 +731,16 @@ namespace { propertyValue = p.platformStr; else throw std::runtime_error("Unhandled property '" + propertyName + "'"); - const std::string& method = tok->strAt(3); + 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())); + return boolResult(endsWith(propertyValue, arg.c_str(), arg.size())); if (method == "StartsWith") - return boolResult(startsWith(propertyValue,arg)); + return boolResult(startsWith(propertyValue, arg)); throw std::runtime_error("Unhandled method '" + method + "'"); } if (tok->str().size() >= 2 && tok->str()[0] == '\'') // String Literal @@ -752,40 +752,109 @@ namespace { std::string mCondition; }; + 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; + } + + std::string resolveIncludePath(const std::string &raw, + const std::string &sourceDir, + std::map &variables) + { + std::string s = raw; + // $(MSBuildThisFileDirectory) is the directory of the file being parsed — + // resolve it now while sourceDir is still correct. + findAndReplace(s, "$(MSBuildThisFileDirectory)", sourceDir); + // Best-effort: substitute any variables we know right now. + // $(ProjectDir) and $(SolutionDir) may also be present; substitute if known. + const bool hasVariables = raw.find("$(") != std::string::npos; + { + auto it = variables.find("ProjectDir"); + if (it != variables.end()) + findAndReplace(s, "$(ProjectDir)", it->second); + } + { + auto it = variables.find("SolutionDir"); + if (it != variables.end()) + findAndReplace(s, "$(SolutionDir)", it->second); + } + // A substituted variable already yields a path that is absolute or relative + // to the working directory, so only prepend sourceDir when the raw value + // had no variables at all (i.e. it is relative to the parsed file). + if (!hasVariables && !Path::isAbsolute(s)) + s = sourceDir + s; + return Path::simplifyPath(std::move(s)); + } + + struct ForcedIncludeFiles { + ForcedIncludeFiles(std::string condition, const std::string &commaSeparatedFiles) : condition(std::move(condition)), files(toStringList(commaSeparatedFiles)) {} + explicit ForcedIncludeFiles(const std::string &commaSeparatedFiles) : files(toStringList(commaSeparatedFiles)) {} + std::string condition; + std::list files; + }; + struct ItemDefinitionGroup : Conditional { - explicit ItemDefinitionGroup(const tinyxml2::XMLElement *idg, std::string includePaths) : Conditional(idg), additionalIncludePaths(std::move(includePaths)) { + explicit ItemDefinitionGroup(const tinyxml2::XMLElement *idg, + std::string includePaths, + const std::string &sourceDir, + std::map &variables) + : Conditional(idg), additionalIncludePaths(std::move(includePaths)) { for (const tinyxml2::XMLElement *e1 = idg->FirstChildElement(); e1; e1 = e1->NextSiblingElement()) { - const char* name = e1->Name(); + 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(); + const char *const text = e->GetText(); if (!text) continue; - const char * const ename = e->Name(); + const char *const ename = e->Name(); if (std::strcmp(ename, "PreprocessorDefinitions") == 0) preprocessorDefinitions = text; - else if (std::strcmp(ename, "AdditionalIncludeDirectories") == 0) { + else if (std::strcmp(ename, "AdditionalIncludeDirectories") == 0 || std::strcmp(ename, "ExternalIncludePath") == 0) { if (!additionalIncludePaths.empty()) additionalIncludePaths += ';'; additionalIncludePaths += text; } else if (std::strcmp(ename, "LanguageStandard") == 0) { - if (std::strcmp(text, "stdcpp14") == 0) + if (std::strcmp(text, "stdcpp11") == 0) + cppstd = Standards::CPP11; + else 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, "stdcpp23") == 0) + cppstd = Standards::CPP23; else if (std::strcmp(text, "stdcpplatest") == 0) cppstd = Standards::CPPLatest; } else if (std::strcmp(ename, "EnableEnhancedInstructionSet") == 0) { enhancedInstructionSet = text; + } else if (std::strcmp(ename, "ForcedIncludeFiles") == 0) { + const char *condition = e->Attribute("Condition"); + ForcedIncludeFiles entry = condition ? ForcedIncludeFiles(condition, text) : ForcedIncludeFiles(text); + // Resolve eagerly while sourceDir is known. + for (std::string &file : entry.files) { + if (!file.empty() && !startsWith(file, "%(")) + file = resolveIncludePath(file, sourceDir, variables); + } + forcedIncludeFiles.push_back(std::move(entry)); } } } else if (std::strcmp(name, "Link") == 0) { for (const tinyxml2::XMLElement *e = e1->FirstChildElement(); e; e = e->NextSiblingElement()) { - const char * const text = e->GetText(); + const char *const text = e->GetText(); if (!text) continue; if (std::strcmp(e->Name(), "EntryPointSymbol") == 0) { @@ -794,51 +863,73 @@ namespace { } } } + + // $(MSBuildThisFileDirectory) refers to the directory of the file being + // evaluated (the vcxproj for the project's own ItemDefinitionGroups, the + // imported props file otherwise); resolve it while sourceDir is known. + findAndReplace(additionalIncludePaths, "$(MSBuildThisFileDirectory)", sourceDir); } std::string enhancedInstructionSet; std::string preprocessorDefinitions; std::string additionalIncludePaths; + std::list forcedIncludeFiles; std::string entryPointSymbol; // TODO: use this + bool fromProps = false; Standards::cppstd_t cppstd = Standards::CPPLatest; }; struct ConfigurationPropertyGroup : Conditional { explicit ConfigurationPropertyGroup(const tinyxml2::XMLElement *idg) : Conditional(idg) { for (const tinyxml2::XMLElement *e = idg->FirstChildElement(); e; e = e->NextSiblingElement()) { - if (std::strcmp(e->Name(), "UseOfMfc") == 0) { + const char *name = e->Name(); + const char *text = e->GetText(); + if (!name || !text) + continue; + if (std::strcmp(name, "UseOfMfc") == 0) { useOfMfc = true; - } else if (std::strcmp(e->Name(), "CharacterSet") == 0) { - useUnicode = std::strcmp(e->GetText(), "Unicode") == 0; + } else if (std::strcmp(name, "CharacterSet") == 0) { + useUnicode = std::strcmp(text, "Unicode") == 0; } + properties[std::string(name)] = text; } } bool useOfMfc = false; bool useUnicode = false; + std::map properties; }; 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()) { + ItemGroupClCompile(const tinyxml2::XMLElement *element, std::string file, + const std::string &sourceDir, + std::map &variables) : mFilename(std::move(file)) { + for (const tinyxml2::XMLElement *childElement = element->FirstChildElement(); childElement; childElement = childElement->NextSiblingElement()) { const char *name = childElement->Name(); - if (!name) + const char *text = childElement->GetText(); + if (!name || !text) continue; + const char *condition = childElement->Attribute("Condition"); if (std::strcmp(name, "ExcludedFromBuild") == 0) { - const char *condition = childElement->Attribute("Condition"); - const char *text = childElement->GetText(); - if (!condition || !text || std::strcmp(text, "true") != 0) + if (!condition || std::strcmp(text, "true") != 0) continue; mConditions.emplace_back(condition); + } else if (std::strcmp(name, "ForcedIncludeFiles") == 0) { + ForcedIncludeFiles entry = condition ? ForcedIncludeFiles(condition, text) : ForcedIncludeFiles(text); + // Resolve eagerly while sourceDir is known. + for (std::string &forcedFile : entry.files) { + if (!forcedFile.empty() && !startsWith(forcedFile, "%(")) + forcedFile = resolveIncludePath(forcedFile, sourceDir, variables); + } + forcedIncludeFiles.emplace_back(std::move(entry)); } - // TODO: ForcedIncludeFiles and PrecompiledHeaderFile } } - bool exclude(const ProjectConfiguration& p, std::vector& errors) const { + bool exclude(const ProjectConfiguration &p, std::vector &errors) const { if (mConditions.empty()) return false; - for (const std::string& condition : mConditions) { + for (const std::string &condition : mConditions) { Conditional conditional(condition); if (conditional.conditionIsTrue(p, mFilename, errors)) return true; @@ -847,68 +938,92 @@ namespace { } std::string mFilename; std::list mConditions; + std::list forcedIncludeFiles; }; } -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; - } - 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) +static void importPropertyGroup(const tinyxml2::XMLElement *node, + std::map &variables, + std::string &includePath, + std::list &forcedIncludeFiles, + const std::string &sourceDir) { - const char* labelAttribute = node->Attribute("Label"); + 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 *name = propertyGroup->Name(); const char *text = empty_if_null(propertyGroup->GetText()); variables[name] = text; } - } else if (!labelAttribute) { + } else if (!labelAttribute || std::strcmp(labelAttribute, "Globals") == 0) { 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); + if (std::strcmp(propertyGroup->Name(), "IncludePath") == 0) { + 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); + // expose the accumulated value so later paths can use $(IncludePath) + variables["IncludePath"] = includePath; + } else if (std::strcmp(propertyGroup->Name(), "ForcedIncludeFiles") == 0) { + const char *text = propertyGroup->GetText(); + if (!text) + continue; + // Resolve eagerly while sourceDir is known. + const char *condition = propertyGroup->Attribute("Condition"); + ForcedIncludeFiles entry = condition ? ForcedIncludeFiles(condition, text) : ForcedIncludeFiles(text); + for (std::string &file : entry.files) { + if (!file.empty() && !startsWith(file, "%(")) + file = resolveIncludePath(file, sourceDir, variables); + } + forcedIncludeFiles.push_back(std::move(entry)); + } else { + // properties defined in the project become variables so paths can + // reference them (e.g. $(GeneratedFilesDir)) + const char *text = propertyGroup->GetText(); + if (text) + variables[propertyGroup->Name()] = text; + } } } } -static void loadVisualStudioProperties(const std::string &props, std::map &variables, std::string &includePath, const std::string &additionalIncludeDirectories, std::list &itemDefinitionGroupList) +static void loadVisualStudioProperties(const std::string &props, + std::map &variables, + std::string &includePath, + const std::string &additionalIncludeDirectories, + std::list &itemDefinitionGroupList, + std::list &forcedIncludeFiles, + std::set &loadedProps) { - std::string filename(props); + std::string filename = Path::fromNativeSeparators(props); + // remember whether the path contained MSBuild variables; after substitution + // the result is already relative to the current directory (or absolute), so + // prepending ProjectDir again would double-prefix it. + const bool hasVariables = filename.find("$(") != std::string::npos; // variables can't be resolved if (!simplifyPathWithVariables(filename, variables)) return; // prepend project dir (if it exists) to transform relative paths into absolute ones - if (!Path::isAbsolute(filename) && variables.count("ProjectDir") > 0) + if (!hasVariables && !Path::isAbsolute(filename) && variables.count("ProjectDir") > 0) filename = Path::getAbsoluteFilePath(variables.at("ProjectDir") + filename); + // avoid loading the same file more than once + if (!loadedProps.insert(filename).second) + return; + tinyxml2::XMLDocument doc; if (doc.LoadFile(filename.c_str()) != tinyxml2::XML_SUCCESS) return; const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement(); if (rootnode == nullptr) return; + const std::string sourceDir = Path::getPathFromFilename(filename); for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { const char* name = node->Name(); if (std::strcmp(name, "ImportGroup") == 0) { @@ -924,13 +1039,34 @@ static void loadVisualStudioProperties(const std::string &props, std::mapFirstChildElement(); property; property = property->NextSiblingElement()) { + const char *const propertyName = property->Name(); + const char *const propertyValue = property->GetText(); + if (!propertyName || !propertyValue) + continue; + std::string value(propertyValue); + // $(MSBuildThisFileDirectory) refers to the directory of the file being evaluated + findAndReplace(value, "$(MSBuildThisFileDirectory)", sourceDir); + variables[propertyName] = std::move(value); + } + } else if (std::strcmp(name,"Import")==0) { + const char *projectAttribute = node->Attribute("Project"); + if (!projectAttribute) + continue; + std::string importFile(projectAttribute); + if (importFile.find('$') == std::string::npos && !Path::isAbsolute(importFile)) + importFile = sourceDir + importFile; + loadVisualStudioProperties(importFile, variables, includePath, additionalIncludeDirectories, itemDefinitionGroupList, forcedIncludeFiles, loadedProps); } else if (std::strcmp(name,"ItemDefinitionGroup")==0) { - itemDefinitionGroupList.emplace_back(node, additionalIncludeDirectories); + itemDefinitionGroupList.emplace_back(node, additionalIncludeDirectories, sourceDir, variables); + itemDefinitionGroupList.back().fromProps = true; } } } @@ -950,9 +1086,15 @@ bool ImportProject::importVcxproj(const std::string &filename, 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) +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)); + variables.emplace("SolutionDir", variables["ProjectDir"]); std::list projectConfigurationList; std::list compileList; @@ -960,6 +1102,25 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X std::vector configurationPropertyGroups; std::string includePath; std::vector sharedItemsProjects; + std::list propertyGroupForcedIncludes; + const std::string vcxprojDir = Path::getPathFromFilename(filename); + std::set loadedProps; + + // MSBuild automatically imports the closest Directory.Build.props found in the + // project directory or any of its parent directories. + { + std::string dir = Path::fromNativeSeparators(vcxprojDir); + while (!dir.empty()) { + const std::string propsFile = dir + "Directory.Build.props"; + if (Path::isFile(propsFile)) { + loadVisualStudioProperties(propsFile, variables, includePath, additionalIncludeDirectories, itemDefinitionGroupList, propertyGroupForcedIncludes, loadedProps); + break; + } + if (dir.size() == 1) + break; + dir = Path::getPathFromFilename(dir.substr(0, dir.size() - 1)); + } + } const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement(); if (rootnode == nullptr) { @@ -967,7 +1128,7 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X return false; } for (const tinyxml2::XMLElement *node = rootnode->FirstChildElement(); node; node = node->NextSiblingElement()) { - const char* name = node->Name(); + const char *name = node->Name(); if (std::strcmp(name, "ItemGroup") == 0) { const char *labelAttribute = node->Attribute("Label"); if (labelAttribute && std::strcmp(labelAttribute, "ProjectConfigurations") == 0) { @@ -985,33 +1146,43 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X if (std::strcmp(e->Name(), "ClCompile") == 0) { const char *include = e->Attribute("Include"); if (include && Path::acceptFile(include)) { - std::string toInclude = Path::simplifyPath(Path::isAbsolute(include) ? include : Path::getPathFromFilename(filename) + include); + std::string toInclude = include; findAndReplace(toInclude, "$(MSBuildThisFileDirectory)", "./"); - compileList.emplace_back(e, toInclude); + findAndReplace(toInclude, "$(ProjectDir)", variables["ProjectDir"]); + findAndReplace(toInclude, "$(SolutionDir)", variables["SolutionDir"]); + // resolve other variables (e.g. $(RepoRoot)) eagerly; + // configuration-dependent paths (e.g. $(GeneratedFilesDir)) + // are resolved per configuration in the finalize step. + if (toInclude.find("$(") != std::string::npos) + simplifyPathWithVariables(toInclude, variables); + toInclude = Path::simplifyPath(Path::isAbsolute(toInclude) ? toInclude : Path::getPathFromFilename(filename) + toInclude); + compileList.emplace_back(e, toInclude, vcxprojDir, variables); } } } } } else if (std::strcmp(name, "ItemDefinitionGroup") == 0) { - itemDefinitionGroupList.emplace_back(node, additionalIncludeDirectories); + itemDefinitionGroupList.emplace_back(node, additionalIncludeDirectories, vcxprojDir, variables); } else if (std::strcmp(name, "PropertyGroup") == 0) { - const char* labelAttribute = node->Attribute("Label"); + const char *labelAttribute = node->Attribute("Label"); if (labelAttribute && std::strcmp(labelAttribute, "Configuration") == 0) { configurationPropertyGroups.emplace_back(node); } else { - importPropertyGroup(node, variables, includePath); + importPropertyGroup(node, variables, includePath, propertyGroupForcedIncludes, vcxprojDir); } - } else if (std::strcmp(name, "ImportGroup") == 0) { + } + 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); + loadVisualStudioProperties(projectAttribute, variables, includePath, additionalIncludeDirectories, itemDefinitionGroupList, propertyGroupForcedIncludes, loadedProps); } } - } else if (labelAttribute && std::strcmp(labelAttribute, "Shared") == 0) { + } + 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"); @@ -1021,7 +1192,8 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X std::string pathToSharedItemsFile; if (std::string(projectAttribute).rfind("$(SolutionDir)", 0) == 0) { pathToSharedItemsFile = projectAttribute; - } else { + } + else { pathToSharedItemsFile = variables["ProjectDir"] + projectAttribute; } if (!simplifyPathWithVariables(pathToSharedItemsFile, variables)) { @@ -1040,6 +1212,24 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X } } } + else if (std::strcmp(name, "Import") == 0) { + const char *projectAttribute = node->Attribute("Project"); + if (projectAttribute) { + loadVisualStudioProperties(projectAttribute, variables, includePath, additionalIncludeDirectories, itemDefinitionGroupList, propertyGroupForcedIncludes, loadedProps); + // MSBuild honors the ForceImportBefore/AfterCppProps (and ...CppTargets) + // variables when importing Microsoft.Cpp.props / Microsoft.Cpp.targets; + // they are typically set by Directory.Build.props to inject custom files. + std::string importName(projectAttribute); + strTolower(importName); + if (importName.find("microsoft.cpp.props") != std::string::npos || importName.find("microsoft.cpp.targets") != std::string::npos) { + for (const char *forceVariable : {"ForceImportBeforeCppProps", "ForceImportAfterCppProps", "ForceImportBeforeCppTargets", "ForceImportAfterCppTargets"}) { + const auto it = variables.find(forceVariable); + if (it != variables.end() && !it->second.empty()) + loadVisualStudioProperties(it->second, variables, includePath, additionalIncludeDirectories, itemDefinitionGroupList, propertyGroupForcedIncludes, loadedProps); + } + } + } + } } // # 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 @@ -1047,14 +1237,13 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X // 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)); - } + // resolveIncludePath already resolved the files relative to the + // current directory (or to an absolute path), so do not prepend + // the shared items project directory again. + std::transform(sharedProject.sourceFiles.begin(), sharedProject.sourceFiles.end(), std::back_inserter(compileList), [](const std::string &file) { + return ItemGroupClCompile(file); + }); + std::copy(sharedProject.includePaths.begin(), sharedProject.includePaths.end(), std::back_inserter(sharedItemsIncludePaths)); } // Project files @@ -1073,11 +1262,25 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X continue; } + // resolve configuration-dependent variables in the file path + // (e.g. $(IntDir), $(GeneratedFilesDir)); configuration-independent + // paths were already resolved eagerly during parsing. Files whose + // path cannot be resolved are skipped (they typically reference + // build-generated files). + std::string file = compile.mFilename; + if (file.find("$(") != std::string::npos) { + std::map vars = variables; + vars["Configuration"] = p.configuration; + vars["Platform"] = p.platformStr; + if (!simplifyPathWithVariables(file, vars)) + 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 + FileSettings fs{ file, Standards::Language::None, 0}; // file will be identified later on fs.cfg = p.name; // TODO: detect actual MSC version fs.msc = true; @@ -1089,39 +1292,124 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X 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; + std::list globalForcedIncludeFiles; + for (const ForcedIncludeFiles &propForced : propertyGroupForcedIncludes) { + if (!propForced.condition.empty()) { + Conditional conditional(propForced.condition); + if (!conditional.conditionIsTrue(p, file, errors)) + continue; + } + for (const std::string &f : propForced.files) { + if (f.empty() || startsWith(f, "%(")) + continue; + globalForcedIncludeFiles.push_back(f); + } + } + + for (bool wantProps : {true, false}) { + for (const ItemDefinitionGroup &i : itemDefinitionGroupList) { + if (i.fromProps != wantProps) + continue; + if (!i.conditionIsTrue(p, file, 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; + std::list tempFiles; + if (!i.forcedIncludeFiles.empty()) { + for (const ForcedIncludeFiles &forcedInclude : i.forcedIncludeFiles) { + if (!forcedInclude.condition.empty()) { + Conditional conditional(forcedInclude.condition); + if (!conditional.conditionIsTrue(p, file, errors)) + continue; + } + for (const std::string &forcedFile : forcedInclude.files) { + if (forcedFile == "%(ForcedIncludeFiles)" || forcedFile == "%(ClCompile.ForcedIncludeFiles)") { + // self reference: append the value accumulated + // from earlier definitions (e.g. property sheets). + tempFiles.insert(tempFiles.end(), globalForcedIncludeFiles.begin(), globalForcedIncludeFiles.end()); + } + else if (forcedFile.find("%(") != std::string::npos) { + errors.emplace_back(file + ": Can't evaluate forced include file: " + forcedFile); + continue; + } + else { + tempFiles.push_back(forcedFile); + } + } + } + globalForcedIncludeFiles = tempFiles; + } + } } + + std::map configVariables = variables; + configVariables["Configuration"] = p.configuration; + configVariables["Platform"] = p.platformStr; bool useUnicode = false; for (const ConfigurationPropertyGroup &c : configurationPropertyGroups) { - if (!c.conditionIsTrue(p, compile.mFilename, errors)) + if (!c.conditionIsTrue(p, file, errors)) continue; // in msbuild the last definition wins useUnicode = c.useUnicode; fs.useMfc = c.useOfMfc; + for (const auto &prop : c.properties) + configVariables[prop.first] = prop.second; } if (useUnicode) { fs.defines += ";UNICODE=1;_UNICODE=1"; } fsSetDefines(fs, fs.defines); - fsSetIncludePaths(fs, Path::getPathFromFilename(compile.mFilename), toStringList(includePath + ';' + additionalIncludePaths), variables); + fsSetIncludePaths(fs, Path::getPathFromFilename(file), toStringList(includePath + ';' + additionalIncludePaths), configVariables); for (const auto &path : sharedItemsIncludePaths) { fs.includePaths.emplace_back(path); } + + if (!compile.forcedIncludeFiles.empty()) { + std::list temp; + for (const ForcedIncludeFiles &forcedInclude : compile.forcedIncludeFiles) { + if (!forcedInclude.condition.empty()) { + Conditional conditional(forcedInclude.condition); + if (!conditional.conditionIsTrue(p, file, errors)) + continue; + } + for (const std::string &perFile : forcedInclude.files) { + if (perFile == "%(ForcedIncludeFiles)") { + if (!globalForcedIncludeFiles.empty()) + temp.insert(temp.end(), globalForcedIncludeFiles.begin(), globalForcedIncludeFiles.end()); + } else if (perFile.find("%(") != std::string::npos) { + errors.emplace_back(file + ": Can't evaluate forced include file: " + perFile); + } else { + temp.emplace_back(perFile); + } + } + } + fs.forcedIncludes = temp; + } else if (!globalForcedIncludeFiles.empty()) { + fs.forcedIncludes = globalForcedIncludeFiles; + } + + // resolve configuration-dependent variables in forced include paths + // (e.g. $(GeneratedFilesDir), $(IntDir)); configuration-independent + // paths were already resolved eagerly during parsing. + for (std::string &forcedFile : fs.forcedIncludes) { + if (forcedFile.find("$(") == std::string::npos) + continue; + std::string resolved = forcedFile; + if (simplifyPathWithVariables(resolved, configVariables)) + forcedFile = std::move(resolved); + } + fileSettings.push_back(std::move(fs)); } } @@ -1129,7 +1417,9 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X return true; } -ImportProject::SharedItemsProject ImportProject::importVcxitems(const std::string& filename, const std::vector& fileFilters, std::vector &cache) +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; @@ -1155,14 +1445,27 @@ ImportProject::SharedItemsProject ImportProject::importVcxitems(const std::strin errors.emplace_back("Visual Studio project file has no XML root node"); return result; } + + // sourceDir is the directory of the .vcxitems file itself — the correct + // base for resolving $(MSBuildThisFileDirectory) and relative paths. + const std::string sourceDir = Path::getPathFromFilename(filename); + + // variables for this shared items project: at minimum MSBuildThisFileDirectory + // and ProjectDir should point at the .vcxitems directory so that + // resolveIncludePath (called inside ItemDefinitionGroup) works correctly. + std::map vcxitemsVariables; + vcxitemsVariables["MSBuildThisFileDirectory"] = sourceDir; + vcxitemsVariables["ProjectDir"] = sourceDir; + 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)", "./"); + // Resolve to absolute using sourceDir, exactly like + // the ClCompile Include= handling in importVcxproj. + std::string file = resolveIncludePath(include, sourceDir, vcxitemsVariables); // Skip file if it doesn't match the filter if (!fileFilters.empty() && !filtermatcher.match(file)) @@ -1173,18 +1476,17 @@ ImportProject::SharedItemsProject ImportProject::importVcxitems(const std::strin errors.emplace_back("Could not find shared items source file"); return result; } + } else if (std::strcmp(e->Name(), "ItemDefinitionGroup") == 0) { + ItemDefinitionGroup temp(e, "", sourceDir, vcxitemsVariables); + for (const auto& includePath : toStringList(temp.additionalIncludePaths)) { + if (includePath == "%(AdditionalIncludeDirectories)") + continue; + if (includePath.find("$(") != std::string::npos) + errors.emplace_back(filename + ": Can't evaluate include path: " + includePath); + result.includePaths.emplace_back(includePath); + } } } - } else if (std::strcmp(node->Name(), "ItemDefinitionGroup") == 0) { - ItemDefinitionGroup temp(node, ""); - for (const auto& includePath : toStringList(temp.additionalIncludePaths)) { - if (includePath == "%(AdditionalIncludeDirectories)") - continue; - - std::string toAdd(includePath); - findAndReplace(toAdd, "$(MSBuildThisFileDirectory)", "./"); - result.includePaths.emplace_back(toAdd); - } } } 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.cppcheck b/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.cppcheck new file mode 100644 index 00000000000..b6f747fd04b --- /dev/null +++ b/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.cppcheck @@ -0,0 +1,16 @@ + + + exclude-cppcheck-build-dir + vcxproj_forced_includes.slnx + false + true + true + true + 2 + 100 + + Debug + + + exclude + 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..e9f5ca12faf --- /dev/null +++ b/test/cli/vcxproj_forced_includes/vcxproj_forced_includes.props @@ -0,0 +1,8 @@ + + + + PropsDebugX64.h + PropsReleaseX64.h + + + 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..900a673f626 --- /dev/null +++ b/test/cli/vcxproj_forced_includes_test.py @@ -0,0 +1,34 @@ + +# 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(): + args = [ + '--template=cppcheck1', + '--project=vcxproj_forced_includes/vcxproj_forced_includes.cppcheck', + '--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) diff --git a/test/testimportproject.cpp b/test/testimportproject.cpp index 873272030f6..4d5b9dfb1cb 100644 --- a/test/testimportproject.cpp +++ b/test/testimportproject.cpp @@ -83,6 +83,9 @@ class TestImportProject : public TestFixture { TEST_CASE(importCppcheckGuiProjectPremiumMisra); TEST_CASE(ignorePaths); TEST_CASE(testVcxprojUnicode); + TEST_CASE(testVcxprojConfigurationPropertyGroups); + TEST_CASE(testVcxprojForcedIncludeFiles); + TEST_CASE(testVcxprojUnresolvableCompileInclude); TEST_CASE(testCollectArgs1); TEST_CASE(testCollectArgs2); TEST_CASE(testCollectArgs3); @@ -648,6 +651,200 @@ class TestImportProject : public TestFixture { ASSERT_EQUALS(project.fileSettings.back().useMfc, true); } + void testVcxprojConfigurationPropertyGroups() const + { + // Properties in Configuration PropertyGroups are configuration-specific, + // so they must be resolved per configuration when building include paths. + const char vcxproj[] = R"-( + + + + + Debug + x64 + + + Release + x64 + + + + Application + Unicode + $(RepoRoot)generated\ + + + Application + NotSet + $(RepoRoot)generated\release\ + + + + $(GeneratedFilesDir)includes;%(AdditionalIncludeDirectories) + $(GeneratedFilesDir)forced.h;%(ForcedIncludeFiles) + + + + + $(GeneratedFilesDir)includes;%(AdditionalIncludeDirectories) + $(GeneratedFilesDir)forced.h;%(ForcedIncludeFiles) + + + + + + +)-"; + tinyxml2::XMLDocument doc; + ASSERT_EQUALS(tinyxml2::XML_SUCCESS, doc.Parse(vcxproj, sizeof(vcxproj))); + TestImporter project; + std::map variables; + variables["RepoRoot"] = "C:/root/"; + std::vector cache; + ASSERT_EQUALS(project.importVcxproj("test.vcxproj", doc, variables, {}, {}, cache), true); + ASSERT_EQUALS(project.fileSettings.size(), 2); + ASSERT_EQUALS(1U, project.fileSettings.front().includePaths.size()); + ASSERT_EQUALS("C:/root/generated/includes/", project.fileSettings.front().includePaths.front()); + const std::list frontForced{ "C:/root/generated/forced.h" }; + ASSERT_EQUALS(frontForced.size(), project.fileSettings.front().forcedIncludes.size()); + ASSERT(project.fileSettings.front().forcedIncludes == frontForced); + ASSERT_EQUALS(1U, project.fileSettings.back().includePaths.size()); + ASSERT_EQUALS("C:/root/generated/release/includes/", project.fileSettings.back().includePaths.front()); + const std::list backForced{ "C:/root/generated/release/forced.h" }; + ASSERT_EQUALS(backForced.size(), project.fileSettings.back().forcedIncludes.size()); + ASSERT(project.fileSettings.back().forcedIncludes == backForced); + } + + void testVcxprojForcedIncludeFiles() const + { + const char vcxproj[] = R"-( + + + + + Debug + x64 + + + Release + x64 + + + + {A9D955DC-E173-4F16-638A-6DBC2C3013E9} + + + + + Application + true + v145 + Unicode + + + Application + false + v145 + NotSet + Static + + + + + + _DEBUG;_CONSOLE;FILE1;%(PreprocessorDefinitions) + true + stdcpp20 + $(MSBuildThisFileDirectory)file1.h;%(ForcedIncludeFiles) + + + + + _DEBUG;_CONSOLE;FILE2;%(PreprocessorDefinitions) + true + stdcpp20 + $(MSBuildThisFileDirectory)file2.h;%(ClCompile.ForcedIncludeFiles) + + + + + NDEBUG;_CONSOLE;GLOBALX64;%(PreprocessorDefinitions) + stdcpp20 + $(MSBuildThisFileDirectory)GlobalReleaseX64.h;%(ForcedIncludeFiles) + + + + + $(MSBuildThisFileDirectory)AllX64.h + $(MSBuildThisFileDirectory)DebugX64.h;%(ForcedIncludeFiles) + $(MSBuildThisFileDirectory)ReleaseX64.h;%(ForcedIncludeFiles) + true + + + $(MSBuildThisFileDirectory)AllX64.h + $(MSBuildThisFileDirectory)DebugX64.h;%(ForcedIncludeFiles) + $(MSBuildThisFileDirectory)ReleaseX64.h;%(ForcedIncludeFiles) + true + + + + +)-"; + 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); + const std::list front{ "AllX64.h", "DebugX64.h", "file2.h", "file1.h" }; + size_t size = front.size(); + ASSERT_EQUALS(project.fileSettings.front().forcedIncludes.size(), size); + ASSERT(project.fileSettings.front().forcedIncludes == front); + const std::list back{ "AllX64.h", "ReleaseX64.h", "GlobalReleaseX64.h" }; + size = back.size(); + ASSERT_EQUALS(project.fileSettings.back().forcedIncludes.size(), size); + ASSERT(project.fileSettings.back().forcedIncludes == back); + } + + void testVcxprojUnresolvableCompileInclude() const + { + // ClCompile includes whose paths contain configuration-dependent or + // undefined MSBuild variables must not abort the import. Paths that + // can be resolved (e.g. $(RepoRoot)) are analyzed; the rest are skipped. + const char vcxproj[] = R"-( + + + + + Debug + x64 + + + + $(IntDir)Generated Files\ + + + + + + + +)-"; + tinyxml2::XMLDocument doc; + ASSERT_EQUALS(tinyxml2::XML_SUCCESS, doc.Parse(vcxproj, sizeof(vcxproj))); + TestImporter project; + std::map variables; + variables["RepoRoot"] = "C:/root/"; + std::vector cache; + ASSERT_EQUALS(project.importVcxproj("test.vcxproj", doc, variables, {}, {}, cache), true); + // the unresolvable $(GeneratedFilesDir) entry must not produce an error + ASSERT_EQUALS(0U, project.errors.size()); + ASSERT_EQUALS(2U, project.fileSettings.size()); + ASSERT_EQUALS("main.cpp", project.fileSettings.front().filename()); + ASSERT_EQUALS("C:/root/shared/interop.cpp", project.fileSettings.back().filename()); + } + void testCollectArgs1() const { std::vector args;