From ba74ad552592108ccc3e0ad6fb94eff639db2e45 Mon Sep 17 00:00:00 2001 From: Krzysztof Wolicki Date: Mon, 24 Aug 2026 14:41:34 +0200 Subject: [PATCH] Make `@hasDecl` return `true` only for public declarations --- doc/langref.html.in | 2 +- doc/langref/test_hasDecl_builtin.zig | 6 ++---- src/Sema.zig | 14 ++++++-------- test/behavior/hasdecl.zig | 16 ++++++++-------- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/doc/langref.html.in b/doc/langref.html.in index 27e6e9723f001fcf7d8f879e3f144a1aeaa14347..37a7ac4c3b875a641699b17437ce88f5d8a0f625 100644 --- a/doc/langref.html.in +++ b/doc/langref.html.in @@ -5013,7 +5013,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val {#header_open|@hasDecl#}
{#syntax#}@hasDecl(comptime Namespace: type, comptime name: []const u8) bool{#endsyntax#}
-

Returns whether or not a {#link|Namespace#} has a declaration matching {#syntax#}name{#endsyntax#}.

+

Returns whether or not a {#link|Namespace#} has a public declaration matching {#syntax#}name{#endsyntax#}.

{#code|test_hasDecl_builtin.zig#}

Caution: using {#syntax#}@hasDecl{#endsyntax#} to implement diff --git a/doc/langref/test_hasDecl_builtin.zig b/doc/langref/test_hasDecl_builtin.zig index 1be7dcdeb5b01523a3ce0b358b565f917a80b7d2..eff8337fd14711a744001df3f49b5e54494bebb4 100644 --- a/doc/langref/test_hasDecl_builtin.zig +++ b/doc/langref/test_hasDecl_builtin.zig @@ -11,10 +11,8 @@ const Foo = struct { test "@hasDecl" { try expect(@hasDecl(Foo, "blah")); - // Even though `hi` is private, @hasDecl returns true because this test is - // in the same file scope as Foo. It would return false if Foo was declared - // in a different file. - try expect(@hasDecl(Foo, "hi")); + // @hasDecl returns false for private declarations. + try expect(!@hasDecl(Foo, "hi")); // @hasDecl is for declarations; not fields. try expect(!@hasDecl(Foo, "nope")); diff --git a/src/Sema.zig b/src/Sema.zig index 63bee88f76f40e607532964b911b6ec2030f6e0f..d526352778100012be4c44309b1f5c06f03b8475 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -5977,7 +5977,7 @@ fn lookupIdentifier(sema: *Sema, block: *Block, name: InternPool.NullTerminatedS var namespace = block.namespace; while (true) { if (try sema.lookupInNamespace(block, namespace, name)) |lookup| { - assert(lookup.accessible); + assert(lookup.accessible == .public or lookup.accessible == .private_same_file); return lookup.nav; } namespace = zcu.namespacePtr(namespace).parent.unwrap() orelse break; @@ -5993,9 +5993,7 @@ fn lookupInNamespace( ident_name: InternPool.NullTerminatedString, ) CompileError!?struct { nav: InternPool.Nav.Index, - /// If `false`, the declaration is in a different file and is not `pub`. - /// We still return the declaration for better error reporting. - accessible: bool, + accessible: enum { public, private_same_file, private }, } { const pt = sema.pt; const zcu = pt.zcu; @@ -6025,12 +6023,12 @@ fn lookupInNamespace( if (namespace.pub_decls.getKeyAdapted(ident_name, adapter)) |nav_index| { return .{ .nav = nav_index, - .accessible = true, + .accessible = .public, }; } else if (namespace.priv_decls.getKeyAdapted(ident_name, adapter)) |nav_index| { return .{ .nav = nav_index, - .accessible = src_file == namespace.file_scope, + .accessible = if (src_file == namespace.file_scope) .private_same_file else .private, }; } @@ -12877,7 +12875,7 @@ fn zirHasDecl(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air const namespace = container_type.getNamespace(zcu).unwrap() orelse return .bool_false; if (try sema.lookupInNamespace(block, namespace, decl_name)) |lookup| { - if (lookup.accessible) { + if (lookup.accessible == .public) { return .bool_true; } } @@ -26925,7 +26923,7 @@ fn namespaceLookup( const zcu = pt.zcu; const gpa = sema.gpa; if (try sema.lookupInNamespace(block, namespace, decl_name)) |lookup| { - if (!lookup.accessible) { + if (lookup.accessible == .private) { return sema.failWithOwnedErrorMsg(block, msg: { const msg = try sema.errMsg(src, "'{f}' is not marked 'pub'", .{ decl_name.fmt(&zcu.intern_pool), diff --git a/test/behavior/hasdecl.zig b/test/behavior/hasdecl.zig index 71f9200b276e8bb02fdf5f53a7eb33c5c436e7d1..bba8f1f32f33ba09d9526d23609b65c34129fcbb 100644 --- a/test/behavior/hasdecl.zig +++ b/test/behavior/hasdecl.zig @@ -4,7 +4,7 @@ const expect = std.testing.expect; const Foo = @import("hasdecl/foo.zig"); -const Bar = struct { +pub const Bar = struct { nope: i32, const hi = 1; @@ -18,7 +18,7 @@ test "@hasDecl" { try expect(!@hasDecl(Foo, "private_thing")); try expect(!@hasDecl(Foo, "no_thing")); - try expect(@hasDecl(Bar, "hi")); + try expect(!@hasDecl(Bar, "hi")); try expect(@hasDecl(Bar, "blah")); try expect(!@hasDecl(Bar, "nope")); } @@ -26,10 +26,10 @@ test "@hasDecl" { test "@hasDecl using a sliced string literal" { if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest; - try expect(@hasDecl(@This(), "std") == true); - try expect(@hasDecl(@This(), "std"[0..0]) == false); - try expect(@hasDecl(@This(), "std"[0..1]) == false); - try expect(@hasDecl(@This(), "std"[0..2]) == false); - try expect(@hasDecl(@This(), "std"[0..3]) == true); - try expect(@hasDecl(@This(), "std"[0..]) == true); + try expect(@hasDecl(@This(), "Bar") == true); + try expect(@hasDecl(@This(), "Bar"[0..0]) == false); + try expect(@hasDecl(@This(), "Bar"[0..1]) == false); + try expect(@hasDecl(@This(), "Bar"[0..2]) == false); + try expect(@hasDecl(@This(), "Bar"[0..3]) == true); + try expect(@hasDecl(@This(), "Bar"[0..]) == true); } -- 2.54.0