From 9739f7e8511062f873cf46d0414ecf7c026853cd Mon Sep 17 00:00:00 2001 From: Jean-Christophe Fillion-Robin Date: Fri, 29 Aug 2025 21:49:20 -0400 Subject: [PATCH 1/7] tests: Add docstrings to __test_relative_header_create_* helper functions --- integration_test.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/integration_test.py b/integration_test.py index a59ae338..52686181 100644 --- a/integration_test.py +++ b/integration_test.py @@ -7,6 +7,17 @@ from testutils import simplecpp, format_include_path_arg, format_include 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 + """ header_file = os.path.join(dir, 'test.h') with open(header_file, 'wt') as f: f.write(f""" @@ -21,6 +32,17 @@ def __test_relative_header_create_header(dir, with_pragma_once=True): return header_file, "error: #error header_was_already_included" 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 From aa8ef0e09b5c53637d4ab0ce716ee21bafcf0894 Mon Sep 17 00:00:00 2001 From: Hans Johnson Date: Wed, 16 Jul 2025 15:39:20 -0500 Subject: [PATCH 2/7] feat: Darwin-style framework search (-F, -iframework) with ordered path lookup This change teaches simplecpp to resolve headers from Apple-style Framework directories while preserving the left-to-right order of interleaved -I/-F/-iframework search paths (like GCC/Clang on Darwin). This enables both: - `__has_include()` -> `` (or `PrivateHeaders`) - `#include ` -> same framework layout when a package prefix exists Changes: - Add `DUI::SearchPath` with `PathKind {Include, Framework, SystemFramework}`. - If `DUI::searchPaths` is non-empty, use it verbatim (interleaved -I/-F/-iframework). Otherwise preserve back-compat by mirroring `includePaths` as Include paths. - Update `openHeader()` to consult typed paths, and only rewrite `` to `Pkg.framework/{Headers,PrivateHeaders}/Hdr.h` when a package prefix exists. - Implement `toAppleFrameworkRelatives()` returning prioritized candidates (Headers first, then PrivateHeaders). - Tests use `PathKind::Framework` when checking framework layout. CLI - Support -F and -iframework (keep -I as before). Behavior notes - The order of -I/-F/-iframework is preserved exactly as provided. - `Framework` vs `SystemFramework` differ only in diagnostic semantics (not lookup). - Legacy users who only set `DUI::includePaths` see identical behavior. Tests - Add `appleFrameworkHasIncludeTest` for `__has_include` resolution. - Add `appleFrameworkIncludeTest` for `#include` resolution. - Add dummy fixture: `testsuite/Foundation.framework/Headers/Foundation.h`. This brings simplecpp closer to GCC/Clang behavior on macOS and enables robust resolution of framework headers like `Foundation/Foundation.h`. Co-authored-by: Jean-Christophe Fillion-Robin Co-authored-by: Hans Johnson Suggested-by: glankk --- integration_test.py | 259 +++++++++++++++++- main.cpp | 13 +- simplecpp.cpp | 144 ++++++++-- simplecpp.h | 32 ++- test.cpp | 62 +++++ .../Foundation.framework/Headers/Foundation.h | 1 + testutils.py | 6 + 7 files changed, 481 insertions(+), 36 deletions(-) create mode 100644 testsuite/Foundation.framework/Headers/Foundation.h diff --git a/integration_test.py b/integration_test.py index 52686181..3a28cae7 100644 --- a/integration_test.py +++ b/integration_test.py @@ -4,7 +4,79 @@ import pathlib import platform import pytest -from testutils import simplecpp, format_include_path_arg, format_include +from testutils import ( + simplecpp, + format_include_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 {inc_guard}_INCLUDED + #define {inc_guard}_INCLUDED + #else + {f"#error {already_included_error_msg}" if already_included_error_msg else ""} + #endif + 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 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): """ @@ -18,18 +90,10 @@ def __test_relative_header_create_header(dir, with_pragma_once=True): - absolute path to the created header file - expected compiler error substring for duplicate inclusion """ - header_file = os.path.join(dir, 'test.h') - 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 - #else - #error header_was_already_included - #endif - const int dummy = 1; - """) - return header_file, "error: #error header_was_already_included" + 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): """ @@ -258,6 +322,175 @@ 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` / `F1` / `IFW1` point to distinct directories and contain `Component_1.h` (a decoy). + # - `I` / `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, -F, or -iframework) uniquely identifiable + # in the preprocessor `#line` output. + + # Sanity checks + (("I",), "I"), + (("F",), "F"), + (("IFW",), "IFW"), + + # Includes (-I) + (("I1", "I"), "I"), + (("I", "I1"), "I"), + # Includes (-I) duplicates + (("I1", "I", "I1"), "I"), + (("I", "I1", "I"), "I"), + + # 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 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"), + ], +) +def test_searchpath_order(record_property, tmpdir, is_sys, order, expected): + """ + Validate include resolution order across -I (user 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 three parallel trees (two entries per kind): + - inc{,_1}/MyKit/Component{,_1}.h # for -I + - 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, 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) + + # -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', 'F1', 'IFW1'. + Returns 0 when no suffix is present (e.g., 'I', '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 ["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)] + 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, *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 ["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..a55eda66 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.searchPaths.push_back({value, simplecpp::DUI::PathKind::Include}); + found = true; + break; + } + case 'F': { // framework path + const char * const value = arg[2] ? (argv[i] + 2) : argv[++i]; + dui.searchPaths.push_back({value, simplecpp::DUI::PathKind::Framework}); found = true; break; } @@ -54,6 +60,9 @@ int main(int argc, char **argv) } else if (std::strncmp(arg, "-is",3)==0) { use_istream = true; found = true; + } else if (std::strncmp(arg, "-iframework", 11) == 0) { + dui.searchPaths.push_back({arg + 11, simplecpp::DUI::PathKind::SystemFramework}); + found = true; } break; case 's': @@ -100,6 +109,8 @@ 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 << " -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..e0e0e215 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,52 @@ 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; + } + } + } + } + + // -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 +3098,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 +3114,71 @@ 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); - - 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..e148fdc3 100644 --- a/simplecpp.h +++ b/simplecpp.h @@ -395,13 +395,43 @@ 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 + * -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, -F, -iframework. + enum class PathKind { Include, Framework, SystemFramework }; + struct SearchPath { + std::string path; + PathKind kind; + }; + 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..866f1678 100644 --- a/test.cpp +++ b/test.cpp @@ -2141,6 +2141,66 @@ 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.searchPaths.push_back({testSourceDir + "/testsuite", + simplecpp::DUI::PathKind::Framework + }); +#else + dui.searchPaths.push_back({"./testsuite", simplecpp::DUI::PathKind::Framework}); +#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.searchPaths.push_back({testSourceDir + "/testsuite", + simplecpp::DUI::PathKind::Framework + }); +#else + dui.searchPaths.push_back({"./testsuite", simplecpp::DUI::PathKind::Framework}); +#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" @@ -3407,6 +3467,8 @@ int main(int argc, char **argv) TEST_CASE(nestedInclude); TEST_CASE(systemInclude); TEST_CASE(circularInclude); + TEST_CASE(appleFrameworkIncludeTest); + TEST_CASE(appleFrameworkHasIncludeTest); TEST_CASE(nullDirective1); TEST_CASE(nullDirective2); 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..6a246e46 100644 --- a/testutils.py +++ b/testutils.py @@ -50,6 +50,12 @@ def quoted_string(s): def format_include_path_arg(include_path): return f"-I{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]}>" From b92e84854f822666a7cfb9f1002177a4db406258 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Fillion-Robin Date: Fri, 29 Aug 2025 21:53:38 -0400 Subject: [PATCH 3/7] feat: Add convenient API to simplecpp:DUI for adding different type of search paths --- main.cpp | 6 +++--- simplecpp.h | 13 +++++++++++++ test.cpp | 12 ++++-------- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/main.cpp b/main.cpp index a55eda66..d04e5ec5 100644 --- a/main.cpp +++ b/main.cpp @@ -43,13 +43,13 @@ int main(int argc, char **argv) } case 'I': { // include path const char * const value = arg[2] ? (argv[i] + 2) : argv[++i]; - dui.searchPaths.push_back({value, simplecpp::DUI::PathKind::Include}); + dui.addIncludePath(value); found = true; break; } case 'F': { // framework path const char * const value = arg[2] ? (argv[i] + 2) : argv[++i]; - dui.searchPaths.push_back({value, simplecpp::DUI::PathKind::Framework}); + dui.addFrameworkPath(value); found = true; break; } @@ -61,7 +61,7 @@ int main(int argc, char **argv) use_istream = true; found = true; } else if (std::strncmp(arg, "-iframework", 11) == 0) { - dui.searchPaths.push_back({arg + 11, simplecpp::DUI::PathKind::SystemFramework}); + dui.addSystemFrameworkPath(arg + 11); found = true; } break; diff --git a/simplecpp.h b/simplecpp.h index e148fdc3..242e949d 100644 --- a/simplecpp.h +++ b/simplecpp.h @@ -423,6 +423,19 @@ namespace simplecpp { PathKind kind; }; + /** Mirrors compiler option -I */ + void addIncludePath(const std::string& path) { + searchPaths.push_back({path, PathKind::Include}); + } + // 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; diff --git a/test.cpp b/test.cpp index 866f1678..8e1d91ca 100644 --- a/test.cpp +++ b/test.cpp @@ -2158,11 +2158,9 @@ static void appleFrameworkIncludeTest() simplecpp::TokenList tokens2(files); simplecpp::DUI dui; #ifdef SIMPLECPP_TEST_SOURCE_DIR - dui.searchPaths.push_back({testSourceDir + "/testsuite", - simplecpp::DUI::PathKind::Framework - }); + dui.addFrameworkPath(testSourceDir + "/testsuite"); #else - dui.searchPaths.push_back({"./testsuite", simplecpp::DUI::PathKind::Framework}); + dui.addFrameworkPath("./testsuite"); #endif simplecpp::OutputList outputList; simplecpp::preprocess(tokens2, rawtokens, files, cache, dui, &outputList); @@ -2187,11 +2185,9 @@ static void appleFrameworkHasIncludeTest() simplecpp::TokenList tokens2(files); simplecpp::DUI dui; #ifdef SIMPLECPP_TEST_SOURCE_DIR - dui.searchPaths.push_back({testSourceDir + "/testsuite", - simplecpp::DUI::PathKind::Framework - }); + dui.addFrameworkPath(testSourceDir + "/testsuite"); #else - dui.searchPaths.push_back({"./testsuite", simplecpp::DUI::PathKind::Framework}); + dui.addFrameworkPath("./testsuite"); #endif dui.std = "c++17"; // enable __has_include From 0269b854fb609ed0bc69c25b96d9f389dd0ea195 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Fillion-Robin Date: Fri, 29 Aug 2025 23:15:30 -0400 Subject: [PATCH 4/7] feat: Add support for -isystem This change introduces support for the -isystem flag, allowing users to specify system include directories. The addition includes handling the new flag in both the argument parsing and include resolution logic, as well as updating relevant tests to validate the new functionality. --- integration_test.py | 59 ++++++++++++++++++++++++++++++++++++--------- main.cpp | 4 +++ simplecpp.cpp | 16 ++++++++++++ simplecpp.h | 9 +++++-- testutils.py | 4 +++ 5 files changed, 79 insertions(+), 13 deletions(-) diff --git a/integration_test.py b/integration_test.py index 3a28cae7..f3560fe1 100644 --- a/integration_test.py +++ b/integration_test.py @@ -7,6 +7,7 @@ from testutils import ( simplecpp, format_include_path_arg, + format_isystem_path_arg, format_framework_path_arg, format_iframework_path_arg, format_include, @@ -346,15 +347,16 @@ def test_framework_lookup(record_property, tmpdir, is_sys, is_iframework, is_pri "order,expected", [ # Note: - # - `I1` / `F1` / `IFW1` point to distinct directories and contain `Component_1.h` (a decoy). - # - `I` / `F` / `IFW` point to directories that contain `Component.h`, which the + # - `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, -F, or -iframework) uniquely identifiable + # 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"), @@ -365,6 +367,13 @@ def test_framework_lookup(record_property, tmpdir, is_sys, is_iframework, is_pri (("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"), @@ -385,6 +394,16 @@ def test_framework_lookup(record_property, tmpdir, is_sys, is_iframework, is_pri (("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"), @@ -394,16 +413,24 @@ def test_framework_lookup(record_property, tmpdir, is_sys, is_iframework, is_pri (("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), -F (user framework), - and -iframework (system framework) using a minimal file layout, asserting which - physical header path appears in the preprocessor #line output. + 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 three parallel trees (two entries per kind): + 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 @@ -418,7 +445,7 @@ def test_searchpath_order(record_property, tmpdir, is_sys, order, expected): """ # Create two include dirs, two user framework dirs, and two system framework dirs - inc_dirs, fw_dirs, sysfw_dirs = [], [], [] + inc_dirs, isys_dirs, fw_dirs, sysfw_dirs = [], [], [], [] def _suffix(idx: int) -> str: return f"_{idx}" if idx > 0 else "" @@ -429,6 +456,11 @@ def _suffix(idx: int) -> str: __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") @@ -443,8 +475,8 @@ def _suffix(idx: int) -> str: 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', 'F1', 'IFW1'. - Returns 0 when no suffix is present (e.g., 'I', 'F', 'IFW').""" + """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` @@ -452,6 +484,8 @@ def idx_from_flag(prefix: str, flag: str) -> int: 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"]: @@ -469,14 +503,17 @@ def idx_from_flag(prefix: str, flag: str) -> int: 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, *fw_paths, *ifw_paths] + 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"]: diff --git a/main.cpp b/main.cpp index d04e5ec5..f0842817 100644 --- a/main.cpp +++ b/main.cpp @@ -57,6 +57,9 @@ 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; @@ -109,6 +112,7 @@ 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; diff --git a/simplecpp.cpp b/simplecpp.cpp index e0e0e215..51d33caf 100644 --- a/simplecpp.cpp +++ b/simplecpp.cpp @@ -3052,6 +3052,15 @@ static std::string openHeader(std::ifstream &f, const simplecpp::DUI &dui, const } } + // -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) { @@ -3148,6 +3157,13 @@ std::pair simplecpp::FileDataCache::get(const std:: } } + // -isystem + for (const auto &searchPath : searchPaths) { + if (searchPath.kind == DUI::PathKind::SystemInclude) { + push_unique(candidates, simplecpp::simplifyPath(searchPath.path + "/" + header)); + } + } + // -iframework for (const auto &searchPath : searchPaths) { if (searchPath.kind == simplecpp::DUI::PathKind::SystemFramework) { diff --git a/simplecpp.h b/simplecpp.h index 242e949d..37b9ad68 100644 --- a/simplecpp.h +++ b/simplecpp.h @@ -400,6 +400,7 @@ namespace simplecpp { * -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 @@ -416,8 +417,8 @@ namespace simplecpp { struct SIMPLECPP_LIB DUI { DUI() : clearIncludeCache(false), removeComments(false) {} - // Typed search path entry. Mirrors GCC behavior for -I, -F, -iframework. - enum class PathKind { Include, Framework, SystemFramework }; + // 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; @@ -427,6 +428,10 @@ namespace simplecpp { void addIncludePath(const std::string& path) { 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}); diff --git a/testutils.py b/testutils.py index 6a246e46..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,9 @@ 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)}" From d5a9b9225db5741a3f4006cdf27849f6f761b2c1 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Fillion-Robin Date: Fri, 29 Aug 2025 23:41:11 -0400 Subject: [PATCH 5/7] feat: Update DUI::addIncludePath to accept legacy parameter --- main.cpp | 2 +- simplecpp.h | 19 ++++++++++++++----- test.cpp | 30 +++++++++++++++--------------- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/main.cpp b/main.cpp index f0842817..1920f82b 100644 --- a/main.cpp +++ b/main.cpp @@ -43,7 +43,7 @@ int main(int argc, char **argv) } case 'I': { // include path const char * const value = arg[2] ? (argv[i] + 2) : argv[++i]; - dui.addIncludePath(value); + dui.addIncludePath(value, /* legacy= */ false); found = true; break; } diff --git a/simplecpp.h b/simplecpp.h index 37b9ad68..5ba2eaf0 100644 --- a/simplecpp.h +++ b/simplecpp.h @@ -424,19 +424,28 @@ namespace simplecpp { PathKind kind; }; - /** Mirrors compiler option -I */ - void addIncludePath(const std::string& path) { - searchPaths.push_back({path, PathKind::Include}); + /** + * 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) { + 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 + /** Mirrors compiler option -F */ void addFrameworkPath(const std::string& path) { searchPaths.push_back({path, PathKind::Framework}); } - // Mirrors compiler option -iframework + /** Mirrors compiler option -iframework */ void addSystemFrameworkPath(const std::string& path) { searchPaths.push_back({path, PathKind::SystemFramework}); } diff --git a/test.cpp b/test.cpp index 8e1d91ca..04df5806 100644 --- a/test.cpp +++ b/test.cpp @@ -1561,7 +1561,7 @@ static void has_include_1() " #endif\n" "#endif"; simplecpp::DUI dui; - dui.includePaths.push_back(testSourceDir); + dui.addIncludePath(testSourceDir, /* legacy= */ true); dui.std = "c++17"; ASSERT_EQUALS("\n\nA", preprocess(code, dui)); dui.std = "c++14"; @@ -1579,7 +1579,7 @@ static void has_include_2() " #endif\n" "#endif"; simplecpp::DUI dui; - dui.includePaths.push_back(testSourceDir); + dui.addIncludePath(testSourceDir, /* legacy= */ true); dui.std = "c++17"; ASSERT_EQUALS("\n\nA", preprocess(code, dui)); ASSERT_EQUALS("", preprocess(code)); @@ -1599,7 +1599,7 @@ 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= */ true); ASSERT_EQUALS("\n\nA", preprocess(code, dui)); ASSERT_EQUALS("", preprocess(code)); } @@ -1615,7 +1615,7 @@ static void has_include_4() "#endif"; simplecpp::DUI dui; dui.std = "c++17"; - dui.includePaths.push_back(testSourceDir); + dui.addIncludePath(testSourceDir, /* legacy= */ true); ASSERT_EQUALS("\n\nA", preprocess(code, dui)); ASSERT_EQUALS("", preprocess(code)); } @@ -1631,7 +1631,7 @@ static void has_include_5() "#endif"; simplecpp::DUI dui; dui.std = "c++17"; - dui.includePaths.push_back(testSourceDir); + dui.addIncludePath(testSourceDir, /* legacy= */ true); ASSERT_EQUALS("\n\nA", preprocess(code, dui)); ASSERT_EQUALS("", preprocess(code)); } @@ -1647,7 +1647,7 @@ static void has_include_6() "#endif"; simplecpp::DUI dui; dui.std = "gnu99"; - dui.includePaths.push_back(testSourceDir); + dui.addIncludePath(testSourceDir, /* legacy= */ true); ASSERT_EQUALS("\n\nA", preprocess(code, dui)); ASSERT_EQUALS("", preprocess(code)); } @@ -2042,7 +2042,7 @@ static void missingHeader2() simplecpp::TokenList tokens2(files); const simplecpp::TokenList rawtokens = makeTokenList(code,files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.addIncludePath(".", /* legacy= */ true); simplecpp::preprocess(tokens2, rawtokens, files, cache, dui, &outputList); ASSERT_EQUALS("", toString(outputList)); } @@ -2074,7 +2074,7 @@ static void nestedInclude() simplecpp::OutputList outputList; simplecpp::TokenList tokens2(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.addIncludePath(".", /* legacy= */ true); simplecpp::preprocess(tokens2, rawtokens, files, cache, dui, &outputList); ASSERT_EQUALS("file0,1,include_nested_too_deeply,#include nested too deeply\n", toString(outputList)); @@ -2092,7 +2092,7 @@ static void systemInclude() simplecpp::OutputList outputList; simplecpp::TokenList tokens2(files); simplecpp::DUI dui; - dui.includePaths.push_back("include"); + dui.addIncludePath("include", /* legacy= */ true); simplecpp::preprocess(tokens2, rawtokens, files, cache, dui, &outputList); ASSERT_EQUALS("", toString(outputList)); @@ -2374,7 +2374,7 @@ static void include3() // #16 - crash when expanding macro from header simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.addIncludePath(".", /* legacy= */ true); simplecpp::preprocess(out, rawtokens_c, files, cache, dui); ASSERT_EQUALS("\n1234", out.stringify()); @@ -2401,7 +2401,7 @@ static void include4() // #27 - -include simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.addIncludePath(".", /* legacy= */ true); dui.includes.push_back("27.h"); simplecpp::preprocess(out, rawtokens_c, files, cache, dui); @@ -2428,7 +2428,7 @@ static void include5() // #3 - handle #include MACRO simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.addIncludePath(".", /* legacy= */ true); simplecpp::preprocess(out, rawtokens_c, files, cache, dui); ASSERT_EQUALS("\n#line 1 \"3.h\"\n123", out.stringify()); @@ -2474,7 +2474,7 @@ static void include7() // #include MACRO simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.addIncludePath(".", /* legacy= */ true); simplecpp::preprocess(out, rawtokens_c, files, cache, dui); ASSERT_EQUALS("\n#line 1 \"3.h\"\n123", out.stringify()); @@ -2512,7 +2512,7 @@ static void include9() simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.addIncludePath(".", /* legacy= */ true); simplecpp::preprocess(out, rawtokens_c, files, cache, dui); ASSERT_EQUALS("\n#line 2 \"1.h\"\nx = 1 ;", out.stringify()); @@ -2694,7 +2694,7 @@ static void stringify1() simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.addIncludePath(".", /* legacy= */ true); 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()); From 517d2969ccba7bd386d7d1837974a3e540793826 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Fillion-Robin Date: Sat, 30 Aug 2025 00:16:48 -0400 Subject: [PATCH 6/7] tests: Update test cases to use DUI::addIncludePath with legacy mode This improves test coverage by ensuring that DUI::addIncludePath is tested with both legacy mode on and off. It adds new test functions for each scenario and updates the test cases accordingly. --- test.cpp | 167 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 136 insertions(+), 31 deletions(-) diff --git a/test.cpp b/test.cpp index 04df5806..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.addIncludePath(testSourceDir, /* legacy= */ true); + 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.addIncludePath(testSourceDir, /* legacy= */ true); + 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.addIncludePath(testSourceDir + "/testsuite", /* legacy= */ true); + 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.addIncludePath(testSourceDir, /* legacy= */ true); + 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.addIncludePath(testSourceDir, /* legacy= */ true); + 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.addIncludePath(testSourceDir, /* legacy= */ true); + 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.addIncludePath(".", /* legacy= */ true); + 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.addIncludePath(".", /* legacy= */ true); + 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.addIncludePath("include", /* legacy= */ true); + 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() { @@ -2353,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"; @@ -2374,14 +2428,19 @@ static void include3() // #16 - crash when expanding macro from header simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.addIncludePath(".", /* legacy= */ true); + 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"; @@ -2401,14 +2460,20 @@ static void include4() // #27 - -include simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.addIncludePath(".", /* legacy= */ true); + 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"; @@ -2428,11 +2493,17 @@ static void include5() // #3 - handle #include MACRO simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.addIncludePath(".", /* legacy= */ true); + 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(,) { @@ -2453,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"; @@ -2474,11 +2545,17 @@ static void include7() // #include MACRO simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.addIncludePath(".", /* legacy= */ true); + 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) { @@ -2490,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"; @@ -2512,11 +2589,17 @@ static void include9() simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.addIncludePath(".", /* legacy= */ true); + 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() { @@ -2673,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"; @@ -2694,11 +2777,17 @@ static void stringify1() simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.addIncludePath(".", /* legacy= */ true); + 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() { @@ -3421,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); @@ -3458,10 +3553,13 @@ 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); @@ -3480,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); @@ -3502,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); From 03d7ab08cd182c55ac29436aa84ace05363fa7ff Mon Sep 17 00:00:00 2001 From: Jean-Christophe Fillion-Robin Date: Sat, 30 Aug 2025 00:45:03 -0400 Subject: [PATCH 7/7] feat: Set default legacy mode to false in DUI::addIncludePath This changes the default value of the 'legacy' parameter in the DUI::addIncludePath method to false. Since this API was just introduced, it is preferred to use the new capabilities by preserving the left-to-right order. --- simplecpp.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simplecpp.h b/simplecpp.h index 5ba2eaf0..8b45c9c8 100644 --- a/simplecpp.h +++ b/simplecpp.h @@ -430,7 +430,7 @@ namespace simplecpp { * 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) { + void addIncludePath(const std::string& path, bool legacy=false) { if (legacy) { includePaths.push_back(path); } else {