diff --git a/src/pipeline/pass_pkgmap.c b/src/pipeline/pass_pkgmap.c index 075378b12..d7b4b5c57 100644 --- a/src/pipeline/pass_pkgmap.c +++ b/src/pipeline/pass_pkgmap.c @@ -3,7 +3,7 @@ * * Scans discovered files for manifest files (package.json, go.mod, Cargo.toml, * pyproject.toml, composer.json, pubspec.yaml, pom.xml, build.gradle, mix.exs, - * *.gemspec) and builds a hash table mapping bare package specifiers to resolved + * *.gemspec, Package.swift) and builds a hash table mapping bare package specifiers to resolved * module QNs. This enables IMPORTS edges for non-relative imports like * "@myorg/pkg", "github.com/foo/bar", "use my_crate::foo". * @@ -665,6 +665,271 @@ static void parse_gemspec(const char *source, int source_len, const char *rel_pa } } +/* Swift: Package.swift — SwiftPM manifest. + * + * Package.swift is executable Swift, not declarative, so this is a + * hand-rolled literal pattern-extractor (same spirit as parse_cargo_toml), + * not a Swift evaluator: it locates `.target(...)` call expressions via a + * comment/string-aware token scan and pulls only their quoted-string-literal + * `name:` / `path:` arguments. Anything computed or concatenated is + * skipped, not guessed — fail-closed, like every other parser here. + * + * Only a manifest's OWN `.target(...)` declarations self-register — never + * `.library(...)` products (a product name is not generally an importable + * module: SwiftPM lets it alias multiple targets, or none sharing its + * name), and never `.package(url:)` / `.package(path:)` dependencies or + * target-to-target dependency references. A dependency resolves because + * the DEPENDENCY's own Package.swift registers itself when the repo-wide + * manifest walk (cbm_pkgmap_scan_repo) reaches it, exactly like a JS + * workspace sibling's package.json (see repro_issue408.c). + * + * Deliberately out of scope (matches the item-1 authorization): evaluating + * `#if`/`#endif` conditional-compilation blocks. A `.target(...)` inside + * one is scanned like any other — teaching the extractor which platform + * conditions are "active" would need a Swift semantic tier, which this PR + * was explicitly asked not to add. */ + +/* One step of comment/string-aware scanning, shared by swift_find_code_token + * and swift_match_paren below. If `*pp` sits inside, or is entering, a `//` + * line comment, a nesting-aware slash-star block comment (continued across + * calls via `*block_depth`), or a "..." string literal (escape aware, + * tracked via `*in_str`), advances `*pp` past that one step and returns + * true. Otherwise leaves `*pp` untouched and returns false, so the caller + * handles the "real code" character itself (paren tracking, needle + * matching, ...). */ +static bool swift_skip_comment_or_string(const char **pp, const char *end, int *block_depth, + bool *in_str) { + const char *p = *pp; + if (*block_depth > 0) { + if (p + SKIP_ONE < end && p[0] == '/' && p[SKIP_ONE] == '*') { + (*block_depth)++; + *pp = p + PAIR_LEN; + } else if (p + SKIP_ONE < end && p[0] == '*' && p[SKIP_ONE] == '/') { + (*block_depth)--; + *pp = p + PAIR_LEN; + } else { + *pp = p + SKIP_ONE; + } + return true; + } + if (*in_str) { + if (*p == '\\' && p + SKIP_ONE < end) { + *pp = p + PAIR_LEN; + } else { + if (*p == '"') { + *in_str = false; + } + *pp = p + SKIP_ONE; + } + return true; + } + if (p + SKIP_ONE < end && p[0] == '/' && p[SKIP_ONE] == '/') { + while (p < end && *p != '\n') { + p++; + } + *pp = p; + return true; + } + if (p + SKIP_ONE < end && p[0] == '/' && p[SKIP_ONE] == '*') { + *block_depth = SKIP_ONE; + *pp = p + PAIR_LEN; + return true; + } + if (*p == '"') { + *in_str = true; + *pp = p + SKIP_ONE; + return true; + } + return false; +} + +/* Scans [start, end) for the next occurrence of `needle` that is not inside + * a `//` line comment, a nesting-aware slash-star block comment, or a "..." + * string literal (backslash-escapes honored). This is the single source of + * truth for "real code" scanning below — a `.target(`, `name:`, or `path:` + * spelled inside a comment or a string constant must never be mistaken for + * a live declaration. Returns a pointer to the match, or NULL. */ +static const char *swift_find_code_token(const char *start, const char *end, const char *needle) { + size_t nlen = strlen(needle); + int block_depth = 0; + bool in_str = false; + const char *p = start; + while (p < end) { + if (swift_skip_comment_or_string(&p, end, &block_depth, &in_str)) { + continue; + } + if ((size_t)(end - p) >= nlen && memcmp(p, needle, nlen) == 0) { + return p; + } + p++; + } + return NULL; +} + +/* Find the matching ')' for the '(' at `open`, scanning forward to `end`. + * Delegates comment/string spans to swift_skip_comment_or_string — matching + * swift_find_code_token — so a ')', '(', or comment delimiter inside a + * quoted argument value or a comment never perturbs the depth count. + * Returns NULL for an unbalanced/unterminated call — the caller treats + * that span as unparseable and skips it (fail-closed). */ +static const char *swift_match_paren(const char *open, const char *end) { + int depth = 0; + int block_depth = 0; + bool in_str = false; + const char *p = open; + while (p < end) { + if (swift_skip_comment_or_string(&p, end, &block_depth, &in_str)) { + continue; + } + if (*p == '(') { + depth++; + } else if (*p == ')') { + depth--; + if (depth == 0) { + return p; + } + } + p++; + } + return NULL; +} + +/* Extract a bare double-quoted string-literal value at `p` (after skipping + * leading whitespace and honoring backslash-escapes inside the literal), + * requiring the closing quote be immediately followed by a comma, or by + * `end` — rejects `name: "Foo" + suffix`-style concatenation, which a plain + * quote-scan alone cannot tell apart from a true literal. Every caller here + * passes the enclosing call's own closing ')' position as `end`, so landing + * exactly on it after the closing quote correctly means "immediately + * followed by the close-paren", not just "ran off the end of the buffer". + * Returns heap string, or NULL (fail-closed). */ +static char *swift_quoted_literal(const char *p, const char *end) { + while (p < end && (*p == ' ' || *p == '\t')) { + p++; + } + if (p >= end || *p != '"') { + return NULL; + } + p++; + const char *start = p; + while (p < end && *p != '"' && *p != '\n') { + if (*p == '\\' && p + SKIP_ONE < end) { + p += PAIR_LEN; + continue; + } + p++; + } + if (p >= end || *p != '"') { + return NULL; + } + char *value = cbm_strndup(start, (size_t)(p - start)); + p++; /* past closing quote */ + while (p < end && (*p == ' ' || *p == '\t')) { + p++; + } + if (p >= end || *p == ',') { + return value; + } + free(value); + return NULL; +} + +/* Search [start, end) for `needle` via swift_find_code_token (skipping + * comments/strings), then extract the bare quoted-literal argument + * immediately following it via swift_quoted_literal. Bounded to `end` so a + * match can never leak past the enclosing call's own argument list into a + * later, unrelated declaration. When `out_found` is non-NULL, it is set to + * whether `needle` was located at all — independent of whether its value + * parsed as a bare literal — so callers can tell "argument absent" (fine, + * apply a default) apart from "argument present but not a literal" + * (unknowable, fail closed). Returns heap string or NULL. */ +static char *swift_extract_after(const char *start, const char *end, const char *needle, + bool *out_found) { + if (out_found) { + *out_found = false; + } + const char *hit = swift_find_code_token(start, end, needle); + if (!hit) { + return NULL; + } + if (out_found) { + *out_found = true; + } + return swift_quoted_literal(hit + strlen(needle), end); +} + +/* Register `target_name` → its resolved source directory: the literal + * `path:` argument the target declared, if any, else the conventional + * `Sources/` (fixed-convention, same approach parse_cargo_toml + * takes for `src/lib`). `literal_path` is borrowed; caller retains + * ownership. */ +static void swift_register_target(const char *rel_path, const char *target_name, + const char *literal_path, cbm_pkg_entries_t *entries) { + if (!target_name || !target_name[0]) { + return; + } + char suffix_buf[PKGMAP_PATH_BUF]; + const char *suffix; + if (literal_path && literal_path[0]) { + suffix = literal_path; + } else { + snprintf(suffix_buf, sizeof(suffix_buf), "Sources/%s", target_name); + suffix = suffix_buf; + } + char *entry = build_entry_path(rel_path, suffix); + if (entry) { + pkg_entries_push(entries, strdup(target_name), entry); + } +} + +/* Scan for `.target(name: "Foo", ...)` declarations, skipping any + * `.target(` spelling that falls inside a comment or string literal. + * Non-overlapping: the scan resumes AFTER each matched close paren, so a + * `dependencies: [.target(name: "Bar")]` reference nested inside Foo's own + * argument list is never re-visited as a second top-level target. + * + * A target with a `path:` argument that isn't a bare literal (computed, + * concatenated, ...) is skipped entirely, even though its `name:` may be a + * valid literal: SwiftPM would use the computed path, not the + * `Sources/` convention, and guessing that convention anyway would + * mint a location that is not just unconfirmed but actively likely wrong. */ +static void swift_scan_targets(const char *source, const char *end, const char *rel_path, + cbm_pkg_entries_t *entries) { + static const char prefix[] = ".target("; + const char *cursor = source; + while (cursor < end) { + const char *hit = swift_find_code_token(cursor, end, prefix); + if (!hit) { + return; + } + const char *open = hit + strlen(prefix) - SKIP_ONE; + const char *close = swift_match_paren(open, end); + if (!close) { + return; /* unbalanced — nothing further here is trustworthy */ + } + char *name = swift_extract_after(open, close, "name:", NULL); + if (name) { + bool path_present = false; + char *literal_path = swift_extract_after(open, close, "path:", &path_present); + if (literal_path || !path_present) { + swift_register_target(rel_path, name, literal_path, entries); + } + free(literal_path); + free(name); + } + cursor = close + SKIP_ONE; + } +} + +/* SwiftPM: Package.swift — targets → Sources/ (or their literal + * `path:`). Products deliberately do not self-register: see the file + * comment above swift_find_code_token. */ +static void parse_package_swift(const char *source, int source_len, const char *rel_path, + cbm_pkg_entries_t *entries) { + const char *end = source + source_len; + swift_scan_targets(source, end, rel_path, entries); +} + /* ── Public: manifest detection + parsing ──────────────────────── */ bool cbm_pkgmap_try_parse(const char *basename, const char *rel_path, const char *source, @@ -713,6 +978,10 @@ bool cbm_pkgmap_try_parse(const char *basename, const char *rel_path, const char parse_gemspec(source, source_len, rel_path, entries); return true; } + if (strcmp(basename, "Package.swift") == 0) { + parse_package_swift(source, source_len, rel_path, entries); + return true; + } return false; } @@ -773,7 +1042,8 @@ static bool is_pkgmap_manifest_basename(const char *basename) { strcmp(basename, "Cargo.toml") == 0 || strcmp(basename, "pyproject.toml") == 0 || strcmp(basename, "composer.json") == 0 || strcmp(basename, "pubspec.yaml") == 0 || strcmp(basename, "pom.xml") == 0 || strcmp(basename, "build.gradle") == 0 || - strcmp(basename, "build.gradle.kts") == 0 || strcmp(basename, "mix.exs") == 0) { + strcmp(basename, "build.gradle.kts") == 0 || strcmp(basename, "mix.exs") == 0 || + strcmp(basename, "Package.swift") == 0) { return true; } return ends_with(basename, ".gemspec"); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 6cb2a6150..1de99c7b4 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -2165,6 +2165,104 @@ TEST(pipeline_go_cross_package_call) { PASS(); } +/* End-to-end (issue #551 item 1): two SwiftPM packages, Core and App, + * indexed under one root. App declares a local path dependency on Core and + * a target dependency on Core's product; App.swift does a bare + * `import Core`. This only resolves because Core's OWN Package.swift + * self-registers "Core" -> Core/Sources/Core in the shared pkgmap, landing + * App.swift's IMPORTS edge on that directory's Folder node -- proving real + * cross-package resolution. Asserts the EXACT provider node (the + * Core/Sources/Core Folder, found by its real QN, not a substring guess) + * and the exact edge (from App.swift's own file node to that provider) -- + * stronger than repro_issue408.c/repro_issue56.c's own count/substring + * checks, which this test intentionally does not settle for. */ +TEST(pipeline_swift_cross_package_import) { + const char *files[] = { + "Core/Package.swift", + "Core/Sources/Core/Core.swift", + "App/Package.swift", + "App/Sources/App/App.swift", + }; + const char *contents[] = { + ("let package = Package(\n" + " name: \"Core\",\n" + " products: [.library(name: \"Core\", targets: [\"Core\"])],\n" + " targets: [.target(name: \"Core\", dependencies: [])]\n" + ")\n"), + + "public func coreValue() -> Int {\n return 1\n}\n", + + ("let package = Package(\n" + " name: \"App\",\n" + " dependencies: [.package(path: \"../Core\")],\n" + " targets: [.target(name: \"App\", dependencies: [\n" + " .product(name: \"Core\", package: \"Core\")\n" + " ])]\n" + ")\n"), + + ("import Core\n\n" + "func run() -> Int {\n return coreValue()\n}\n"), + }; + + if (setup_lang_repo(files, contents, 4) != 0) + FAIL("tmpdir"); + char db[512]; + snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); + + cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + + cbm_store_t *s = cbm_store_open_path(db); + ASSERT_NOT_NULL(s); + const char *proj = cbm_pipeline_project_name(p); + + /* The exact provider node: the Folder for Core/Sources/Core, which is + * exactly what Core/Package.swift's OWN self-registration resolves to + * (see cbm_pkgmap_build: pkg name "Core" -> fqn_module(entry_rel)). + * cbm_pipeline_fqn_module and cbm_pipeline_fqn_folder agree on this + * path (no extension on any segment to strip), so this is the same QN + * the structural pass gave the real Folder node -- not a guess. */ + char *provider_qn = cbm_pipeline_fqn_folder(proj, "Core/Sources/Core"); + ASSERT_NOT_NULL(provider_qn); + cbm_node_t provider = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, proj, provider_qn, &provider), CBM_STORE_OK); + ASSERT_STR_EQ(provider.label, "Folder"); + free(provider_qn); + + /* The exact importing file node: App/Sources/App/App.swift. */ + char *importer_qn = cbm_pipeline_fqn_compute(proj, "App/Sources/App/App.swift", "__file__"); + ASSERT_NOT_NULL(importer_qn); + cbm_node_t importer = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, proj, importer_qn, &importer), CBM_STORE_OK); + free(importer_qn); + + /* The IMPORTS edge must run from THAT importer to THAT provider -- + * not merely "some edge lands on a QN containing Core" (a node named + * e.g. "AppCore" would have false-passed the old substring check). */ + cbm_edge_t *edges = NULL; + int ec = 0; + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, importer.id, "IMPORTS", &edges, &ec), + CBM_STORE_OK); + + bool found_exact_edge = false; + for (int i = 0; i < ec; i++) { + if (edges[i].target_id == provider.id) { + found_exact_edge = true; + } + } + ASSERT_TRUE(found_exact_edge); + + if (edges) + cbm_store_free_edges(edges, ec); + cbm_node_free_fields(&provider); + cbm_node_free_fields(&importer); + cbm_store_close(s); + cbm_pipeline_free(p); + teardown_lang_repo(); + PASS(); +} + TEST(pipeline_python_cross_module_call) { /* Port of TestPythonCrossModuleCallViaImport */ const char *files[] = {"utils.py", "main.py"}; @@ -5247,6 +5345,276 @@ static int pkg_entries_has_name(const cbm_pkg_entries_t *e, const char *name) { return 0; } +/* Helper: return the entry_rel registered for `name`, or NULL. */ +static const char *pkg_entries_entry_for(const cbm_pkg_entries_t *e, const char *name) { + for (int i = 0; i < e->count; i++) { + if (e->items[i].pkg_name && strcmp(e->items[i].pkg_name, name) == 0) + return e->items[i].entry_rel; + } + return NULL; +} + +/* ── SwiftPM Package.swift manifest resolution (issue #551 item 1) ── + * + * parse_package_swift is a literal pattern-extractor (mirrors + * parse_cargo_toml), not a Swift evaluator. These call cbm_pkgmap_try_parse + * directly, covering the RED categories the maintainer asked for (local + * path deps, remote identities, products not aliasing, targets, target-name + * deps, literal + computed `path:`, and comment/string false positives) + * plus fail-closed ambiguous-name cases. See pipeline_swift_cross_package_import + * above for the full end-to-end proof. */ + +TEST(pkgmap_swift_targets_registers_module) { + static const char src[] = + "// swift-tools-version:5.9\n" + "import PackageDescription\n" + "let package = Package(\n" + " name: \"Core\",\n" + " targets: [.target(name: \"Core\", dependencies: [])]\n" + ")\n"; + cbm_pkg_entries_t entries; + cbm_pkg_entries_init(&entries); + bool ok = cbm_pkgmap_try_parse("Package.swift", "Core/Package.swift", src, + (int)strlen(src), &entries); + ASSERT_TRUE(ok); + ASSERT_TRUE(pkg_entries_has_name(&entries, "Core")); + ASSERT_STR_EQ(pkg_entries_entry_for(&entries, "Core"), "Core/Sources/Core"); + cbm_pkg_entries_free(&entries); + PASS(); +} + +/* Products deliberately do NOT self-register a separate alias: a product + * name is not generally an importable module (SwiftPM lets it alias + * multiple targets, or none sharing its own name), so only the underlying + * target -- under its OWN name -- registers. */ +TEST(pkgmap_swift_products_do_not_register_alias) { + static const char src[] = + "let package = Package(\n" + " name: \"Core\",\n" + " products: [.library(name: \"CoreKit\", targets: [\"CoreImpl\"])],\n" + " targets: [.target(name: \"CoreImpl\", dependencies: [])]\n" + ")\n"; + cbm_pkg_entries_t entries; + cbm_pkg_entries_init(&entries); + bool ok = cbm_pkgmap_try_parse("Package.swift", "Core/Package.swift", src, + (int)strlen(src), &entries); + ASSERT_TRUE(ok); + ASSERT_FALSE(pkg_entries_has_name(&entries, "CoreKit")); + ASSERT_TRUE(pkg_entries_has_name(&entries, "CoreImpl")); + ASSERT_STR_EQ(pkg_entries_entry_for(&entries, "CoreImpl"), "Core/Sources/CoreImpl"); + ASSERT_EQ(entries.count, 1); + cbm_pkg_entries_free(&entries); + PASS(); +} + +/* Regression: a target whose `name:` is the LAST argument, immediately + * followed by the call's own closing ')' with no trailing comma, must still + * register. swift_quoted_literal's terminator check used to compare against + * `end` with a strict '<', but every caller passes the wrapping call's own + * ')' position AS `end` -- so the literal's closing quote landing exactly + * on that boundary was wrongly rejected as "unterminated". Every other + * fixture in this file happens to follow `name:` with `dependencies:` or a + * comma, so this specific shape was previously untested and unnoticed. */ +TEST(pkgmap_swift_target_name_immediately_before_close_paren) { + static const char src[] = + "let package = Package(\n" + " name: \"Core\",\n" + " targets: [.target(name: \"Core\")]\n" + ")\n"; + cbm_pkg_entries_t entries; + cbm_pkg_entries_init(&entries); + bool ok = cbm_pkgmap_try_parse("Package.swift", "Core/Package.swift", src, + (int)strlen(src), &entries); + ASSERT_TRUE(ok); + ASSERT_TRUE(pkg_entries_has_name(&entries, "Core")); + ASSERT_STR_EQ(pkg_entries_entry_for(&entries, "Core"), "Core/Sources/Core"); + ASSERT_EQ(entries.count, 1); + cbm_pkg_entries_free(&entries); + PASS(); +} + +/* A literal `path:` argument overrides the Sources/ convention. */ +TEST(pkgmap_swift_target_honors_literal_path) { + static const char src[] = + "let package = Package(\n" + " name: \"Core\",\n" + " targets: [.target(name: \"Core\", path: \"Vendor/CoreLegacy\")]\n" + ")\n"; + cbm_pkg_entries_t entries; + cbm_pkg_entries_init(&entries); + bool ok = cbm_pkgmap_try_parse("Package.swift", "Core/Package.swift", src, + (int)strlen(src), &entries); + ASSERT_TRUE(ok); + ASSERT_TRUE(pkg_entries_has_name(&entries, "Core")); + ASSERT_STR_EQ(pkg_entries_entry_for(&entries, "Core"), "Core/Vendor/CoreLegacy"); + cbm_pkg_entries_free(&entries); + PASS(); +} + +/* A `path:` argument that IS present but not a bare literal (computed) is + * unknowable -- SwiftPM would not use the Sources/ convention here, + * so guessing it anyway would mint a location likely to be wrong. Skip the + * target entirely (fail closed), even though its `name:` is a valid + * literal. */ +TEST(pkgmap_swift_target_computed_path_fails_closed) { + static const char src[] = + "let customPath = computePath()\n" + "let package = Package(\n" + " name: \"Core\",\n" + " targets: [.target(name: \"Core\", path: customPath)]\n" + ")\n"; + cbm_pkg_entries_t entries; + cbm_pkg_entries_init(&entries); + bool ok = cbm_pkgmap_try_parse("Package.swift", "Core/Package.swift", src, + (int)strlen(src), &entries); + ASSERT_TRUE(ok); + ASSERT_EQ(entries.count, 0); + cbm_pkg_entries_free(&entries); + PASS(); +} + +/* A `.target(` spelled inside a `//` line comment, a nesting-aware + * slash-star block comment, or a string literal must never be mistaken for a live + * declaration -- the bug a raw strstr scan cannot avoid. Only the one real + * target registers. */ +TEST(pkgmap_swift_target_in_comment_or_string_not_registered) { + static const char src[] = + "// .target(name: \"Decoy\")\n" + "/* outer /* nested */ still a comment: .target(name: \"NestedDecoy\") */\n" + "let manifestSnippet = \".target(name: \\\"StringDecoy\\\")\"\n" + "let package = Package(\n" + " name: \"App\",\n" + " targets: [.target(name: \"App\", dependencies: [])]\n" + ")\n"; + cbm_pkg_entries_t entries; + cbm_pkg_entries_init(&entries); + bool ok = cbm_pkgmap_try_parse("Package.swift", "App/Package.swift", src, + (int)strlen(src), &entries); + ASSERT_TRUE(ok); + ASSERT_TRUE(pkg_entries_has_name(&entries, "App")); + ASSERT_FALSE(pkg_entries_has_name(&entries, "Decoy")); + ASSERT_FALSE(pkg_entries_has_name(&entries, "NestedDecoy")); + ASSERT_FALSE(pkg_entries_has_name(&entries, "StringDecoy")); + ASSERT_EQ(entries.count, 1); + cbm_pkg_entries_free(&entries); + PASS(); +} + +/* Local path + remote url dependencies (`.package(path:)` / `.package(url:)`) + * mint NO entries of their own -- mirroring package.json/Cargo.toml, only a + * manifest's OWN products/targets self-register. A local sibling's name is + * produced by ITS OWN Package.swift when the repo-wide walk reaches it + * (see repro_issue408.c's JS-workspace analog); a remote dependency has no + * local path to point at, so nothing is minted (fail-closed). */ +TEST(pkgmap_swift_dependencies_do_not_leak_entries) { + static const char src[] = + "let package = Package(\n" + " name: \"App\",\n" + " dependencies: [\n" + " .package(path: \"../Core\"),\n" + " .package(url: \"https://github.com/example/RemoteKit.git\", from: \"1.0.0\")\n" + " ],\n" + " targets: [.target(name: \"App\", dependencies: [])]\n" + ")\n"; + cbm_pkg_entries_t entries; + cbm_pkg_entries_init(&entries); + bool ok = cbm_pkgmap_try_parse("Package.swift", "App/Package.swift", src, + (int)strlen(src), &entries); + ASSERT_TRUE(ok); + ASSERT_TRUE(pkg_entries_has_name(&entries, "App")); + ASSERT_FALSE(pkg_entries_has_name(&entries, "Core")); + ASSERT_FALSE(pkg_entries_has_name(&entries, "RemoteKit")); + ASSERT_EQ(entries.count, 1); + cbm_pkg_entries_free(&entries); + PASS(); +} + +/* Only App itself (the declaring target) registers -- a bare same-package + * target-name dependency ("Core") and a cross-package product dependency + * (Utils/UtilsPkg) name OTHER modules, not this manifest's own + * products/targets, so neither mints an entry. */ +TEST(pkgmap_swift_target_name_dependency_does_not_leak_entry) { + static const char src[] = + "let package = Package(\n" + " name: \"App\",\n" + " targets: [.target(name: \"App\", dependencies: [\n" + " \"Core\",\n" + " .product(name: \"Utils\", package: \"UtilsPkg\")\n" + " ])]\n" + ")\n"; + cbm_pkg_entries_t entries; + cbm_pkg_entries_init(&entries); + bool ok = cbm_pkgmap_try_parse("Package.swift", "App/Package.swift", src, + (int)strlen(src), &entries); + ASSERT_TRUE(ok); + ASSERT_TRUE(pkg_entries_has_name(&entries, "App")); + ASSERT_FALSE(pkg_entries_has_name(&entries, "Core")); + ASSERT_FALSE(pkg_entries_has_name(&entries, "Utils")); + ASSERT_FALSE(pkg_entries_has_name(&entries, "UtilsPkg")); + ASSERT_EQ(entries.count, 1); + cbm_pkg_entries_free(&entries); + PASS(); +} + +/* Fail-closed on any `name:` that is not a bare literal: a computed + * variable, and a literal concatenated with a dynamic suffix (which a + * naive quote-scan would wrongly accept as "App"). Neither mints an entry, + * though the manifest is still recognized and parsed. */ +TEST(pkgmap_swift_ambiguous_target_name_fails_closed) { + static const char dynamic_src[] = + "let generatedName = \"App\" + String(buildNumber)\n" + "let package = Package(\n" + " name: \"App\",\n" + " targets: [.target(name: generatedName, dependencies: [])]\n" + ")\n"; + static const char concat_src[] = + "let package = Package(\n" + " name: \"App\",\n" + " targets: [.target(name: \"App\" + suffix, dependencies: [])]\n" + ")\n"; + + cbm_pkg_entries_t entries; + cbm_pkg_entries_init(&entries); + ASSERT_TRUE(cbm_pkgmap_try_parse("Package.swift", "App/Package.swift", dynamic_src, + (int)strlen(dynamic_src), &entries)); + ASSERT_EQ(entries.count, 0); + cbm_pkg_entries_free(&entries); + + cbm_pkg_entries_init(&entries); + ASSERT_TRUE(cbm_pkgmap_try_parse("Package.swift", "App/Package.swift", concat_src, + (int)strlen(concat_src), &entries)); + ASSERT_EQ(entries.count, 0); + cbm_pkg_entries_free(&entries); + PASS(); +} + +/* The repo-wide manifest walker must also recognize "Package.swift" -- a + * second code path (is_pkgmap_manifest_basename) from the direct + * cbm_pkgmap_try_parse calls above. */ +TEST(pkgmap_swift_scan_repo_finds_nested_manifest) { + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_pkgmap_swift_XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) + FAIL("tmpdir"); + char dir[512]; + snprintf(dir, sizeof(dir), "%s/Core", tmpdir); + cbm_mkdir(dir); + write_temp_file(tmpdir, "Core/Package.swift", + "let package = Package(\n" + " name: \"Core\",\n" + " targets: [.target(name: \"Core\", dependencies: [])]\n" + ")\n"); + + cbm_pkg_entries_t entries; + cbm_pkg_entries_init(&entries); + cbm_pkgmap_scan_repo(tmpdir, &entries, NULL, 0); + ASSERT_TRUE(pkg_entries_has_name(&entries, "Core")); + cbm_pkg_entries_free(&entries); + + th_rmtree(tmpdir); + PASS(); +} + /* The pkgmap repo walk must honor discovery exclusions (issue #792: a * gitignored huge subtree kept the pkgmap walk busy for 15 minutes). * Control run first (no exclusions → BOTH manifests parsed) so the @@ -7362,6 +7730,7 @@ SUITE(pipeline) { RUN_TEST(pipeline_python_project); RUN_TEST(pipeline_imports_multi_symbol_edges); RUN_TEST(pipeline_go_cross_package_call); + RUN_TEST(pipeline_swift_cross_package_import); RUN_TEST(pipeline_python_cross_module_call); RUN_TEST(pipeline_go_type_classification); RUN_TEST(pipeline_go_grouped_types); @@ -7471,6 +7840,17 @@ SUITE(pipeline) { RUN_TEST(envscan_secret_file_exclusion); RUN_TEST(envscan_skips_ignored_dirs); RUN_TEST(envscan_non_url_values_skipped); + /* SwiftPM Package.swift manifest resolution (issue #551 item 1) */ + RUN_TEST(pkgmap_swift_targets_registers_module); + RUN_TEST(pkgmap_swift_products_do_not_register_alias); + RUN_TEST(pkgmap_swift_target_name_immediately_before_close_paren); + RUN_TEST(pkgmap_swift_target_honors_literal_path); + RUN_TEST(pkgmap_swift_target_computed_path_fails_closed); + RUN_TEST(pkgmap_swift_target_in_comment_or_string_not_registered); + RUN_TEST(pkgmap_swift_dependencies_do_not_leak_entries); + RUN_TEST(pkgmap_swift_target_name_dependency_does_not_leak_entry); + RUN_TEST(pkgmap_swift_ambiguous_target_name_fails_closed); + RUN_TEST(pkgmap_swift_scan_repo_finds_nested_manifest); /* Discovery-exclusion plumbing in auxiliary repo walks (#792) */ RUN_TEST(pipeline_relpath_excluded_boundary); RUN_TEST(pkgmap_scan_repo_honors_discovery_exclusions);