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:
58
src/sema.zig
58
src/sema.zig
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user