ast-check for zls!

This commit is contained in:
Auguste Rame 2022-08-31 20:48:42 -04:00
parent da03c81992
commit 57a35a7bc8
No known key found for this signature in database
GPG Key ID: 3A5E3F90DF2AAEFE
5 changed files with 83 additions and 67 deletions

View File

@ -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"
},

View File

@ -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,

View File

@ -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();
var decl_iterator = scope.decls.iterator();
while (decl_iterator.next()) |decl| {
var identifier_count: usize = 0;
process.stdin = null;
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,
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,
};
const msg = pos_and_diag_iterator.rest()[1..];
if (std.mem.startsWith(u8, msg, "error: ")) {
try diagnostics.append(allocator, .{
.range = .{ .start = pos, .end = pos },
.severity = .Error,
.code = "ast_check",
.source = "zls",
.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),
});
}
},
.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)
try diagnostics.append(allocator, .{
.range = astLocationToRange(tree.tokenLocation(0, name_token_index)),
.severity = .Error,
.code = "unused_variable",
.source = "zls",
.message = "Unused variable; either remove the variable or use '_ = ' on the variable to bypass this error",
});
}
},
else => {},
}
}
}

View File

@ -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,

View File

@ -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 {