Skip to content

Commit ba46ee3

Browse files
committed
stabilize imports
1 parent f55b815 commit ba46ee3

File tree

10 files changed

+870
-37
lines changed

10 files changed

+870
-37
lines changed

src/ast/type_resolver/core/expression.zig

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,7 +1026,10 @@ fn synthCall(
10261026
if (self.module_exports) |me| {
10271027
const base = fa.target.Identifier.name;
10281028
if (me.isModuleAlias(base)) {
1029-
if (me.lookupExport(base, fa.field)) |_| {
1029+
if (me.lookupExport(base, fa.field)) |kind| {
1030+
if (kind != .Function) {
1031+
return TypeResolutionError.TypeMismatch;
1032+
}
10301033
if (self.function_registry) |registry| {
10311034
const reg_map = @as(*std.StringHashMap(*FunctionNode), @ptrCast(@alignCast(registry)));
10321035
if (reg_map.get(fa.field)) |function| {
@@ -1315,9 +1318,10 @@ fn synthFieldAccess(
13151318
if (fa.target.* == .Identifier) {
13161319
const base = fa.target.Identifier.name;
13171320
if (me.isModuleAlias(base)) {
1318-
if (me.lookupExport(base, fa.field)) |kind| {
1319-
const result_type = switch (kind) {
1321+
if (me.lookupExportInfo(base, fa.field)) |export_info| {
1322+
const result_type = switch (export_info.kind) {
13201323
.Function => blk: {
1324+
if (export_info.type_info) |ti| break :blk ti;
13211325
if (self.function_registry) |registry| {
13221326
const reg_map = @as(*std.StringHashMap(*FunctionNode), @ptrCast(@alignCast(registry)));
13231327
if (reg_map.get(fa.field)) |function| {
@@ -1326,8 +1330,32 @@ fn synthFieldAccess(
13261330
}
13271331
break :blk TypeInfo.unknown();
13281332
},
1329-
.Contract, .StructDecl, .EnumDecl, .BitfieldDecl => TypeInfo.unknown(),
1330-
.Constant, .Variable => TypeInfo.unknown(),
1333+
.Constant, .Variable => blk: {
1334+
if (export_info.type_info) |ti| break :blk ti;
1335+
1336+
// Fallback: resolved constant types are written back to symbols.
1337+
const root_scope: ?*const Scope = @as(?*const Scope, @ptrCast(self.symbol_table.root));
1338+
if (SymbolTable.findUp(root_scope, fa.field)) |sym| {
1339+
if (sym.typ) |ti| break :blk ti;
1340+
}
1341+
break :blk TypeInfo.unknown();
1342+
},
1343+
.StructDecl => if (export_info.type_info) |ti|
1344+
ti
1345+
else
1346+
TypeInfo.fromOraType(.{ .struct_type = fa.field }),
1347+
.BitfieldDecl => if (export_info.type_info) |ti|
1348+
ti
1349+
else
1350+
TypeInfo.fromOraType(.{ .bitfield_type = fa.field }),
1351+
.EnumDecl => if (export_info.type_info) |ti|
1352+
ti
1353+
else
1354+
TypeInfo.fromOraType(.{ .enum_type = fa.field }),
1355+
.Contract => if (export_info.type_info) |ti|
1356+
ti
1357+
else
1358+
TypeInfo.fromOraType(.{ .contract_type = fa.field }),
13311359
.LogDecl, .ErrorDecl => TypeInfo.unknown(),
13321360
};
13331361
fa.type_info = result_type;

src/imports/mod.test.zig

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,35 @@ test "imports: relative specifier must include .ora extension" {
117117
);
118118
}
119119

120+
test "imports: relative import cannot escape workspace root" {
121+
const allocator = testing.allocator;
122+
var tmp = testing.tmpDir(.{});
123+
defer tmp.cleanup();
124+
125+
try tmp.dir.makePath("project/contracts");
126+
try tmp.dir.makePath("outside");
127+
128+
try tmp.dir.writeFile(.{
129+
.sub_path = "outside/secret.ora",
130+
.data = "contract Secret { }",
131+
});
132+
try tmp.dir.writeFile(.{
133+
.sub_path = "project/contracts/entry.ora",
134+
.data = "const secret = @import(\"../../outside/secret.ora\");",
135+
});
136+
137+
const entry_path = try pathFromTmpAlloc(allocator, tmp, "project/contracts/entry.ora");
138+
defer allocator.free(entry_path);
139+
const workspace_root = try pathFromTmpAlloc(allocator, tmp, "project");
140+
defer allocator.free(workspace_root);
141+
142+
const roots = [_][]const u8{workspace_root};
143+
try testing.expectError(
144+
error.RelativeImportOutsideAllowedRoots,
145+
imports.validateNormalImportsWithOptions(allocator, entry_path, .{ .workspace_roots = roots[0..] }),
146+
);
147+
}
148+
120149
test "imports: package style resolves from workspace roots" {
121150
const allocator = testing.allocator;
122151
var tmp = testing.tmpDir(.{});

src/imports/mod.zig

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub const ImportValidationError = error{
2424
ImportAliasRequired,
2525
DuplicateImportAlias,
2626
RelativeImportMustIncludeOraExtension,
27+
RelativeImportOutsideAllowedRoots,
2728
InvalidImportSpecifier,
2829
PackageRootConflict,
2930
ParseFailed,
@@ -279,7 +280,6 @@ const Resolver = struct {
279280
};
280281

281282
const canonical_id = std.fmt.allocPrint(self.allocator, "file:{s}", .{resolved_path}) catch {
282-
self.allocator.free(resolved_path);
283283
return ImportValidationError.OutOfMemory;
284284
};
285285

@@ -325,6 +325,7 @@ const Resolver = struct {
325325
}
326326

327327
try self.states.put(record.canonical_id, .visiting);
328+
errdefer _ = self.states.remove(record.canonical_id);
328329
try self.stack.append(self.allocator, record.canonical_id);
329330
defer _ = self.stack.pop();
330331

@@ -407,6 +408,16 @@ const Resolver = struct {
407408
std.log.warn("Import target not found: '{s}' in module '{s}' ({s})", .{ specifier, importer.resolved_path, @errorName(err) });
408409
return ImportValidationError.ImportTargetNotFound;
409410
};
411+
errdefer self.allocator.free(resolved_path);
412+
413+
if (!try self.isAllowedRelativePath(resolved_path)) {
414+
std.log.warn("Relative import escapes allowed roots: '{s}' resolved to '{s}' from '{s}'", .{
415+
specifier,
416+
resolved_path,
417+
importer.resolved_path,
418+
});
419+
return ImportValidationError.RelativeImportOutsideAllowedRoots;
420+
}
410421

411422
const canonical_id = std.fmt.allocPrint(self.allocator, "file:{s}", .{resolved_path}) catch {
412423
self.allocator.free(resolved_path);
@@ -420,6 +431,42 @@ const Resolver = struct {
420431
};
421432
}
422433

434+
fn isPathWithinRoot(path: []const u8, root: []const u8) bool {
435+
if (!std.mem.startsWith(u8, path, root)) return false;
436+
if (path.len == root.len) return true;
437+
if (root.len == 0) return false;
438+
if (root[root.len - 1] == std.fs.path.sep) return true;
439+
return path[root.len] == std.fs.path.sep;
440+
}
441+
442+
fn isAllowedRelativePath(self: *Resolver, resolved_path: []const u8) ImportValidationError!bool {
443+
if (self.options.workspace_roots.len == 0 and self.options.include_roots.len == 0) {
444+
const cwd_root = std.fs.cwd().realpathAlloc(self.allocator, ".") catch {
445+
return ImportValidationError.OutOfMemory;
446+
};
447+
defer self.allocator.free(cwd_root);
448+
return isPathWithinRoot(resolved_path, cwd_root);
449+
}
450+
451+
for (self.options.workspace_roots) |root| {
452+
const real_root = std.fs.cwd().realpathAlloc(self.allocator, root) catch {
453+
continue;
454+
};
455+
defer self.allocator.free(real_root);
456+
if (isPathWithinRoot(resolved_path, real_root)) return true;
457+
}
458+
459+
for (self.options.include_roots) |root| {
460+
const real_root = std.fs.cwd().realpathAlloc(self.allocator, root) catch {
461+
continue;
462+
};
463+
defer self.allocator.free(real_root);
464+
if (isPathWithinRoot(resolved_path, real_root)) return true;
465+
}
466+
467+
return false;
468+
}
469+
423470
fn parsePackageSpecifier(specifier: []const u8) ImportValidationError!PackageSpecifier {
424471
const slash_index = std.mem.indexOfScalar(u8, specifier, '/') orelse return ImportValidationError.InvalidImportSpecifier;
425472
if (slash_index == 0 or slash_index + 1 >= specifier.len) {

0 commit comments

Comments
 (0)