zls/tests/lsp_features/references.zig
2022-09-18 22:47:06 +00:00

190 lines
6.2 KiB
Zig

const std = @import("std");
const zls = @import("zls");
const helper = @import("../helper.zig");
const Context = @import("../context.zig").Context;
const ErrorBuilder = @import("../ErrorBuilder.zig");
const types = zls.types;
const requests = zls.requests;
const offsets = zls.offsets;
const allocator: std.mem.Allocator = std.testing.allocator;
// TODO fix references so that we can stop skipping these tests
const skip_references_tests = true;
test "references" {
if (skip_references_tests) return error.SkipZigTest;
try testReferences(
\\const <0> = 0;
\\const foo = <0>;
);
try testReferences(
\\var <0> = 0;
\\var foo = <0>;
);
try testReferences(
\\const <0> = struct {};
\\var foo: <0> = <0>{};
);
try testReferences(
\\const <0> = enum {};
\\var foo: <0> = undefined;
);
try testReferences(
\\const <0> = union {};
\\var foo: <0> = <0>{};
);
try testReferences(
\\fn <0>() void {}
\\var foo = <0>();
);
try testReferences(
\\const <0> = error{};
\\fn bar() <0>!void {}
);
}
test "references - global scope" {
if (skip_references_tests) return error.SkipZigTest;
try testReferences(
\\const foo = <0>;
\\const <0> = 0;
\\const bar = <0>;
);
}
test "references - local scope" {
try testReferences(
\\fn foo(<0>: u32, bar: u32) void {
\\ return <0> + bar;
\\}
);
if (skip_references_tests) return error.SkipZigTest;
try testReferences(
\\const foo = blk: {
\\ _ = blk: {
\\ const <0> = 0;
\\ break :blk <0>;
\\ };
\\ const <1> = 0;
\\ break :blk <1>;
\\};
\\const bar = foo;
);
}
test "references - label" {
if (skip_references_tests) return error.SkipZigTest;
try testReferences(
\\const foo = <0>: {
\\ break :<0> 0;
\\};
);
}
fn testReferences(source: []const u8) !void {
const file_uri = "file:///test.zig";
const new_name = "placeholder";
var phr = try helper.collectReplacePlaceholders(allocator, source, new_name);
defer phr.deinit(allocator);
var ctx = try Context.init();
defer ctx.deinit();
try ctx.requestDidOpen(file_uri, phr.new_source);
var i: usize = 0;
while (i < phr.locations.len) : (i += 1) {
const var_loc = phr.locations.items(.old)[i];
const var_name = offsets.locToSlice(source, var_loc);
const var_loc_middle = var_loc.start + (var_loc.end - var_loc.start) / 2;
const request = requests.References{
.params = .{
.textDocument = .{ .uri = file_uri },
.position = offsets.indexToPosition(source, var_loc_middle, ctx.server.offset_encoding),
.context = .{ .includeDeclaration = true },
},
};
const response = try ctx.requestGetResponse(?[]types.Location, "textDocument/references", request);
defer response.deinit();
const locations: []types.Location = response.result orelse {
std.debug.print("Server returned `null` as the result\n", .{});
return error.InvalidResponse;
};
for (locations) |response_location| {
const actual_name = offsets.rangeToSlice(phr.new_source, response_location.range, ctx.server.offset_encoding);
try std.testing.expectEqualStrings(file_uri, response_location.uri);
try std.testing.expectEqualStrings(new_name, actual_name);
}
// collect all new placeholder locations with the given name
const expected_locs: []offsets.Loc = blk: {
var locs = std.ArrayListUnmanaged(offsets.Loc){};
errdefer locs.deinit(allocator);
var j: usize = 0;
while (j < phr.locations.len) : (j += 1) {
const old_loc = phr.locations.items(.old)[j];
const new_loc = phr.locations.items(.new)[j];
const old_loc_name = offsets.locToSlice(source, old_loc);
if (!std.mem.eql(u8, var_name, old_loc_name)) continue;
try locs.append(allocator, new_loc);
}
break :blk locs.toOwnedSlice(allocator);
};
defer allocator.free(expected_locs);
var error_builder = ErrorBuilder.init(allocator, phr.new_source);
defer error_builder.deinit();
errdefer {
const note_loc = phr.locations.items(.new)[i];
error_builder.msgAtLoc("asked for references here", note_loc, .info, .{}) catch {};
error_builder.writeDebug();
}
// keeps track of expected locations that have been given by the server
// used to detect double references and missing references
var visited = try std.DynamicBitSetUnmanaged.initEmpty(allocator, expected_locs.len);
defer visited.deinit(allocator);
for (locations) |response_location| {
const actual_loc = offsets.rangeToLoc(phr.new_source, response_location.range, ctx.server.offset_encoding);
const index = found_index: {
for (expected_locs) |expected_loc, idx| {
if (expected_loc.start != actual_loc.start) continue;
if (expected_loc.end != actual_loc.end) continue;
break :found_index idx;
}
try error_builder.msgAtLoc("server returned unexpected reference!", actual_loc, .err, .{});
return error.UnexpectedReference;
};
if (visited.isSet(index)) {
try error_builder.msgAtLoc("server returned duplicate reference!", actual_loc, .err, .{});
return error.DuplicateReference;
} else {
visited.set(index);
}
}
var has_unvisited = false;
var unvisited_it = visited.iterator(.{ .kind = .unset });
while (unvisited_it.next()) |index| {
try error_builder.msgAtLoc("expected reference here!", expected_locs[index], .err, .{});
has_unvisited = true;
}
if (has_unvisited) return error.ExpectedReference;
}
}