lsp/sema: diagnose passing *T where a T value is expected

Bring the lower.zig call-argument check to the LSP analyzer so the
`*T`-where-`T` mismatch (a `for xs: (*m)` capture or a `*T` parameter
forwarded into a by-value parameter) is reported inline as you type,
not only at build time.

The fn-signature registry resolved parameter types with the shallow
Type.fromTypeExpr, which yields 'unresolved' for user structs, so the
argument type never matched the parameter. Resolve params through the
registry-aware fieldType instead (as the param symbols already do).
Restricted to direct identifier calls so args align 1:1 with params.

Add a regression test.
This commit is contained in:
agra
2026-05-31 14:37:11 +03:00
parent 14d1d9d3a8
commit b497b74acb

View File

@@ -162,7 +162,7 @@ pub const Analyzer = struct {
var param_types = std.ArrayList(Type).empty;
var has_variadic = false;
for (fd.params) |param| {
const pt = Type.fromTypeExpr(param.type_expr) orelse Type.unresolved;
const pt = self.fieldType(param.type_expr);
if (param.is_variadic) {
has_variadic = true;
// Variadic param becomes a slice type
@@ -200,7 +200,7 @@ pub const Analyzer = struct {
const lam = cd.value.data.lambda;
var param_types = std.ArrayList(Type).empty;
for (lam.params) |param| {
const pt = Type.fromTypeExpr(param.type_expr) orelse Type.unresolved;
const pt = self.fieldType(param.type_expr);
try param_types.append(self.allocator, pt);
}
const ret = if (lam.return_type) |rt|
@@ -924,7 +924,7 @@ pub const Analyzer = struct {
{
var param_types = std.ArrayList(Type).empty;
for (fd.params) |param| {
const pt = Type.fromTypeExpr(param.type_expr) orelse Type.unresolved;
const pt = self.fieldType(param.type_expr);
try param_types.append(self.allocator, pt);
}
try self.fn_signatures.put(fd.name, .{
@@ -992,6 +992,31 @@ pub const Analyzer = struct {
for (call.args) |arg| {
try self.analyzeNode(arg);
}
// Mirror lower.zig: passing a `*T` where a `T` value is expected
// (a `for xs: (*m)` capture, a `*T` parameter, any pointer local).
// Restricted to direct (identifier) calls so args align 1:1 with
// the declared params — UFCS/method calls drop the receiver.
if (call.callee.data == .identifier) {
if (self.resolveCalleeName(call)) |callee_name| {
if (self.fn_signatures.get(callee_name)) |sig| {
const n = @min(call.args.len, sig.param_types.len);
var i: usize = 0;
while (i < n) : (i += 1) {
const pt = sig.param_types[i];
if (pt.isPointer()) continue;
const pt_name = pt.toName() orelse continue;
const at = self.inferExprType(call.args[i]);
if (!at.isPointer()) continue;
if (!std.mem.eql(u8, at.pointer_type.pointee_name, pt_name)) continue;
const msg = if (call.args[i].data == .identifier)
std.fmt.allocPrint(self.allocator, "argument '{s}' has type '*{s}', but '{s}' is expected here; dereference it with `{s}.*`", .{ call.args[i].data.identifier.name, pt_name, pt_name, call.args[i].data.identifier.name }) catch continue
else
std.fmt.allocPrint(self.allocator, "argument has type '*{s}', but '{s}' is expected here; dereference it with `.*`", .{ pt_name, pt_name }) catch continue;
try self.diagnostics.append(self.allocator, .{ .level = .err, .span = call.args[i].span, .message = msg });
}
}
}
}
},
.ffi_intrinsic_call => |fic| {
try self.analyzeNode(fic.return_type);
@@ -1299,7 +1324,7 @@ pub const Analyzer = struct {
fn inferFnReturnType(self: *Analyzer, params: []const ast.Param, body: *const Node) ?Type {
self.pushScope() catch return null;
for (params) |param| {
const pt = Type.fromTypeExpr(param.type_expr) orelse Type.unresolved;
const pt = self.fieldType(param.type_expr);
self.addSymbol(param.name, .param, pt, param.name_span) catch {};
}
// Arrow fn_decl wraps body in block{[expr]} — unwrap to inner expression
@@ -2121,6 +2146,31 @@ fn offsetIn(source: []const u8, span: ast.Span, needle: []const u8) bool {
return span.start >= at and span.start < at + needle.len;
}
test "sema: passing *T where a T value is expected is diagnosed" {
const parser_mod = @import("parser.zig");
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const source =
"Move :: struct { flag: s64; }" ++
"take :: (m: Move) -> s64 { m.flag; }" ++
"take_ptr :: (m: *Move) -> s64 { m.flag; }" ++
"bad :: (p: *Move) -> s64 { take(p); }" ++ // *Move into a Move param → flagged
"good :: (p: *Move) -> s64 { take_ptr(p); }"; // *Move into a *Move param → fine
var parser = parser_mod.Parser.init(alloc, source);
const root = try parser.parse();
var an = Analyzer.init(alloc);
an.source = source;
const res = try an.analyze(root);
var mismatch_count: usize = 0;
for (res.diagnostics) |d| {
if (std.mem.indexOf(u8, d.message, "expected here") != null) mismatch_count += 1;
}
try std.testing.expectEqual(@as(usize, 1), mismatch_count);
}
test "sema: member references record fields, methods, and enum variants" {
const parser_mod = @import("parser.zig");
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);