diff --git a/schema.json b/schema.json index a00cec91d..4ad59fb65 100644 --- a/schema.json +++ b/schema.json @@ -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" }, diff --git a/src/Config.zig b/src/Config.zig index 5767a1091..2d651c4cc 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -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, diff --git a/src/Server.zig b/src/Server.zig index f404253e8..844712047 100644 --- a/src/Server.zig +++ b/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 and tree.errors.len == 0) 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 ast-check process, error: {}", .{err}); + break :diag; }; - - var decl_iterator = scope.decls.iterator(); - while (decl_iterator.next()) |decl| { - var identifier_count: usize = 0; - - 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, + 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, + }; + + 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 => {}, } } } diff --git a/src/requests.zig b/src/requests.zig index d96496ed9..9e8d71ba7 100644 --- a/src/requests.zig +++ b/src/requests.zig @@ -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, diff --git a/src/setup.zig b/src/setup.zig index 8d124f88d..40b058efe 100644 --- a/src/setup.zig +++ b/src/setup.zig @@ -170,7 +170,7 @@ pub fn wizard(allocator: std.mem.Allocator) !void { const editor = try askSelectOne("Which code editor do you use?", enum { VSCode, Sublime, Kate, Neovim, Vim8, Emacs, Doom, Spacemacs, Helix, Other }); const snippets = try askBool("Do you want to enable snippets?"); - const unused_variables = try askBool("Do you want to enable unused variable warnings?"); + const ast_check = try askBool("Do you want to enable ast-check diagnostics?"); const ief_apc = try askBool("Do you want to enable @import/@embedFile argument path completion?"); const style = try askBool("Do you want to enable style warnings?"); const semantic_tokens = try askBool("Do you want to enable semantic highlighting?"); @@ -192,7 +192,7 @@ pub fn wizard(allocator: std.mem.Allocator) !void { .@"$schema" = "https://raw.githubusercontent.com/zigtools/zls/master/schema.json", .zig_exe_path = zig_exe_path, .enable_snippets = snippets, - .enable_unused_variable_warnings = unused_variables, + .enable_ast_check_diagnostics = ast_check, .enable_import_embedfile_argument_completions = ief_apc, .warn_style = style, .enable_semantic_tokens = semantic_tokens, diff --git a/src/types.zig b/src/types.zig index 659a77b1b..f6500a285 100644 --- a/src/types.zig +++ b/src/types.zig @@ -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 = &.{}, }; pub const TextDocument = struct {