Skip to content

Commit c1cb215

Browse files
authored
Merge pull request #9492 from roc-lang/fix-9488
Fix roc check package shorthand leak
2 parents f24d308 + c8690cb commit c1cb215

7 files changed

Lines changed: 120 additions & 48 deletions

File tree

src/cli/test/roc_subcommands.zig

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1832,6 +1832,39 @@ test "roc check does not panic on invalid package shorthand import (issue 9084)"
18321832
try testing.expect(result.stderr.len > 0);
18331833
}
18341834

1835+
test "roc check succeeds with unused app package shorthand (issue 9488)" {
1836+
const testing = std.testing;
1837+
const gpa = testing.allocator;
1838+
1839+
const result = try util.runRoc(gpa, &.{ "check", "--no-cache" }, "test/cli/package_shorthand_check_app/main.roc");
1840+
defer gpa.free(result.stdout);
1841+
defer gpa.free(result.stderr);
1842+
1843+
try testing.expect(result.term == .exited and result.term.exited == 0);
1844+
try testing.expect(std.mem.find(u8, result.stderr, "leaked") == null);
1845+
try testing.expect(std.mem.find(u8, result.stderr, "panic") == null);
1846+
}
1847+
1848+
test "roc check resolves and checks a used sibling package shorthand (issue 9488)" {
1849+
const testing = std.testing;
1850+
const gpa = testing.allocator;
1851+
1852+
const result = try util.runRoc(gpa, &.{ "check", "--no-cache" }, "test/cli/package_shorthand_used_app/main.roc");
1853+
defer gpa.free(result.stdout);
1854+
defer gpa.free(result.stderr);
1855+
1856+
// The app imports from a sibling package (declared via a `../` shorthand)
1857+
// that lives outside the app directory. Registering the package directory
1858+
// as a workspace root lets the sandbox resolve it, so the package is
1859+
// genuinely type-checked rather than silently skipped: the planted type
1860+
// error inside the package must surface, referencing the package file.
1861+
try testing.expect(result.term == .exited);
1862+
try testing.expect(std.mem.find(u8, result.stderr, "leaked") == null);
1863+
try testing.expect(std.mem.find(u8, result.stderr, "panic") == null);
1864+
try testing.expect(std.mem.find(u8, result.stderr, "package_shorthand_used_pkg") != null);
1865+
try testing.expect(std.mem.find(u8, result.stderr, "TYPE MISMATCH") != null);
1866+
}
1867+
18351868
test "roc check does not hang on tag union type alias inside List (issue 9481)" {
18361869
const testing = std.testing;
18371870
const gpa = testing.allocator;

src/compile/compile_build.zig

Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,14 @@ const PathUtils = struct {
9696

9797
fn isWithinRoot(candidate: []const u8, roots: []const []const u8) bool {
9898
for (roots) |root| {
99-
if (std.mem.startsWith(u8, candidate, root)) return true;
99+
if (!std.mem.startsWith(u8, candidate, root)) continue;
100+
// Only match whole path segments so root "/x/app" does not capture
101+
// sibling "/x/app-secrets". An exact match counts, as does a match
102+
// where the next character (or the root's own trailing char) is a
103+
// path separator.
104+
if (candidate.len == root.len) return true;
105+
if (std.fs.path.isSep(candidate[root.len])) return true;
106+
if (root.len > 0 and std.fs.path.isSep(root[root.len - 1])) return true;
100107
}
101108
return false;
102109
}
@@ -426,6 +433,13 @@ pub const BuildEnv = struct {
426433
self.coordinator = coord;
427434
}
428435

436+
/// Register a directory as a workspace root, skipping it if an existing root
437+
/// already contains it. Takes ownership of nothing; copies `dir` when stored.
438+
fn addWorkspaceRoot(self: *BuildEnv, dir: []const u8) !void {
439+
if (PathUtils.isWithinRoot(dir, self.workspace_roots.items)) return;
440+
try self.workspace_roots.append(try self.gpa.dupe(u8, dir));
441+
}
442+
429443
/// Phase 1: Parse headers, create package entries, extract TargetsConfig, and populate
430444
/// shorthands. Does NOT init the Coordinator, allowing the caller to inspect
431445
/// discovered state (e.g., TargetsConfig) and change the target before compilation.
@@ -437,7 +451,7 @@ pub const BuildEnv = struct {
437451
const root_dir = if (std.fs.path.dirname(root_abs)) |d| try std.fs.path.resolve(self.gpa, &.{d}) else try self.gpa.dupe(u8, ".");
438452
self.discovered_root_dir = root_dir;
439453

440-
try self.workspace_roots.append(try self.gpa.dupe(u8, root_dir));
454+
try self.addWorkspaceRoot(root_dir);
441455

442456
var header_info = try self.parseHeaderDeps(root_abs);
443457
defer header_info.deinit(self.gpa);
@@ -1089,9 +1103,7 @@ pub const BuildEnv = struct {
10891103
// can be resolved. This is needed for both URL packages (cached paths) and
10901104
// relative paths that may point outside the app directory (e.g., ../platform/main.roc)
10911105
if (std.fs.path.dirname(plat_path)) |plat_dir| {
1092-
if (!PathUtils.isWithinRoot(plat_dir, self.workspace_roots.items)) {
1093-
try self.workspace_roots.append(try self.gpa.dupe(u8, plat_dir));
1094-
}
1106+
try self.addWorkspaceRoot(plat_dir);
10951107
}
10961108

10971109
// Packages map
@@ -1112,12 +1124,17 @@ pub const BuildEnv = struct {
11121124
const cached_path = try self.resolveUrlPackage(relp);
11131125
// Add cache directory to workspace roots for URL packages
11141126
if (std.fs.path.dirname(cached_path)) |cache_pkg_dir| {
1115-
try self.workspace_roots.append(try self.gpa.dupe(u8, cache_pkg_dir));
1127+
try self.addWorkspaceRoot(cache_pkg_dir);
11161128
}
11171129
break :blk cached_path;
11181130
} else blk: {
11191131
const header_dir2 = std.fs.path.dirname(file_abs) orelse ".";
1120-
break :blk try PathUtils.makeAbsolute(self.gpa, header_dir2, relp);
1132+
const abs_path = try PathUtils.makeAbsolute(self.gpa, header_dir2, relp);
1133+
errdefer self.gpa.free(abs_path);
1134+
if (std.fs.path.dirname(abs_path)) |pkg_dir| {
1135+
try self.addWorkspaceRoot(pkg_dir);
1136+
}
1137+
break :blk abs_path;
11211138
};
11221139

11231140
// TODO: actually handle duplicate keys
@@ -1147,7 +1164,7 @@ pub const BuildEnv = struct {
11471164
const cached_path = try self.resolveUrlPackage(relp);
11481165
// Add cache directory to workspace roots for URL packages
11491166
if (std.fs.path.dirname(cached_path)) |cache_pkg_dir| {
1150-
try self.workspace_roots.append(try self.gpa.dupe(u8, cache_pkg_dir));
1167+
try self.addWorkspaceRoot(cache_pkg_dir);
11511168
}
11521169
break :blk cached_path;
11531170
} else blk: {
@@ -1188,7 +1205,7 @@ pub const BuildEnv = struct {
11881205
const cached_path = try self.resolveUrlPackage(relp);
11891206
// Add cache directory to workspace roots for URL packages
11901207
if (std.fs.path.dirname(cached_path)) |cache_pkg_dir| {
1191-
try self.workspace_roots.append(try self.gpa.dupe(u8, cache_pkg_dir));
1208+
try self.addWorkspaceRoot(cache_pkg_dir);
11921209
}
11931210
break :blk cached_path;
11941211
} else blk: {
@@ -1502,6 +1519,29 @@ pub const BuildEnv = struct {
15021519
});
15031520
}
15041521

1522+
fn putPackageShorthand(self: *BuildEnv, pack: *Package, alias: []const u8, target_name: []const u8, root_file: []const u8) ![]const u8 {
1523+
const key = try self.gpa.dupe(u8, alias);
1524+
errdefer self.gpa.free(key);
1525+
1526+
const name = try self.gpa.dupe(u8, target_name);
1527+
errdefer self.gpa.free(name);
1528+
1529+
const root_file_owned = try self.gpa.dupe(u8, root_file);
1530+
errdefer self.gpa.free(root_file_owned);
1531+
1532+
if (pack.shorthands.fetchRemove(key)) |old_entry| {
1533+
freeConstSlice(self.gpa, old_entry.key);
1534+
freeConstSlice(self.gpa, old_entry.value.name);
1535+
freeConstSlice(self.gpa, old_entry.value.root_file);
1536+
}
1537+
try pack.shorthands.put(self.gpa, key, .{
1538+
.name = name,
1539+
.root_file = root_file_owned,
1540+
});
1541+
1542+
return name;
1543+
}
1544+
15051545
const PkgSinkCtx = struct {
15061546
gpa: Allocator,
15071547
sink: *OrderedSink,
@@ -1594,9 +1634,7 @@ pub const BuildEnv = struct {
15941634
return error.InvalidDependency;
15951635
}
15961636

1597-
const dep_key = try self.gpa.dupe(u8, alias);
1598-
const dep_name = try self.gpa.dupe(u8, alias);
1599-
try self.ensurePackage(dep_name, .platform, abs);
1637+
try self.ensurePackage(alias, .platform, abs);
16001638

16011639
// Transfer provides entries and targets_config from parsed header to platform package
16021640
if (self.packages.getPtr(alias)) |plat_pkg| {
@@ -1613,17 +1651,7 @@ pub const BuildEnv = struct {
16131651
// Re-fetch pack pointer since ensurePackage may have caused HashMap reallocation
16141652
pack = self.packages.getPtr(pkg_name).?;
16151653

1616-
// If key already exists, free the old value before overwriting
1617-
if (pack.shorthands.fetchRemove(dep_key)) |old_entry| {
1618-
freeConstSlice(self.gpa, old_entry.key);
1619-
freeConstSlice(self.gpa, old_entry.value.name);
1620-
freeConstSlice(self.gpa, old_entry.value.root_file);
1621-
}
1622-
try pack.shorthands.put(self.gpa, dep_key, .{
1623-
.name = dep_name,
1624-
.root_file = try self.gpa.dupe(u8, abs),
1625-
});
1626-
1654+
const dep_name = try self.putPackageShorthand(pack, alias, alias, abs);
16271655
try self.populatePackageShorthands(dep_name, &child_info);
16281656

16291657
// Register platform-exposed modules as packages so apps can import them
@@ -1648,16 +1676,7 @@ pub const BuildEnv = struct {
16481676
pack = self.packages.getPtr(pkg_name).?;
16491677

16501678
// Also add to app's shorthands so imports resolve correctly
1651-
const mod_key = try self.gpa.dupe(u8, module_name);
1652-
if (pack.shorthands.fetchRemove(mod_key)) |old_entry| {
1653-
freeConstSlice(self.gpa, old_entry.key);
1654-
freeConstSlice(self.gpa, old_entry.value.name);
1655-
freeConstSlice(self.gpa, old_entry.value.root_file);
1656-
}
1657-
try pack.shorthands.put(self.gpa, mod_key, .{
1658-
.name = try self.gpa.dupe(u8, module_name),
1659-
.root_file = try self.gpa.dupe(u8, module_path),
1660-
});
1679+
_ = try self.putPackageShorthand(pack, module_name, module_name, module_path);
16611680

16621681
// Add to pending list - will be registered after schedulers are created
16631682
// Use the QUALIFIED name (e.g., "pf.Stdout") because that's how imports are tracked
@@ -1704,10 +1723,7 @@ pub const BuildEnv = struct {
17041723
return error.InvalidDependency;
17051724
}
17061725

1707-
const dep_key = try self.gpa.dupe(u8, alias);
1708-
const dep_name = try self.gpa.dupe(u8, alias);
1709-
1710-
try self.ensurePackage(dep_name, child_info.kind, abs);
1726+
try self.ensurePackage(alias, child_info.kind, abs);
17111727

17121728
// Transfer provides entries from parsed header to platform package
17131729
if (child_info.kind == .platform) {
@@ -1722,17 +1738,7 @@ pub const BuildEnv = struct {
17221738
// Re-fetch pack pointer since ensurePackage may have caused HashMap reallocation
17231739
pack = self.packages.getPtr(pkg_name).?;
17241740

1725-
// If key already exists, free the old value before overwriting
1726-
if (pack.shorthands.fetchRemove(dep_key)) |old_entry| {
1727-
freeConstSlice(self.gpa, old_entry.key);
1728-
freeConstSlice(self.gpa, old_entry.value.name);
1729-
freeConstSlice(self.gpa, old_entry.value.root_file);
1730-
}
1731-
try pack.shorthands.put(self.gpa, dep_key, .{
1732-
.name = dep_name,
1733-
.root_file = try self.gpa.dupe(u8, abs),
1734-
});
1735-
1741+
const dep_name = try self.putPackageShorthand(pack, alias, alias, abs);
17361742
try self.populatePackageShorthands(dep_name, &child_info);
17371743
}
17381744
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
app [main!] {
2+
pf: platform "../../fx/platform/main.roc",
3+
pkg: "../package_shorthand_check_pkg/main.roc",
4+
}
5+
6+
import pf.Stdout
7+
8+
main! = || {
9+
Stdout.line!("hello")
10+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package [] {}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
app [main!] {
2+
pf: platform "../../fx/platform/main.roc",
3+
pkg: "../package_shorthand_used_pkg/main.roc",
4+
}
5+
6+
import pf.Stdout
7+
import pkg.Thing
8+
9+
main! = || {
10+
Stdout.line!(Thing.greeting)
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Thing := [].{
2+
greeting : Str
3+
greeting = "hello from sibling package"
4+
5+
# Deliberate type error: a checker that reaches this sibling package will
6+
# report a TYPE MISMATCH here. If the package were silently skipped, no
7+
# error would be surfaced.
8+
mismatched : U64
9+
mismatched = "not a number"
10+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package [Thing] {}

0 commit comments

Comments
 (0)