diff --git a/integration_test.py b/integration_test.py index a59ae338..f3560fe1 100644 --- a/integration_test.py +++ b/integration_test.py @@ -4,23 +4,110 @@ import pathlib import platform import pytest -from testutils import simplecpp, format_include_path_arg, format_include - -def __test_relative_header_create_header(dir, with_pragma_once=True): - header_file = os.path.join(dir, 'test.h') +from testutils import ( + simplecpp, + format_include_path_arg, + format_isystem_path_arg, + format_framework_path_arg, + format_iframework_path_arg, + format_include, +) + +def __test_create_header(dir, hdr_relpath, with_pragma_once=True, already_included_error_msg=None): + """ + Creates a C header file under `dir/hdr_relpath` with simple include guards. + + The file contains: + - optional `#pragma once` (when `with_pragma_once=True`) + - a header guard derived from `hdr_relpath` (e.g. "test.h" -> TEST_H_INCLUDED) + - optional `#error ` if the guard is already defined + - a dummy non-preprocessor declaration to force line emission + + Return absolute path to the created header file. + """ + inc_guard = hdr_relpath.upper().replace(".", "_") # "test.h" -> "TEST_H" + header_file = os.path.join(dir, hdr_relpath) + os.makedirs(os.path.dirname(header_file), exist_ok=True) with open(header_file, 'wt') as f: f.write(f""" {"#pragma once" if with_pragma_once else ""} - #ifndef TEST_H_INCLUDED - #define TEST_H_INCLUDED + #ifndef {inc_guard}_INCLUDED + #define {inc_guard}_INCLUDED #else - #error header_was_already_included + {f"#error {already_included_error_msg}" if already_included_error_msg else ""} #endif - const int dummy = 1; + int __force_line_emission; /* anything non-preprocessor */ + """) + return header_file + +def __test_create_source(dir, include, is_include_sys=False): + """ + Creates a minimal C source file that includes a single header. + + The generated `/test.c` contains one `#include` directive. + If `is_include_sys` is True, the include is written as `<...>`; + otherwise it is written as `"..."`. + + Returns absolute path to the created header file. + """ + src_file = os.path.join(dir, 'test.c') + with open(src_file, 'wt') as f: + f.write(f""" + #include {format_include(include, is_include_sys)} """) - return header_file, "error: #error header_was_already_included" + return src_file + +def __test_create_framework(dir, fw_name, hdr_relpath, content="", private=False): + """ + Creates a minimal Apple-style framework layout containing one header. + + The generated structure is: + `/.framework/{Headers|PrivateHeaders}/` + + The header file contains the given `content` followed by a dummy + declaration to force line emission. + + Returns absolute path to the created header file. + """ + fwdir = os.path.join(dir, f"{fw_name}.framework", "PrivateHeaders" if private else "Headers") + header_file = os.path.join(fwdir, hdr_relpath) + os.makedirs(os.path.dirname(header_file), exist_ok=True) + with open(header_file, "wt", encoding="utf-8") as f: + f.write(f""" + {content} + int __force_line_emission; /* anything non-preprocessor */ + """) + return header_file + +def __test_relative_header_create_header(dir, with_pragma_once=True): + """ + Creates a local `test.h` header with both `#pragma once` (optional) + and a macro guard. + + The header emits `#error header_was_already_included` if it is + re-included past the guard. + + Returns tuple of: + - absolute path to the created header file + - expected compiler error substring for duplicate inclusion + """ + already_included_error_msg="header_was_already_included" + header_file = __test_create_header( + dir, "test.h", with_pragma_once=with_pragma_once, already_included_error_msg=already_included_error_msg) + return header_file, f"error: #error {already_included_error_msg}" def __test_relative_header_create_source(dir, include1, include2, is_include1_sys=False, is_include2_sys=False, inv=False): + """ + Creates a C source file that includes two headers in order. + + The generated `/test.c`: + - `#undef TEST_H_INCLUDED` to reset the guard in `test.h` + - includes `include1` then `include2` + - if `inv=True`, the order is swapped (`include2` then `include1`) + - each include can be written as `<...>` or `"..."` + + Returns absolute path to the created source file. + """ if inv: return __test_relative_header_create_source(dir, include1=include2, include2=include1, is_include1_sys=is_include2_sys, is_include2_sys=is_include1_sys) ## otherwise @@ -236,6 +323,211 @@ def test_same_name_header(record_property, tmpdir): assert "OK" in stdout assert stderr == "" +@pytest.mark.parametrize("is_sys", (False, True)) +@pytest.mark.parametrize("is_iframework", (False, True)) +@pytest.mark.parametrize("is_private", (False, True)) +def test_framework_lookup(record_property, tmpdir, is_sys, is_iframework, is_private): + # Arrange framework: /FwRoot/MyKit.framework/(Headers|PrivateHeaders)/Component.h + fw_root = os.path.join(tmpdir, "FwRoot") + __test_create_framework(fw_root, "MyKit", "Component.h", private=is_private) + + test_file = __test_create_source(tmpdir, "MyKit/Component.h", is_include_sys=is_sys) + + args = [format_iframework_path_arg(fw_root) if is_iframework else format_framework_path_arg(fw_root), test_file] + _, stdout, stderr = simplecpp(args, cwd=tmpdir) + record_property("stdout", stdout) + record_property("stderr", stderr) + + assert stderr == "" + relative = "PrivateHeaders" if is_private else "Headers" + assert f'#line 3 "{pathlib.PurePath(tmpdir).as_posix()}/FwRoot/MyKit.framework/{relative}/Component.h"' in stdout + +@pytest.mark.parametrize("is_sys", (False, True)) +@pytest.mark.parametrize( + "order,expected", + [ + # Note: + # - `I1` / `ISYS1` / `F1` / `IFW1` point to distinct directories and contain `Component_1.h` (a decoy). + # - `I` / `ISYS` / `F` / `IFW` point to directories that contain `Component.h`, which the + # translation unit (TU) includes via `#include "MyKit/Component.h"`. + # + # This makes the winning flag (-I, -isystem, -F, or -iframework) uniquely identifiable + # in the preprocessor `#line` output. + + # Sanity checks + (("I",), "I"), + (("ISYS",), "ISYS"), + (("F",), "F"), + (("IFW",), "IFW"), + + # Includes (-I) + (("I1", "I"), "I"), + (("I", "I1"), "I"), + # Includes (-I) duplicates + (("I1", "I", "I1"), "I"), + (("I", "I1", "I"), "I"), + + # System includes (-isystem) + (("ISYS1", "ISYS"), "ISYS"), + (("ISYS", "ISYS1"), "ISYS"), + # System includes (-isystem) duplicates + (("ISYS1", "ISYS", "ISYS1"), "ISYS"), + (("ISYS", "ISYS1", "ISYS"), "ISYS"), + + # Framework (-F) + (("F1", "F"), "F"), + (("F", "F1"), "F"), + # Framework (-F) duplicates + (("F1", "F", "F1"), "F"), + (("F", "F1", "F"), "F"), + + # System framework (-iframework) + (("IFW1", "IFW"), "IFW"), + (("IFW", "IFW1"), "IFW"), + # System framework (-iframework) duplicates + (("IFW1", "IFW", "IFW1"), "IFW"), + (("IFW", "IFW1", "IFW"), "IFW"), + + # -I and -F are processed as specified (left-to-right) + (("I", "F"), "I"), + (("I1", "I", "F"), "I"), + (("F", "I"), "F"), + (("F1", "F", "I"), "F"), + + # -I and -F takes precedence over -isystem + (("I", "ISYS"), "I"), + (("F", "ISYS"), "F"), + (("ISYS", "F"), "F"), + (("ISYS", "I", "F"), "I"), + (("ISYS", "I1", "F1", "I", "F"), "I"), + (("ISYS", "I"), "I"), + (("ISYS", "F", "I"), "F"), + (("ISYS", "F1", "I1", "F", "I"), "F"), + + # -I and -F beat system framework (-iframework) + (("I", "IFW"), "I"), + (("F", "IFW"), "F"), + (("IFW", "F"), "F"), + (("IFW", "I", "F"), "I"), + (("IFW", "I1", "F1", "I", "F"), "I"), + (("IFW", "I"), "I"), + (("IFW", "F", "I"), "F"), + (("IFW", "F1", "I1", "F", "I"), "F"), + + # system include (-isystem) beats system framework (-iframework) + (("ISYS", "IFW"), "ISYS"), + (("IFW", "ISYS"), "ISYS"), + (("IFW1", "ISYS1", "IFW", "ISYS"), "ISYS"), + (("I1", "F1", "IFW1", "ISYS1", "IFW", "ISYS"), "ISYS"), + ], +) +def test_searchpath_order(record_property, tmpdir, is_sys, order, expected): + """ + Validate include resolution order across -I (user include), + -isystem (system include), -F (user framework), and + -iframework (system framework) using a minimal file layout, + asserting which physical header path appears in the preprocessor #line output. + + The test constructs four parallel trees (two entries per kind): + - inc{,_1}/MyKit/Component{,_1}.h # for -I + - isys{,_1}/MyKit/Component{,_1}.h # for -isystem + - Fw{,_1}/MyKit.framework/Headers/Component{,_1}.h # for -F + - SysFw{,_1}/MyKit.framework/Headers/Component{,_1}.h # for -iframework + + It then preprocesses a TU that includes "MyKit/Component.h" (or <...> when + is_sys=True), assembles compiler args in the exact `order`, and asserts that + only the expected path appears in #line. Distinct names (Component.h vs + Component_1.h) ensure a unique winner per bucket. + + References: + - https://gcc.gnu.org/onlinedocs/cpp/Invocation.html#Invocation + - https://gcc.gnu.org/onlinedocs/gcc/Darwin-Options.html + """ + + # Create two include dirs, two user framework dirs, and two system framework dirs + inc_dirs, isys_dirs, fw_dirs, sysfw_dirs = [], [], [], [] + + def _suffix(idx: int) -> str: + return f"_{idx}" if idx > 0 else "" + + for idx in range(2): + # -I paths + inc_dir = os.path.join(tmpdir, f"inc{_suffix(idx)}") + __test_create_header(inc_dir, hdr_relpath=f"MyKit/Component{_suffix(idx)}.h") + inc_dirs.append(inc_dir) + + # -isystem paths (system includes) + isys_dir = os.path.join(tmpdir, f"isys{_suffix(idx)}") + __test_create_header(isys_dir, hdr_relpath=f"MyKit/Component{_suffix(idx)}.h") + isys_dirs.append(isys_dir) + + # -F paths (user frameworks) + fw_dir = os.path.join(tmpdir, f"Fw{_suffix(idx)}") + __test_create_framework(fw_dir, "MyKit", f"Component{_suffix(idx)}.h") + fw_dirs.append(fw_dir) + + # -iframework paths (system frameworks) + sysfw_dir = os.path.join(tmpdir, f"SysFw{_suffix(idx)}") + __test_create_framework(sysfw_dir, "MyKit", f"Component{_suffix(idx)}.h") + sysfw_dirs.append(sysfw_dir) + + # Translation unit under test: include MyKit/Component.h (quote or system form) + test_file = __test_create_source(tmpdir, "MyKit/Component.h", is_include_sys=is_sys) + + def idx_from_flag(prefix: str, flag: str) -> int: + """Extract numeric suffix from tokens like 'I1', 'ISYS1', 'F1', 'IFW1'. + Returns 0 when no suffix is present (e.g., 'I', 'ISYS', 'F', 'IFW').""" + return int(flag[len(prefix):]) if len(flag) > len(prefix) else 0 + + # Build argv in the exact order requested by `order` + args = [] + for flag in order: + if flag in ["I", "I1"]: + args.append(format_include_path_arg(inc_dirs[idx_from_flag("I", flag)])) + elif flag in ["ISYS", "ISYS1"]: + args.append(format_isystem_path_arg(isys_dirs[idx_from_flag("ISYS", flag)])) + elif flag in ["F", "F1"]: + args.append(format_framework_path_arg(fw_dirs[idx_from_flag("F", flag)])) + elif flag in ["IFW", "IFW1"]: + args.append(format_iframework_path_arg(sysfw_dirs[idx_from_flag("IFW", flag)])) + else: + raise AssertionError(f"unknown flag in order: {flag}") + args.append(test_file) + + # Run the preprocessor and capture outputs + _, stdout, stderr = simplecpp(args, cwd=tmpdir) + record_property("stdout", stdout) + record_property("stderr", stderr) + + # Resolve the absolute expected/forbidden paths we want to see in #line output + root = pathlib.PurePath(tmpdir).as_posix() + + inc_paths = [f"{root}/inc{_suffix(idx)}/MyKit/Component{_suffix(idx)}.h" for idx in range(2)] + isys_paths = [f"{root}/isys{_suffix(idx)}/MyKit/Component{_suffix(idx)}.h" for idx in range(2)] + fw_paths = [f"{root}/Fw{_suffix(idx)}/MyKit.framework/Headers/Component{_suffix(idx)}.h" for idx in range(2)] + ifw_paths = [f"{root}/SysFw{_suffix(idx)}/MyKit.framework/Headers/Component{_suffix(idx)}.h" for idx in range(2)] + all_candidate_paths = [*inc_paths, *isys_paths, *fw_paths, *ifw_paths] + + # Compute the single path we expect to appear + expected_path = None + if expected in ["I", "I1"]: + expected_path = inc_paths[idx_from_flag("I", expected)] + elif expected in ["ISYS", "ISYS1"]: + expected_path = isys_paths[idx_from_flag("ISYS", expected)] + elif expected in ["F", "F1"]: + expected_path = fw_paths[idx_from_flag("F", expected)] + elif expected in ["IFW", "IFW1"]: + expected_path = ifw_paths[idx_from_flag("IFW", expected)] + assert expected_path is not None, "test configuration error: expected token not recognized" + + # Assert ONLY the expected path appears in the preprocessor #line output + assert expected_path in stdout + for p in (p for p in all_candidate_paths if p != expected_path): + assert p not in stdout + + # No diagnostics expected + assert stderr == "" + def test_pragma_once_matching(record_property, tmpdir): test_dir = os.path.join(tmpdir, "test_dir") test_subdir = os.path.join(test_dir, "test_subdir") diff --git a/main.cpp b/main.cpp index a6d14386..1920f82b 100644 --- a/main.cpp +++ b/main.cpp @@ -43,7 +43,13 @@ int main(int argc, char **argv) } case 'I': { // include path const char * const value = arg[2] ? (argv[i] + 2) : argv[++i]; - dui.includePaths.push_back(value); + dui.addIncludePath(value, /* legacy= */ false); + found = true; + break; + } + case 'F': { // framework path + const char * const value = arg[2] ? (argv[i] + 2) : argv[++i]; + dui.addFrameworkPath(value); found = true; break; } @@ -51,9 +57,15 @@ int main(int argc, char **argv) if (std::strncmp(arg, "-include=",9)==0) { dui.includes.push_back(arg+9); found = true; + } else if (std::strncmp(arg, "-isystem", 8) == 0) { + dui.searchPaths.push_back({arg + 8, simplecpp::DUI::PathKind::SystemInclude}); + found = true; } else if (std::strncmp(arg, "-is",3)==0) { use_istream = true; found = true; + } else if (std::strncmp(arg, "-iframework", 11) == 0) { + dui.addSystemFrameworkPath(arg + 11); + found = true; } break; case 's': @@ -100,6 +112,9 @@ int main(int argc, char **argv) std::cout << "simplecpp [options] filename" << std::endl; std::cout << " -DNAME Define NAME." << std::endl; std::cout << " -IPATH Include path." << std::endl; + std::cout << " -isystemPATH System include path." << std::endl; + std::cout << " -FPATH Framework path." << std::endl; + std::cout << " -iframeworkPATH System framework path." << std::endl; std::cout << " -include=FILE Include FILE." << std::endl; std::cout << " -UNAME Undefine NAME." << std::endl; std::cout << " -std=STD Specify standard." << std::endl; diff --git a/simplecpp.cpp b/simplecpp.cpp index 84e4b54b..51d33caf 100644 --- a/simplecpp.cpp +++ b/simplecpp.cpp @@ -13,6 +13,7 @@ #include "simplecpp.h" #include +#include #include #include #include @@ -2429,6 +2430,27 @@ static bool isAbsolutePath(const std::string &path) } #endif +namespace { + // "" -> "" (and PrivateHeaders variant). + // Returns candidates in priority order (Headers, then PrivateHeaders). + inline std::array + toAppleFrameworkRelatives(const std::string& header) + { + const std::size_t slash = header.find('/'); + if (slash == std::string::npos) + return { header, header }; // no transformation applicable + const std::string pkg = header.substr(0, slash); + const std::string tail = header.substr(slash); // includes '/' + return { pkg + ".framework/Headers" + tail, + pkg + ".framework/PrivateHeaders" + tail }; + } + + inline void push_unique(std::vector &v, std::string p) { + if (!p.empty() && (v.empty() || v.back() != p)) + v.push_back(std::move(p)); + } +} + namespace simplecpp { /** * perform path simplifications for . and .. @@ -2999,12 +3021,61 @@ static std::string openHeader(std::ifstream &f, const simplecpp::DUI &dui, const } } - // search the header on the include paths (provided by the flags "-I...") - for (const auto &includePath : dui.includePaths) { - std::string path = openHeaderDirect(f, simplecpp::simplifyPath(includePath + "/" + header)); - if (!path.empty()) - return path; + // Build an ordered, typed path list: + // - Prefer DUI::searchPaths when provided (interleaved -I/-F/-iframework). + // - Otherwise mirror legacy includePaths into Include entries (back-compat). + std::vector searchPaths; + if (!dui.searchPaths.empty()) { + searchPaths = dui.searchPaths; + } else { + searchPaths.reserve(dui.includePaths.size()); + for (const auto &includePath : dui.includePaths) + searchPaths.push_back({includePath, simplecpp::DUI::PathKind::Include}); } + + // Interleave -I and -F in CLI order + for (const auto &searchPath : searchPaths) { + if (searchPath.kind == simplecpp::DUI::PathKind::Include) { + const std::string path = openHeaderDirect(f, simplecpp::simplifyPath(searchPath.path + "/" + header)); + if (!path.empty()) + return path; + } else if (searchPath.kind == simplecpp::DUI::PathKind::Framework) { + // try Headers then PrivateHeaders + const auto relatives = toAppleFrameworkRelatives(header); + if (relatives[0] != header) { // Skip if no framework rewrite was applied. + for (const auto &rel : relatives) { + const std::string frameworkPath = openHeaderDirect(f, simplecpp::simplifyPath(searchPath.path + "/" + rel)); + if (!frameworkPath.empty()) + return frameworkPath; + } + } + } + } + + // -isystem + for (const auto &searchPath : searchPaths) { + if (searchPath.kind == simplecpp::DUI::PathKind::SystemInclude) { + std::string path = openHeaderDirect(f, simplecpp::simplifyPath(searchPath.path + "/" + header)); + if (!path.empty()) + return path; + } + } + + // -iframework + for (const auto &searchPath : searchPaths) { + if (searchPath.kind == simplecpp::DUI::PathKind::SystemFramework) { + const auto relatives = toAppleFrameworkRelatives(header); + if (relatives[0] != header) { // Skip if no framework rewrite was applied. + // Try Headers then PrivateHeaders + for (const auto &rel : relatives) { + const std::string frameworkPath = openHeaderDirect(f, simplecpp::simplifyPath(searchPath.path + "/" + rel)); + if (!frameworkPath.empty()) + return frameworkPath; + } + } + } + } + return ""; } @@ -3036,6 +3107,7 @@ std::pair simplecpp::FileDataCache::tryload(FileDat std::pair simplecpp::FileDataCache::get(const std::string &sourcefile, const std::string &header, const simplecpp::DUI &dui, bool systemheader, std::vector &filenames, simplecpp::OutputList *outputList) { + // Absolute path: load directly if (isAbsolutePath(header)) { auto ins = mNameMap.emplace(simplecpp::simplifyPath(header), nullptr); @@ -3051,32 +3123,78 @@ std::pair simplecpp::FileDataCache::get(const std:: return {nullptr, false}; } + // Build ordered candidates. + std::vector candidates; + + // Prefer first to search the header relatively to source file if found, when not a system header if (!systemheader) { - auto ins = mNameMap.emplace(simplecpp::simplifyPath(dirPath(sourcefile) + header), nullptr); + push_unique(candidates, simplecpp::simplifyPath(dirPath(sourcefile) + header)); + } - if (ins.second) { - const auto ret = tryload(ins.first, dui, filenames, outputList); - if (ret.first != nullptr) { - return ret; + // Build an ordered, typed path list: + // - Prefer DUI::searchPaths when provided (interleaved -I/-F/-iframework). + // - Otherwise mirror legacy includePaths into Include entries (back-compat). + std::vector searchPaths; + if (!dui.searchPaths.empty()) { + searchPaths = dui.searchPaths; + } else { + searchPaths.reserve(dui.includePaths.size()); + for (const auto &p : dui.includePaths) + searchPaths.push_back({p, simplecpp::DUI::PathKind::Include}); + } + + // Interleave -I and -F in CLI order + for (const auto &searchPath : searchPaths) { + if (searchPath.kind == simplecpp::DUI::PathKind::Include) { + push_unique(candidates, simplecpp::simplifyPath(searchPath.path + "/" + header)); + } else if (searchPath.kind == simplecpp::DUI::PathKind::Framework) { + // Try Headers then PrivateHeaders + const auto relatives = toAppleFrameworkRelatives(header); + if (relatives[0] != header) { // Skip if no framework rewrite was applied. + push_unique(candidates, simplecpp::simplifyPath(searchPath.path + "/" + relatives[0])); + push_unique(candidates, simplecpp::simplifyPath(searchPath.path + "/" + relatives[1])); } - } else if (ins.first->second != nullptr) { - return {ins.first->second, false}; } } - for (const auto &includePath : dui.includePaths) { - auto ins = mNameMap.emplace(simplecpp::simplifyPath(includePath + "/" + header), nullptr); + // -isystem + for (const auto &searchPath : searchPaths) { + if (searchPath.kind == DUI::PathKind::SystemInclude) { + push_unique(candidates, simplecpp::simplifyPath(searchPath.path + "/" + header)); + } + } - if (ins.second) { - const auto ret = tryload(ins.first, dui, filenames, outputList); - if (ret.first != nullptr) { - return ret; + // -iframework + for (const auto &searchPath : searchPaths) { + if (searchPath.kind == simplecpp::DUI::PathKind::SystemFramework) { + // Try Headers then PrivateHeaders + const auto relatives = toAppleFrameworkRelatives(header); + if (relatives[0] != header) { // Skip if no framework rewrite was applied. + push_unique(candidates, simplecpp::simplifyPath(searchPath.path + "/" + relatives[0])); + push_unique(candidates, simplecpp::simplifyPath(searchPath.path + "/" + relatives[1])); } - } else if (ins.first->second != nullptr) { - return {ins.first->second, false}; } } + // Try loading each candidate path (left-to-right). + for (const std::string &candidate : candidates) { + // Already loaded? + auto it = mNameMap.find(candidate); + if (it != mNameMap.end()) { + return {it->second, false}; + } + + auto ins = mNameMap.emplace(candidate, static_cast(nullptr)); + const auto ret = tryload(ins.first, dui, filenames, outputList); + if (ret.first != nullptr) { + return ret; + } + + // Failed: remove placeholder so we can retry later if needed. + mNameMap.erase(ins.first); + } + + // Not found. return {nullptr, false}; } diff --git a/simplecpp.h b/simplecpp.h index ac367154..8b45c9c8 100644 --- a/simplecpp.h +++ b/simplecpp.h @@ -395,13 +395,70 @@ namespace simplecpp { /** * Command line preprocessor settings. - * On the command line these are configured by -D, -U, -I, --include, -std + * + * Mirrors typical compiler options: + * -D = Add macro definition + * -U Undefine macro + * -I Add include search directory + * -isystem Add system include search directory + * -F Add framework search directory (Darwin) + * -iframework Add system framework search directory (Darwin) + * --include Force inclusion of a header + * -std= Select language standard (C++17, C23, etc.) + * + * Path search behavior: + * - If searchPaths is non-empty, it is used directly, preserving the + * left-to-right order and distinguishing between Include, Framework, + * and SystemFramework kinds. + * - If searchPaths is empty, legacy includePaths is used instead, and + * each entry is treated as a normal Include path (for backward + * compatibility). */ struct SIMPLECPP_LIB DUI { DUI() : clearIncludeCache(false), removeComments(false) {} + + // Typed search path entry. Mirrors GCC behavior for -I, -isystem, -F, -iframework. + enum class PathKind { Include, SystemInclude, Framework, SystemFramework }; + struct SearchPath { + std::string path; + PathKind kind; + }; + + /** + * Mirrors compiler option -I + * + * If 'legacy' is true, the path is added to the 'includePaths' vector; + * otherwise, it is added to 'searchPaths' with 'PathKind::Include'. + */ + void addIncludePath(const std::string& path, bool legacy=false) { + if (legacy) { + includePaths.push_back(path); + } else { + searchPaths.push_back({path, PathKind::Include}); + } + } + /** Mirrors compiler option -I */ + void addSystemIncludePath(const std::string& path) { + searchPaths.push_back({path, PathKind::SystemInclude}); + } + /** Mirrors compiler option -F */ + void addFrameworkPath(const std::string& path) { + searchPaths.push_back({path, PathKind::Framework}); + } + /** Mirrors compiler option -iframework */ + void addSystemFrameworkPath(const std::string& path) { + searchPaths.push_back({path, PathKind::SystemFramework}); + } + std::list defines; std::set undefined; + + // Back-compat: legacy -I list. If searchPaths is empty at use time, + // consumers should mirror includePaths -> searchPaths as Include. std::list includePaths; + // New: ordered, interleaved search paths with kind. + std::vector searchPaths; + std::list includes; std::string std; bool clearIncludeCache; diff --git a/test.cpp b/test.cpp index 0ecaa3b1..a96b778b 100644 --- a/test.cpp +++ b/test.cpp @@ -1551,7 +1551,7 @@ static void hashhash_universal_character() ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'A', Invalid ## usage when expanding 'A': Combining '\\u01' and '04' yields universal character '\\u0104'. This is undefined behavior according to C standard chapter 5.1.1.2, paragraph 4.\n", toString(outputList)); } -static void has_include_1() +static void has_include_1(bool legacy) { const char code[] = "#ifdef __has_include\n" " #if __has_include(\"simplecpp.h\")\n" @@ -1561,15 +1561,21 @@ static void has_include_1() " #endif\n" "#endif"; simplecpp::DUI dui; - dui.includePaths.push_back(testSourceDir); + dui.addIncludePath(testSourceDir, legacy); dui.std = "c++17"; ASSERT_EQUALS("\n\nA", preprocess(code, dui)); dui.std = "c++14"; ASSERT_EQUALS("", preprocess(code, dui)); ASSERT_EQUALS("", preprocess(code)); } +static void has_include_1() { + has_include_1(false); +} +static void has_include_1_legacy() { + has_include_1(true); +} -static void has_include_2() +static void has_include_2(bool legacy) { const char code[] = "#if defined( __has_include)\n" " #if /*comment*/ __has_include /*comment*/(\"simplecpp.h\") // comment\n" @@ -1579,13 +1585,19 @@ static void has_include_2() " #endif\n" "#endif"; simplecpp::DUI dui; - dui.includePaths.push_back(testSourceDir); + dui.addIncludePath(testSourceDir, legacy); dui.std = "c++17"; ASSERT_EQUALS("\n\nA", preprocess(code, dui)); ASSERT_EQUALS("", preprocess(code)); } +static void has_include_2() { + has_include_2(false); +} +static void has_include_2_legacy() { + has_include_2(true); +} -static void has_include_3() +static void has_include_3(bool legacy) { const char code[] = "#ifdef __has_include\n" " #if __has_include()\n" @@ -1599,12 +1611,18 @@ static void has_include_3() // Test file not found... ASSERT_EQUALS("\n\n\n\nB", preprocess(code, dui)); // Unless -I is set (preferably, we should differentiate -I and -isystem...) - dui.includePaths.push_back(testSourceDir + "/testsuite"); + dui.addIncludePath(testSourceDir + "/testsuite", legacy); ASSERT_EQUALS("\n\nA", preprocess(code, dui)); ASSERT_EQUALS("", preprocess(code)); } +static void has_include_3() { + has_include_3(false); +} +static void has_include_3_legacy() { + has_include_3(true); +} -static void has_include_4() +static void has_include_4(bool legacy) { const char code[] = "#ifdef __has_include\n" " #if __has_include(\"testsuite/realFileName1.cpp\")\n" @@ -1615,12 +1633,18 @@ static void has_include_4() "#endif"; simplecpp::DUI dui; dui.std = "c++17"; - dui.includePaths.push_back(testSourceDir); + dui.addIncludePath(testSourceDir, legacy); ASSERT_EQUALS("\n\nA", preprocess(code, dui)); ASSERT_EQUALS("", preprocess(code)); } +static void has_include_4() { + has_include_4(false); +} +static void has_include_4_legacy() { + has_include_4(true); +} -static void has_include_5() +static void has_include_5(bool legacy) { const char code[] = "#if defined( __has_include)\n" " #if !__has_include()\n" @@ -1631,12 +1655,18 @@ static void has_include_5() "#endif"; simplecpp::DUI dui; dui.std = "c++17"; - dui.includePaths.push_back(testSourceDir); + dui.addIncludePath(testSourceDir, legacy); ASSERT_EQUALS("\n\nA", preprocess(code, dui)); ASSERT_EQUALS("", preprocess(code)); } +static void has_include_5() { + has_include_5(false); +} +static void has_include_5_legacy() { + has_include_5(true); +} -static void has_include_6() +static void has_include_6(bool legacy) { const char code[] = "#if defined( __has_include)\n" " #if !__has_include()\n" @@ -1647,10 +1677,16 @@ static void has_include_6() "#endif"; simplecpp::DUI dui; dui.std = "gnu99"; - dui.includePaths.push_back(testSourceDir); + dui.addIncludePath(testSourceDir, legacy); ASSERT_EQUALS("\n\nA", preprocess(code, dui)); ASSERT_EQUALS("", preprocess(code)); } +static void has_include_6() { + has_include_6(false); +} +static void has_include_6_legacy() { + has_include_6(true); +} static void strict_ansi_1() { @@ -2032,7 +2068,7 @@ static void missingHeader1() ASSERT_EQUALS("file0,1,missing_header,Header not found: \"notexist.h\"\n", toString(outputList)); } -static void missingHeader2() +static void missingHeader2(bool legacy) { const char code[] = "#include \"foo.h\"\n"; // this file exists std::vector files; @@ -2042,10 +2078,16 @@ static void missingHeader2() simplecpp::TokenList tokens2(files); const simplecpp::TokenList rawtokens = makeTokenList(code,files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.addIncludePath(".", legacy); simplecpp::preprocess(tokens2, rawtokens, files, cache, dui, &outputList); ASSERT_EQUALS("", toString(outputList)); } +static void missingHeader2() { + missingHeader2(false); +} +static void missingHeader2_legacy() { + missingHeader2(true); +} static void missingHeader3() { @@ -2063,7 +2105,7 @@ static void missingHeader4() ASSERT_EQUALS("file0,1,syntax_error,No header in #include\n", toString(outputList)); } -static void nestedInclude() +static void nestedInclude(bool legacy) { const char code[] = "#include \"test.h\"\n"; std::vector files; @@ -2074,13 +2116,19 @@ static void nestedInclude() simplecpp::OutputList outputList; simplecpp::TokenList tokens2(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.addIncludePath(".", legacy); simplecpp::preprocess(tokens2, rawtokens, files, cache, dui, &outputList); ASSERT_EQUALS("file0,1,include_nested_too_deeply,#include nested too deeply\n", toString(outputList)); } +static void nestedInclude() { + nestedInclude(false); +} +static void nestedInclude_legacy() { + nestedInclude(true); +} -static void systemInclude() +static void systemInclude(bool legacy) { const char code[] = "#include \n"; std::vector files; @@ -2092,11 +2140,17 @@ static void systemInclude() simplecpp::OutputList outputList; simplecpp::TokenList tokens2(files); simplecpp::DUI dui; - dui.includePaths.push_back("include"); + dui.addIncludePath("include", legacy); simplecpp::preprocess(tokens2, rawtokens, files, cache, dui, &outputList); ASSERT_EQUALS("", toString(outputList)); } +static void systemInclude() { + systemInclude(false); +} +static void systemInclude_legacy() { + systemInclude(true); +} static void circularInclude() { @@ -2141,6 +2195,62 @@ static void circularInclude() ASSERT_EQUALS("", toString(outputList)); } +static void appleFrameworkIncludeTest() +{ + // This test checks Apple framework include handling. + // + // If -I /tmp/testFrameworks + // and we write: + // #include + // + // then simplecpp should find: + // ./testsuite/Foundation.framework/Headers/Foundation.h + const char code[] = "#include \n"; + std::vector files; + const simplecpp::TokenList rawtokens = makeTokenList(code, files, "sourcecode.cpp"); + simplecpp::FileDataCache cache; + simplecpp::TokenList tokens2(files); + simplecpp::DUI dui; +#ifdef SIMPLECPP_TEST_SOURCE_DIR + dui.addFrameworkPath(testSourceDir + "/testsuite"); +#else + dui.addFrameworkPath("./testsuite"); +#endif + simplecpp::OutputList outputList; + simplecpp::preprocess(tokens2, rawtokens, files, cache, dui, &outputList); + ASSERT_EQUALS("", toString(outputList)); +} + +static void appleFrameworkHasIncludeTest() +{ + const char code[] = + "#ifdef __has_include\n" + "#if __has_include()\n" + "A\n" + "#else\n" + "B\n" + "#endif\n" + "#endif\n"; + + std::vector files; + const simplecpp::TokenList rawtokens = makeTokenList(code, files, "sourcecode.cpp"); + + simplecpp::FileDataCache cache; + simplecpp::TokenList tokens2(files); + simplecpp::DUI dui; +#ifdef SIMPLECPP_TEST_SOURCE_DIR + dui.addFrameworkPath(testSourceDir + "/testsuite"); +#else + dui.addFrameworkPath("./testsuite"); +#endif + dui.std = "c++17"; // enable __has_include + + simplecpp::OutputList outputList; + simplecpp::preprocess(tokens2, rawtokens, files, cache, dui, &outputList); + + ASSERT_EQUALS("\n\nA", tokens2.stringify()); // should take the "A" branch +} + static void multiline1() { const char code[] = "#define A \\\n" @@ -2297,7 +2407,7 @@ static void include2() ASSERT_EQUALS("# include ", readfile(code)); } -static void include3() // #16 - crash when expanding macro from header +static void include3(bool legacy) // #16 - crash when expanding macro from header { const char code_c[] = "#include \"A.h\"\n" "glue(1,2,3,4)\n"; @@ -2318,14 +2428,19 @@ static void include3() // #16 - crash when expanding macro from header simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.addIncludePath(".", legacy); simplecpp::preprocess(out, rawtokens_c, files, cache, dui); ASSERT_EQUALS("\n1234", out.stringify()); } +static void include3() { + include3(false); +} +static void include3_legacy() { + include3(true); +} - -static void include4() // #27 - -include +static void include4(bool legacy) // #27 - -include { const char code_c[] = "X\n"; const char code_h[] = "#define X 123\n"; @@ -2345,14 +2460,20 @@ static void include4() // #27 - -include simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.addIncludePath(".", legacy); dui.includes.push_back("27.h"); simplecpp::preprocess(out, rawtokens_c, files, cache, dui); ASSERT_EQUALS("123", out.stringify()); } +static void include4() { + include4(false); +} +static void include4_legacy() { + include4(true); +} -static void include5() // #3 - handle #include MACRO +static void include5(bool legacy) // #3 - handle #include MACRO { const char code_c[] = "#define A \"3.h\"\n#include A\n"; const char code_h[] = "123\n"; @@ -2372,11 +2493,17 @@ static void include5() // #3 - handle #include MACRO simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.addIncludePath(".", legacy); simplecpp::preprocess(out, rawtokens_c, files, cache, dui); ASSERT_EQUALS("\n#line 1 \"3.h\"\n123", out.stringify()); } +static void include5() { + include5(false); +} +static void include5_legacy() { + include5(true); +} static void include6() // #57 - incomplete macro #include MACRO(,) { @@ -2397,7 +2524,7 @@ static void include6() // #57 - incomplete macro #include MACRO(,) } -static void include7() // #include MACRO +static void include7(bool legacy) // #include MACRO { const char code_c[] = "#define HDR <3.h>\n" "#include HDR\n"; @@ -2418,11 +2545,17 @@ static void include7() // #include MACRO simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.addIncludePath(".", legacy); simplecpp::preprocess(out, rawtokens_c, files, cache, dui); ASSERT_EQUALS("\n#line 1 \"3.h\"\n123", out.stringify()); } +static void include7() { + include7(false); +} +static void include7_legacy() { + include7(true); +} static void include8() // #include MACRO(X) { @@ -2434,7 +2567,7 @@ static void include8() // #include MACRO(X) ASSERT_EQUALS("file0,3,missing_header,Header not found: <../somewhere/header.h>\n", toString(outputList)); } -static void include9() +static void include9(bool legacy) { const char code_c[] = "#define HDR \"1.h\"\n" "#include HDR\n"; @@ -2456,11 +2589,17 @@ static void include9() simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.addIncludePath(".", legacy); simplecpp::preprocess(out, rawtokens_c, files, cache, dui); ASSERT_EQUALS("\n#line 2 \"1.h\"\nx = 1 ;", out.stringify()); } +static void include9_legacy() { + include9(true); +} +static void include9() { + include9(false); +} static void readfile_nullbyte() { @@ -2617,7 +2756,7 @@ static void readfile_file_not_found() ASSERT_EQUALS("file0,1,file_not_found,File is missing: NotAFile\n", toString(outputList)); } -static void stringify1() +static void stringify1(bool legacy) { const char code_c[] = "#include \"A.h\"\n" "#include \"A.h\"\n"; @@ -2638,11 +2777,17 @@ static void stringify1() simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.addIncludePath(".", legacy); simplecpp::preprocess(out, rawtokens_c, files, cache, dui); ASSERT_EQUALS("\n#line 1 \"A.h\"\n1\n2\n#line 1 \"A.h\"\n1\n2", out.stringify()); } +static void stringify1() { + stringify1(false); +} +static void stringify1_legacy() { + stringify1(true); +} static void tokenMacro1() { @@ -3365,6 +3510,12 @@ int main(int argc, char **argv) TEST_CASE(has_include_4); TEST_CASE(has_include_5); TEST_CASE(has_include_6); + TEST_CASE(has_include_1_legacy); + TEST_CASE(has_include_2_legacy); + TEST_CASE(has_include_3_legacy); + TEST_CASE(has_include_4_legacy); + TEST_CASE(has_include_5_legacy); + TEST_CASE(has_include_6_legacy); TEST_CASE(strict_ansi_1); TEST_CASE(strict_ansi_2); @@ -3402,11 +3553,16 @@ int main(int argc, char **argv) TEST_CASE(missingHeader1); TEST_CASE(missingHeader2); + TEST_CASE(missingHeader2_legacy); TEST_CASE(missingHeader3); TEST_CASE(missingHeader4); TEST_CASE(nestedInclude); + TEST_CASE(nestedInclude_legacy); TEST_CASE(systemInclude); + TEST_CASE(systemInclude_legacy); TEST_CASE(circularInclude); + TEST_CASE(appleFrameworkIncludeTest); + TEST_CASE(appleFrameworkHasIncludeTest); TEST_CASE(nullDirective1); TEST_CASE(nullDirective2); @@ -3422,6 +3578,12 @@ int main(int argc, char **argv) TEST_CASE(include8); // #include MACRO(X) TEST_CASE(include9); // #include MACRO + TEST_CASE(include3_legacy); + TEST_CASE(include4_legacy); // -include + TEST_CASE(include5_legacy); // #include MACRO + TEST_CASE(include7_legacy); // #include MACRO + TEST_CASE(include9_legacy); // #include MACRO + TEST_CASE(multiline1); TEST_CASE(multiline2); TEST_CASE(multiline3); @@ -3444,6 +3606,7 @@ int main(int argc, char **argv) TEST_CASE(readfile_file_not_found); TEST_CASE(stringify1); + TEST_CASE(stringify1_legacy); TEST_CASE(tokenMacro1); TEST_CASE(tokenMacro2); diff --git a/testsuite/Foundation.framework/Headers/Foundation.h b/testsuite/Foundation.framework/Headers/Foundation.h new file mode 100644 index 00000000..5e6f5415 --- /dev/null +++ b/testsuite/Foundation.framework/Headers/Foundation.h @@ -0,0 +1 @@ +// Dummy Foundation.h for appleFrameworkIncludeTest diff --git a/testutils.py b/testutils.py index 55a2686d..f38a81f9 100644 --- a/testutils.py +++ b/testutils.py @@ -42,6 +42,7 @@ def simplecpp(args = [], cwd = None): simplecpp_path = os.environ['SIMPLECPP_EXE_PATH'] else: simplecpp_path = os.path.join(dir_path, "simplecpp") + return __run_subprocess([simplecpp_path] + args, cwd = cwd) def quoted_string(s): @@ -50,6 +51,15 @@ def quoted_string(s): def format_include_path_arg(include_path): return f"-I{str(include_path)}" +def format_isystem_path_arg(include_path): + return f"-isystem{str(include_path)}" + +def format_framework_path_arg(framework_path): + return f"-F{str(framework_path)}" + +def format_iframework_path_arg(framework_path): + return f"-iframework{str(framework_path)}" + def format_include(include, is_sys_header=False): if is_sys_header: return f"<{quoted_string(include)[1:-1]}>"