lsp: find-references for fields, methods, and enum variants
Members aren't symbols, so their uses were never recorded. Adds a member-reference list (declaration + uses) tracked during analysis: struct fields/methods and enum variants as declarations; field access, method calls, bare enum literals, qualified Type.variant, and match-arm patterns as uses. Spans are derived from the source-relative name slices; uses carry the owner type (via inferExprType, dereferencing pointers). find-references matches by (owner, name) across loaded documents, treating an unknown owner as a wildcard. Verified: references for a field (legal_moves), a method (clear_valid_targets), and a variant (promote_rook — decl + comparisons + case patterns + struct-literal values across 5 files).
This commit is contained in:
@@ -339,6 +339,7 @@ pub const DocumentStore = struct {
|
||||
}
|
||||
|
||||
// Run sema on this file's own AST
|
||||
analyzer.source = doc.source;
|
||||
doc.sema = analyzer.analyze(root) catch null;
|
||||
if (doc.sema != null) {
|
||||
doc.last_good_sema = doc.sema;
|
||||
|
||||
@@ -203,6 +203,30 @@ pub const Server = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// A struct field / method / enum variant under the cursor — matched by
|
||||
// (owner type, name) across loaded documents.
|
||||
for (sema.member_refs) |mt| {
|
||||
if (offset < mt.span.start or offset >= mt.span.end) continue;
|
||||
var buf = std.ArrayList(u8).empty;
|
||||
defer buf.deinit(self.allocator);
|
||||
try buf.append(self.allocator, '[');
|
||||
var first = true;
|
||||
var dit = self.documents.by_path.iterator();
|
||||
while (dit.next()) |entry| {
|
||||
const odoc = entry.value_ptr.*;
|
||||
const osema = odoc.sema orelse continue;
|
||||
for (osema.member_refs) |mr| {
|
||||
if (!std.mem.eql(u8, mr.name, mt.name)) continue;
|
||||
// Owner must match, treating an unknown owner ("") as a wildcard.
|
||||
if (mr.owner.len != 0 and mt.owner.len != 0 and !std.mem.eql(u8, mr.owner, mt.owner)) continue;
|
||||
if (mr.is_def and !include_decl) continue;
|
||||
try self.appendRefLoc(&buf, &first, odoc, mr.span);
|
||||
}
|
||||
}
|
||||
try buf.append(self.allocator, ']');
|
||||
return try self.sendResponse(id_json, buf.items);
|
||||
}
|
||||
|
||||
// Resolve the target symbol: a reference at the cursor, or a definition.
|
||||
var target_idx: ?u32 = null;
|
||||
if (sx.sema.findReferenceAtOffset(sema.references, offset)) |ri| {
|
||||
|
||||
81
src/sema.zig
81
src/sema.zig
@@ -37,6 +37,15 @@ pub const Reference = struct {
|
||||
symbol_index: u32,
|
||||
};
|
||||
|
||||
/// A reference to a struct field / method / enum variant. These aren't symbols,
|
||||
/// so they're tracked separately and matched by (owner type, name).
|
||||
pub const MemberRef = struct {
|
||||
span: Span, // the member-name token
|
||||
name: []const u8,
|
||||
owner: []const u8 = "", // owning type name, "" if unknown (bare enum literal)
|
||||
is_def: bool = false, // declaration site rather than a use
|
||||
};
|
||||
|
||||
pub const FnSignature = struct {
|
||||
param_types: []const Type,
|
||||
return_type: Type,
|
||||
@@ -54,6 +63,7 @@ pub const TypeMap = std.AutoHashMap(*const Node, Type);
|
||||
pub const SemaResult = struct {
|
||||
symbols: []const Symbol,
|
||||
references: []const Reference,
|
||||
member_refs: []const MemberRef,
|
||||
diagnostics: []const Diagnostic,
|
||||
fn_signatures: std.StringHashMap(FnSignature),
|
||||
struct_types: std.StringHashMap(StructTypeInfo),
|
||||
@@ -66,6 +76,9 @@ pub const Analyzer = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
symbols: std.ArrayList(Symbol),
|
||||
references: std.ArrayList(Reference),
|
||||
member_refs: std.ArrayList(MemberRef),
|
||||
/// Source text — lets `spanOf` map an AST string slice back to a Span.
|
||||
source: []const u8 = "",
|
||||
diagnostics: std.ArrayList(Diagnostic),
|
||||
scope_depth: u32,
|
||||
/// Stack of symbol counts at each scope entry, for popScope cleanup.
|
||||
@@ -84,6 +97,7 @@ pub const Analyzer = struct {
|
||||
.allocator = allocator,
|
||||
.symbols = std.ArrayList(Symbol).empty,
|
||||
.references = std.ArrayList(Reference).empty,
|
||||
.member_refs = std.ArrayList(MemberRef).empty,
|
||||
.diagnostics = std.ArrayList(Diagnostic).empty,
|
||||
.scope_depth = 0,
|
||||
.scope_starts = std.ArrayList(u32).empty,
|
||||
@@ -112,6 +126,7 @@ pub const Analyzer = struct {
|
||||
return .{
|
||||
.symbols = try self.symbols.toOwnedSlice(self.allocator),
|
||||
.references = try self.references.toOwnedSlice(self.allocator),
|
||||
.member_refs = try self.member_refs.toOwnedSlice(self.allocator),
|
||||
.diagnostics = try self.diagnostics.toOwnedSlice(self.allocator),
|
||||
.fn_signatures = self.fn_signatures,
|
||||
.struct_types = self.struct_types,
|
||||
@@ -207,6 +222,7 @@ pub const Analyzer = struct {
|
||||
try self.addSymbol(ed.name, .enum_type, .{ .enum_type = ed.name }, node.span);
|
||||
try self.enum_types.put(ed.name, ed.variant_names);
|
||||
}
|
||||
for (ed.variant_names) |vn| self.recordMemberRef(vn, ed.name, true);
|
||||
},
|
||||
.struct_decl => |sd| {
|
||||
try self.addSymbol(sd.name, .struct_type, .{ .struct_type = sd.name }, node.span);
|
||||
@@ -256,9 +272,12 @@ pub const Analyzer = struct {
|
||||
.type_params = tp_slice,
|
||||
});
|
||||
}
|
||||
for (sd.field_names) |fname| self.recordMemberRef(fname, sd.name, true);
|
||||
for (sd.methods) |mnode| {
|
||||
if (mnode.data == .fn_decl)
|
||||
if (mnode.data == .fn_decl) {
|
||||
try self.registerMethodSig(mnode.data.fn_decl.name, ns_prefix, mnode.data.fn_decl.return_type);
|
||||
self.recordMemberRef(mnode.data.fn_decl.name, sd.name, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
.protocol_decl => |pd| {
|
||||
@@ -799,6 +818,23 @@ pub const Analyzer = struct {
|
||||
try gop.value_ptr.append(self.allocator, idx);
|
||||
}
|
||||
|
||||
/// Span of an AST string slice that points into `source` (field/variant/
|
||||
/// method names are such slices). Returns {0,0} for synthetic strings.
|
||||
fn spanOf(self: *Analyzer, s: []const u8) Span {
|
||||
if (self.source.len == 0 or s.len == 0) return .{ .start = 0, .end = 0 };
|
||||
const base = @intFromPtr(self.source.ptr);
|
||||
const p = @intFromPtr(s.ptr);
|
||||
if (p < base or p + s.len > base + self.source.len) return .{ .start = 0, .end = 0 };
|
||||
const start: u32 = @intCast(p - base);
|
||||
return .{ .start = start, .end = start + @as(u32, @intCast(s.len)) };
|
||||
}
|
||||
|
||||
fn recordMemberRef(self: *Analyzer, name: []const u8, owner: []const u8, is_def: bool) void {
|
||||
const span = self.spanOf(name);
|
||||
if (span.end == 0) return; // not locatable in source
|
||||
self.member_refs.append(self.allocator, .{ .span = span, .name = name, .owner = owner, .is_def = is_def }) catch {};
|
||||
}
|
||||
|
||||
fn resolveIdentifier(self: *Analyzer, name: []const u8, span: Span) !void {
|
||||
// Use symbol index for O(1) name lookup, then walk backwards through indices
|
||||
if (self.symbol_index.get(name)) |indices| {
|
||||
@@ -914,6 +950,12 @@ pub const Analyzer = struct {
|
||||
},
|
||||
.field_access => |fa| {
|
||||
try self.analyzeNode(fa.object);
|
||||
var owner_ty = self.inferExprType(fa.object);
|
||||
if (owner_ty.isPointer()) owner_ty = self.resolveTypeNameStr(owner_ty.pointer_type.pointee_name);
|
||||
self.recordMemberRef(fa.field, owner_ty.toName() orelse "", false);
|
||||
},
|
||||
.enum_literal => |el| {
|
||||
self.recordMemberRef(el.name, "", false);
|
||||
},
|
||||
.if_expr => |ie| {
|
||||
try self.analyzeNode(ie.condition);
|
||||
@@ -937,7 +979,13 @@ pub const Analyzer = struct {
|
||||
},
|
||||
.match_expr => |me| {
|
||||
try self.analyzeNode(me.subject);
|
||||
var subj_ty = self.inferExprType(me.subject);
|
||||
if (subj_ty.isPointer()) subj_ty = self.resolveTypeNameStr(subj_ty.pointer_type.pointee_name);
|
||||
const subj_owner = subj_ty.toName() orelse "";
|
||||
for (me.arms) |arm| {
|
||||
if (arm.pattern) |pat| {
|
||||
if (pat.data == .enum_literal) self.recordMemberRef(pat.data.enum_literal.name, subj_owner, false);
|
||||
}
|
||||
try self.pushScope();
|
||||
if (arm.capture) |cap_name| {
|
||||
try self.addSymbol(cap_name, .variable, null, arm.body.span);
|
||||
@@ -1023,7 +1071,6 @@ pub const Analyzer = struct {
|
||||
try self.addSymbol(ud.name, .enum_type, .{ .union_type = ud.name }, node.span);
|
||||
},
|
||||
// Leaf nodes — nothing to recurse into
|
||||
.enum_literal,
|
||||
.int_literal,
|
||||
.float_literal,
|
||||
.bool_literal,
|
||||
@@ -1954,6 +2001,36 @@ test "sema: field access + index through a *Struct param" {
|
||||
try std.testing.expectEqualStrings("Cell", c_ty.?.struct_type);
|
||||
}
|
||||
|
||||
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);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
const source =
|
||||
"Color :: enum { red; green; }" ++
|
||||
"P :: struct { x: s64; m :: (self: P) -> s64 { self.x; } }" ++
|
||||
"use :: (p: *P) { a := p.x; b := p.m(); c := Color.red; }";
|
||||
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 x_use = false;
|
||||
var m_use = false;
|
||||
var red_use = false;
|
||||
for (res.member_refs) |mr| {
|
||||
if (mr.is_def) continue;
|
||||
if (std.mem.eql(u8, mr.name, "x") and std.mem.eql(u8, mr.owner, "P")) x_use = true;
|
||||
if (std.mem.eql(u8, mr.name, "m") and std.mem.eql(u8, mr.owner, "P")) m_use = true;
|
||||
if (std.mem.eql(u8, mr.name, "red") and std.mem.eql(u8, mr.owner, "Color")) red_use = true;
|
||||
}
|
||||
try std.testing.expect(x_use);
|
||||
try std.testing.expect(m_use);
|
||||
try std.testing.expect(red_use);
|
||||
}
|
||||
|
||||
test "sema: variable shadowing in same scope is allowed" {
|
||||
const parser_mod = @import("parser.zig");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user