Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
zig-version: ["0.15.1"]
zig-version: ["0.15.1", "0.15.2"]

steps:
- uses: actions/checkout@v3
Expand Down
37 changes: 19 additions & 18 deletions src/interface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -230,31 +230,31 @@ fn generateTypeHint(comptime expected: type, comptime got: type) ?[]const u8 {
const got_info = @typeInfo(got);

// Check for common slice constness issues
if (exp_info == .Pointer and got_info == .Pointer) {
const exp_ptr = exp_info.Pointer;
const got_ptr = got_info.Pointer;
if (exp_info == .pointer and got_info == .pointer) {
const exp_ptr = exp_info.pointer;
const got_ptr = got_info.pointer;
if (exp_ptr.is_const and !got_ptr.is_const) {
return "Consider making the parameter type const (e.g., []const u8 instead of []u8)";
}
}

// Check for optional vs non-optional mismatches
if (exp_info == .Optional and got_info != .Optional) {
if (exp_info == .optional and got_info != .optional) {
return "The expected type is optional. Consider wrapping the parameter in '?'";
}
if (exp_info != .Optional and got_info == .Optional) {
if (exp_info != .optional and got_info == .optional) {
return "The expected type is non-optional. Remove the '?' from the parameter type";
}

// Check for enum type mismatches
if (exp_info == .Enum and got_info == .Enum) {
if (exp_info == .@"enum" and got_info == .@"enum") {
return "Check that the enum values and field names match exactly";
}

// Check for struct field mismatches
if (exp_info == .Struct and got_info == .Struct) {
const exp_s = exp_info.Struct;
const got_s = got_info.Struct;
if (exp_info == .@"struct" and got_info == .@"struct") {
const exp_s = exp_info.@"struct";
const got_s = got_info.@"struct";
if (exp_s.fields.len != got_s.fields.len) {
return "The structs have different numbers of fields";
}
Expand All @@ -263,9 +263,9 @@ fn generateTypeHint(comptime expected: type, comptime got: type) ?[]const u8 {
}

// Generic catch-all for pointer size mismatches
if (exp_info == .Pointer and got_info == .Pointer) {
const exp_ptr = exp_info.Pointer;
const got_ptr = got_info.Pointer;
if (exp_info == .pointer and got_info == .pointer) {
const exp_ptr = exp_info.pointer;
const got_ptr = got_info.pointer;
if (exp_ptr.size != got_ptr.size) {
return "Check pointer type (single item vs slice vs many-item)";
}
Expand All @@ -280,7 +280,7 @@ fn formatTypeMismatch(
comptime got: type,
indent: []const u8,
) []const u8 {
var result = std.fmt.comptimePrint(
const base = std.fmt.comptimePrint(
"{s}Expected: {s}\n{s}Got: {s}",
.{
indent,
Expand All @@ -292,10 +292,10 @@ fn formatTypeMismatch(

// Add hint if available
if (generateTypeHint(expected, got)) |hint| {
result = result ++ std.fmt.comptimePrint("\n {s}Hint: {s}", .{ indent, hint });
return base ++ std.fmt.comptimePrint("\n {s}Hint: {s}", .{ indent, hint });
}

return result;
return base;
}

fn generateVTableType(comptime methods: anytype, comptime embedded_interfaces: anytype, comptime has_embeds: bool) type {
Expand Down Expand Up @@ -472,7 +472,7 @@ fn CreateValidationNamespace(comptime methods: anytype, comptime embedded_interf
var interface_count: usize = 0;

// Count primary interface
if (@hasDecl(Methods, method_name)) {
if (@hasField(Methods, method_name)) {
interface_count += 1;
}

Expand All @@ -492,7 +492,8 @@ fn CreateValidationNamespace(comptime methods: anytype, comptime embedded_interf
var index: usize = 0;

// Add primary interface
if (@hasDecl(Methods, method_name)) {
if (@hasField(Methods, method_name)) {
interfaces[index] = "primary";
index += 1;
}

Expand All @@ -515,7 +516,7 @@ fn CreateValidationNamespace(comptime methods: anytype, comptime embedded_interf
pub fn hasMethod(comptime method_name: []const u8) bool {
comptime {
// Check primary interface
if (@hasDecl(Methods, method_name)) {
if (@hasField(Methods, method_name)) {
return true;
}

Expand Down
72 changes: 72 additions & 0 deletions test/complex.zig
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,75 @@ test "complex type support with embedding" {
try std.testing.expect(IComplexTypes.validation.incompatibilities(BadImpl1).len > 0);
try std.testing.expect(IComplexTypes.validation.incompatibilities(BadImpl2).len > 0);
}

test "const slice mismatch detected" {
const IProcessor = Interface(.{
.process = fn ([]const u8) void,
}, null);

const BadImpl = struct {
pub fn process(self: @This(), data: []u8) void {
_ = self;
_ = data;
}
};

const problems = comptime IProcessor.validation.incompatibilities(BadImpl);
try std.testing.expectEqual(@as(usize, 1), problems.len);
}

test "optional vs non-optional return mismatch detected" {
const ILookup = Interface(.{
.find = fn (u64) ?[]const u8,
}, null);

const BadImpl = struct {
pub fn find(self: @This(), id: u64) []const u8 {
_ = self;
_ = id;
return "data";
}
};

const problems = comptime ILookup.validation.incompatibilities(BadImpl);
try std.testing.expectEqual(@as(usize, 1), problems.len);
}

test "struct field type mismatch detected" {
const Config = struct { name: []const u8, value: u32 };
const WrongConfig = struct { name: []const u8, value: i32 };

const IConfigurable = Interface(.{
.configure = fn (Config) void,
}, null);

const BadImpl = struct {
pub fn configure(self: @This(), cfg: WrongConfig) void {
_ = self;
_ = cfg;
}
};

const problems = comptime IConfigurable.validation.incompatibilities(BadImpl);
try std.testing.expectEqual(@as(usize, 1), problems.len);
}

test "enum variant mismatch detected" {
const GoodStatus = enum { ok, err, pending };
const BadStatus = enum { ok, err };

const IStatusCheck = Interface(.{
.check = fn (GoodStatus) bool,
}, null);

const BadImpl = struct {
pub fn check(self: @This(), status: BadStatus) bool {
_ = self;
_ = status;
return true;
}
};

const problems = comptime IStatusCheck.validation.incompatibilities(BadImpl);
try std.testing.expectEqual(@as(usize, 1), problems.len);
}
40 changes: 40 additions & 0 deletions test/embedded.zig
Original file line number Diff line number Diff line change
Expand Up @@ -588,3 +588,43 @@ test "high-level: repository fallback chain with embedded interfaces" {
// Still only 1 backing store read
try std.testing.expectEqual(@as(usize, 1), backing.reads);
}

test "hasMethod finds primary interface methods" {
const IWriter = Interface(.{
.write = fn ([]const u8) anyerror!usize,
.flush = fn () anyerror!void,
}, null);

const has_write = comptime IWriter.validation.hasMethod("write");
const has_flush = comptime IWriter.validation.hasMethod("flush");
const has_close = comptime IWriter.validation.hasMethod("close");

try std.testing.expect(has_write);
try std.testing.expect(has_flush);
try std.testing.expect(!has_close);
}

test "detects ambiguity between primary and embedded method of same name" {
const IBase = Interface(.{
.process = fn ([]const u8) void,
}, null);

// Primary interface also defines 'process' with the SAME signature.
// This should be flagged as ambiguous, just like two embedded interfaces
// with the same method name are (see "interface embedding with conflicts" test).
const IDerived = Interface(.{
.process = fn ([]const u8) void,
}, .{IBase});

const Impl = struct {
pub fn process(self: @This(), data: []const u8) void {
_ = self;
_ = data;
}
};

const problems = comptime IDerived.validation.incompatibilities(Impl);
// Since both signatures match the implementation, the ONLY way
// to get problems.len > 0 is through ambiguity detection.
try std.testing.expect(problems.len > 0);
}
Loading