ast-check for zls!
This commit is contained in:
parent
da03c81992
commit
57a35a7bc8
@ -9,8 +9,8 @@
|
||||
"type": "boolean",
|
||||
"default": "false"
|
||||
},
|
||||
"enable_unused_variable_warnings": {
|
||||
"description": "Enables warnings for local variables that aren't used",
|
||||
"enable_ast_check_diagnostics": {
|
||||
"description": "Whether to enable ast-check diagnostics",
|
||||
"type": "boolean",
|
||||
"default": "false"
|
||||
},
|
||||
|
@ -13,8 +13,8 @@ const logger = std.log.scoped(.config);
|
||||
/// Whether to enable snippet completions
|
||||
enable_snippets: bool = false,
|
||||
|
||||
/// Whether to enable unused variable warnings
|
||||
enable_unused_variable_warnings: bool = false,
|
||||
/// Whether to enable ast-check diagnostics
|
||||
enable_ast_check_diagnostics: bool = false,
|
||||
|
||||
/// Whether to enable import/embedFile argument completions (NOTE: these are triggered manually as updating the autotrigger characters may cause issues)
|
||||
enable_import_embedfile_argument_completions: bool = false,
|
||||
|
128
src/Server.zig
128
src/Server.zig
@ -237,72 +237,82 @@ fn publishDiagnostics(server: *Server, writer: anytype, handle: DocumentStore.Ha
|
||||
});
|
||||
}
|
||||
|
||||
if (server.config.enable_unused_variable_warnings) {
|
||||
scopes: for (handle.document_scope.scopes.items) |scope| {
|
||||
const scope_data = switch (scope.data) {
|
||||
.function => |f| b: {
|
||||
if (!ast.fnProtoHasBody(tree, f).?) continue :scopes;
|
||||
break :b f;
|
||||
},
|
||||
.block => |b| b,
|
||||
else => continue,
|
||||
if (server.config.enable_ast_check_diagnostics) diag: {
|
||||
if (server.config.zig_exe_path) |zig_exe_path| {
|
||||
var process = std.ChildProcess.init(&[_][]const u8{ zig_exe_path, "ast-check", "--color", "off" }, server.allocator);
|
||||
process.stdin_behavior = .Pipe;
|
||||
process.stderr_behavior = .Pipe;
|
||||
|
||||
process.spawn() catch |err| {
|
||||
Logger.warn(server, writer, "Failed to spawn zig fmt process, error: {}", .{err});
|
||||
break :diag;
|
||||
};
|
||||
try process.stdin.?.writeAll(handle.document.text);
|
||||
process.stdin.?.close();
|
||||
|
||||
process.stdin = null;
|
||||
|
||||
const stderr_bytes = try process.stderr.?.reader().readAllAlloc(server.allocator, std.math.maxInt(usize));
|
||||
defer server.allocator.free(stderr_bytes);
|
||||
|
||||
switch (try process.wait()) {
|
||||
.Exited => {
|
||||
// NOTE: I believe that with color off it's one diag per line; is this correct?
|
||||
var line_iterator = std.mem.split(u8, stderr_bytes, "\n");
|
||||
|
||||
while (line_iterator.next()) |line| lin: {
|
||||
var pos_and_diag_iterator = std.mem.split(u8, line, ":");
|
||||
const maybe_first = pos_and_diag_iterator.next();
|
||||
if (maybe_first) |first| {
|
||||
if (first.len <= 1) break :lin;
|
||||
} else break;
|
||||
|
||||
const pos = types.Position{
|
||||
.line = (try std.fmt.parseInt(i64, pos_and_diag_iterator.next().?, 10)) - 1,
|
||||
.character = (try std.fmt.parseInt(i64, pos_and_diag_iterator.next().?, 10)) - 1,
|
||||
};
|
||||
|
||||
var decl_iterator = scope.decls.iterator();
|
||||
while (decl_iterator.next()) |decl| {
|
||||
var identifier_count: usize = 0;
|
||||
const msg = pos_and_diag_iterator.rest()[1..];
|
||||
|
||||
const name_token_index = switch (decl.value_ptr.*) {
|
||||
.ast_node => |an| s: {
|
||||
const an_tag = tree.nodes.items(.tag)[an];
|
||||
switch (an_tag) {
|
||||
.simple_var_decl => {
|
||||
break :s tree.nodes.items(.main_token)[an] + 1;
|
||||
},
|
||||
else => continue,
|
||||
}
|
||||
},
|
||||
.param_decl => |param| param.name_token orelse continue,
|
||||
else => continue,
|
||||
};
|
||||
|
||||
if (std.mem.eql(u8, tree.tokenSlice(name_token_index), "_"))
|
||||
continue;
|
||||
|
||||
const pit_start = tree.firstToken(scope_data);
|
||||
const pit_end = ast.lastToken(tree, scope_data);
|
||||
|
||||
const tags = tree.tokens.items(.tag)[pit_start..pit_end];
|
||||
for (tags) |tag, index| {
|
||||
if (tag != .identifier) continue;
|
||||
if (!std.mem.eql(u8, tree.tokenSlice(pit_start + @intCast(u32, index)), tree.tokenSlice(name_token_index))) continue;
|
||||
if (index -| 1 > 0 and tags[index - 1] == .period) continue;
|
||||
if (index +| 2 < tags.len and tags[index + 1] == .colon) switch (tags[index + 2]) {
|
||||
.l_brace,
|
||||
.keyword_inline,
|
||||
.keyword_while,
|
||||
.keyword_for,
|
||||
.keyword_switch,
|
||||
=> continue,
|
||||
else => {},
|
||||
};
|
||||
if (index -| 2 > 0 and tags[index - 1] == .colon) switch (tags[index - 2]) {
|
||||
.keyword_break,
|
||||
.keyword_continue,
|
||||
=> continue,
|
||||
else => {},
|
||||
};
|
||||
identifier_count += 1;
|
||||
}
|
||||
|
||||
if (identifier_count <= 1)
|
||||
if (std.mem.startsWith(u8, msg, "error: ")) {
|
||||
try diagnostics.append(allocator, .{
|
||||
.range = astLocationToRange(tree.tokenLocation(0, name_token_index)),
|
||||
.range = .{ .start = pos, .end = pos },
|
||||
.severity = .Error,
|
||||
.code = "unused_variable",
|
||||
.code = "ast_check",
|
||||
.source = "zls",
|
||||
.message = "Unused variable; either remove the variable or use '_ = ' on the variable to bypass this error",
|
||||
.message = try server.arena.allocator().dupe(u8, msg["error: ".len..]),
|
||||
});
|
||||
} else if (std.mem.startsWith(u8, msg, "note: ")) {
|
||||
var latestDiag = &diagnostics.items[diagnostics.items.len - 1];
|
||||
|
||||
var fresh = if (latestDiag.relatedInformation.len == 0)
|
||||
try server.arena.allocator().alloc(types.DiagnosticRelatedInformation, 1)
|
||||
else
|
||||
try server.arena.allocator().realloc(@ptrCast([]types.DiagnosticRelatedInformation, latestDiag.relatedInformation), latestDiag.relatedInformation.len + 1);
|
||||
|
||||
const location = types.Location{
|
||||
.uri = handle.uri(),
|
||||
.range = .{ .start = pos, .end = pos },
|
||||
};
|
||||
|
||||
fresh[fresh.len - 1] = .{
|
||||
.location = location,
|
||||
.message = try server.arena.allocator().dupe(u8, msg["note: ".len..]),
|
||||
};
|
||||
|
||||
latestDiag.relatedInformation = fresh;
|
||||
} else {
|
||||
try diagnostics.append(allocator, .{
|
||||
.range = .{ .start = pos, .end = pos },
|
||||
.severity = .Error,
|
||||
.code = "ast_check",
|
||||
.source = "zls",
|
||||
.message = try server.arena.allocator().dupe(u8, msg),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -276,7 +276,7 @@ pub const Configuration = struct {
|
||||
params: struct {
|
||||
settings: struct {
|
||||
enable_snippets: ?bool,
|
||||
enable_unused_variable_warnings: ?bool,
|
||||
enable_ast_check_diagnostics: ?bool,
|
||||
enable_import_embedfile_argument_completions: ?bool,
|
||||
zig_lib_path: ?[]const u8,
|
||||
zig_exe_path: ?[]const u8,
|
||||
|
@ -108,12 +108,18 @@ pub const DiagnosticSeverity = enum(i64) {
|
||||
}
|
||||
};
|
||||
|
||||
pub const DiagnosticRelatedInformation = struct {
|
||||
location: Location,
|
||||
message: string,
|
||||
};
|
||||
|
||||
pub const Diagnostic = struct {
|
||||
range: Range,
|
||||
severity: DiagnosticSeverity,
|
||||
code: string,
|
||||
source: string,
|
||||
message: string,
|
||||
relatedInformation: []const DiagnosticRelatedInformation = &[0]DiagnosticRelatedInformation{},
|
||||
};
|
||||
|
||||
pub const TextDocument = struct {
|
||||
|
Loading…
Reference in New Issue
Block a user