Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect declarations deprecated with a doc comment #2180

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 44 additions & 9 deletions src/analysis.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,15 @@ const FindBreaks = struct {
}
};

fn isDeprecatedWithDoc(tree: Ast, base_token: Ast.TokenIndex) bool {
const token_tags = tree.tokens.items(.tag);
const doc_index = getDocCommentTokenIndex(token_tags, base_token) orelse return false;
const doc_slice = tree.tokenSlice(doc_index);
if (doc_slice.len < "///Deprecated".len) return false;

return std.ascii.startsWithIgnoreCase(doc_slice[@as(usize, 3) + @intFromBool(doc_slice[3] == ' ') ..], "deprecated");
}

/// Resolves the type of an Ast Node.
/// Returns `null` if the type could not be resolved.
pub fn resolveTypeOfNode(analyser: *Analyser, node_handle: NodeWithHandle) error{OutOfMemory}!?Type {
Expand Down Expand Up @@ -1476,10 +1485,12 @@ fn resolveTypeOfNodeUncached(analyser: *Analyser, node_handle: NodeWithHandle) e
=> {
const var_decl = tree.fullVarDecl(node).?;
var fallback_type: ?Type = null;
const deprecated_comment = isDeprecatedWithDoc(tree, var_decl.firstToken());

if (var_decl.ast.type_node != 0) blk: {
const type_node: NodeWithHandle = .{ .node = var_decl.ast.type_node, .handle = handle };
const decl_type = try analyser.resolveTypeOfNodeInternal(type_node) orelse break :blk;
var decl_type = try analyser.resolveTypeOfNodeInternal(type_node) orelse break :blk;
if (deprecated_comment) decl_type.deprecated_comment = true;
if (decl_type.isMetaType()) {
fallback_type = decl_type;
break :blk;
Expand All @@ -1489,7 +1500,9 @@ fn resolveTypeOfNodeUncached(analyser: *Analyser, node_handle: NodeWithHandle) e

if (var_decl.ast.init_node != 0) blk: {
const value: NodeWithHandle = .{ .node = var_decl.ast.init_node, .handle = handle };
return try analyser.resolveTypeOfNodeInternal(value) orelse break :blk;
var value_type = try analyser.resolveTypeOfNodeInternal(value) orelse break :blk;
if (deprecated_comment) value_type.deprecated_comment = true;
return value_type;
}

return fallback_type;
Expand Down Expand Up @@ -1568,19 +1581,27 @@ fn resolveTypeOfNodeUncached(analyser: *Analyser, node_handle: NodeWithHandle) e
.container_field_init,
.container_field_align,
=> {
const container_type = try innermostContainer(handle, offsets.tokenToIndex(tree, tree.firstToken(node)));
const first_tok = tree.firstToken(node);
const deprecated_comment = isDeprecatedWithDoc(tree, first_tok);

var container_type = try innermostContainer(handle, offsets.tokenToIndex(tree, first_tok));
if (deprecated_comment) container_type.deprecated_comment = true;
if (container_type.isEnumType())
return try container_type.instanceTypeVal(analyser);

if (container_type.isTaggedUnion()) {
var field = tree.fullContainerField(node).?;
field.convertToNonTupleLike(tree.nodes);
if (field.ast.type_expr == 0)
return try Type.typeValFromIP(analyser, .void_type);
if (field.ast.type_expr == 0) {
var void_type = try Type.typeValFromIP(analyser, .void_type);
if (deprecated_comment) void_type.deprecated_comment = true;
return void_type;
}
}

const base: NodeWithHandle = .{ .node = datas[node].lhs, .handle = handle };
const base_type = (try analyser.resolveTypeOfNodeInternal(base)) orelse return null;
var base_type = (try analyser.resolveTypeOfNodeInternal(base)) orelse return null;
if (deprecated_comment) base_type.deprecated_comment = true;
return try base_type.instanceTypeVal(analyser);
},
.@"comptime",
Expand Down Expand Up @@ -1954,12 +1975,17 @@ fn resolveTypeOfNodeUncached(analyser: *Analyser, node_handle: NodeWithHandle) e
.fn_decl,
=> {
var buf: [1]Ast.Node.Index = undefined;
const fn_proto = tree.fullFnProto(&buf, node).?;
// This is a function type
if (tree.fullFnProto(&buf, node).?.name_token == null) {
if (fn_proto.name_token == null) {
return Type.typeVal(node_handle);
}

return .{ .data = .{ .other = .{ .node = node, .handle = handle } }, .is_type_val = false };
return .{
.data = .{ .other = .{ .node = node, .handle = handle } },
.is_type_val = false,
.deprecated_comment = isDeprecatedWithDoc(tree, fn_proto.firstToken()),
};
},
.@"if", .if_simple => {
const if_node = ast.fullIf(tree, node).?;
Expand Down Expand Up @@ -2372,6 +2398,8 @@ pub const Type = struct {
/// ```
/// if `data == .ip_index` then this field is equivalent to `typeOf(index) == .type_type`
is_type_val: bool,
/// True if the type belongs to a declaration with a doc comment starting with "deprecated".
deprecated_comment: bool = false,

pub const Data = union(enum) {
/// - `*const T`
Expand Down Expand Up @@ -2453,6 +2481,7 @@ pub const Type = struct {

pub fn hashWithHasher(self: Type, hasher: anytype) void {
hasher.update(&.{ @intFromBool(self.is_type_val), @intFromEnum(self.data) });
hasher.update(&.{ @intFromBool(self.deprecated_comment), @intFromEnum(self.data) });

switch (self.data) {
.pointer => |info| {
Expand Down Expand Up @@ -2497,6 +2526,7 @@ pub const Type = struct {

pub fn eql(a: Type, b: Type) bool {
if (a.is_type_val != b.is_type_val) return false;
if (a.deprecated_comment != b.deprecated_comment) return false;
if (@intFromEnum(a.data) != @intFromEnum(b.data)) return false;

switch (a.data) {
Expand Down Expand Up @@ -2660,9 +2690,14 @@ pub const Type = struct {
},
},
.is_type_val = payload.index == .type_type,
.deprecated_comment = self.deprecated_comment,
};
},
else => .{ .data = self.data, .is_type_val = false },
else => .{
.data = self.data,
.is_type_val = false,
.deprecated_comment = self.deprecated_comment,
},
};
}

Expand Down
7 changes: 5 additions & 2 deletions src/features/completions.zig
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ fn declToCompletion(builder: *Builder, decl_handle: Analyser.DeclWithHandle, opt

var is_deprecated: bool = false;
if (maybe_resolved_ty) |ty| {
if (ty.deprecated_comment) is_deprecated = true;
if (try builder.analyser.resolveFuncProtoOfCallable(ty)) |func_ty| blk: {
var item = try functionTypeCompletion(builder, name, options.parent_container_ty, func_ty) orelse break :blk;
item.documentation = documentation;
Expand Down Expand Up @@ -274,8 +275,8 @@ fn declToCompletion(builder: *Builder, decl_handle: Analyser.DeclWithHandle, opt
} else documentation,
.detail = detail,
.labelDetails = label_details,
.deprecated = if (compile_error_message != null and builder.server.client_capabilities.supports_completion_deprecated_old) true else null,
.tags = if (compile_error_message != null and builder.server.client_capabilities.supports_completion_deprecated_tag) &.{.Deprecated} else null,
.deprecated = if (is_deprecated and builder.server.client_capabilities.supports_completion_deprecated_old) true else null,
.tags = if (is_deprecated and builder.server.client_capabilities.supports_completion_deprecated_tag) &.{.Deprecated} else null,
});
},
.label => {
Expand Down Expand Up @@ -433,6 +434,8 @@ fn functionTypeCompletion(
.{ .InsertReplaceEdit = .{ .newText = new_text, .insert = insert_range, .replace = replace_range } }
else
.{ .TextEdit = .{ .newText = new_text, .range = insert_range } },
.deprecated = if (func_ty.deprecated_comment and builder.server.client_capabilities.supports_completion_deprecated_old) true else null,
.tags = if (func_ty.deprecated_comment and builder.server.client_capabilities.supports_completion_deprecated_tag) &.{.Deprecated} else null,
};
}

Expand Down
10 changes: 6 additions & 4 deletions src/features/semantic_tokens.zig
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ fn colorIdentifierBasedOnType(
is_parameter: bool,
tok_mod: TokenModifiers,
) !void {
var new_tok_mod = tok_mod;
if (type_node.deprecated_comment) {
new_tok_mod.deprecated = true;
}
if (type_node.is_type_val) {
const token_type: TokenType =
if (type_node.isNamespace())
Expand All @@ -241,11 +245,10 @@ fn colorIdentifierBasedOnType(
else
.type;

try writeTokenMod(builder, target_tok, token_type, tok_mod);
try writeTokenMod(builder, target_tok, token_type, new_tok_mod);
} else if (type_node.isTypeFunc()) {
try writeTokenMod(builder, target_tok, .type, tok_mod);
try writeTokenMod(builder, target_tok, .type, new_tok_mod);
} else if (type_node.isFunc()) {
var new_tok_mod = tok_mod;
if (type_node.isGenericFunc()) {
new_tok_mod.generic = true;
}
Expand All @@ -254,7 +257,6 @@ fn colorIdentifierBasedOnType(

try writeTokenMod(builder, target_tok, if (has_self_param) .method else .function, new_tok_mod);
} else {
var new_tok_mod = tok_mod;
if (type_node.data == .compile_error) {
new_tok_mod.deprecated = true;
}
Expand Down
62 changes: 62 additions & 0 deletions tests/lsp_features/completion.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2347,6 +2347,68 @@ test "deprecated " {
});
}

test "deprecated with doc comment " {
try testCompletion(
\\/// Deprecated; some message
\\const foo = 123;
\\const bar = <cursor>
, &.{
.{
.label = "foo",
.kind = .Constant,
.documentation = " Deprecated; some message",
.deprecated = true,
},
});
try testCompletion(
\\///Deprecated
\\fn bar() void {}
\\const baz = <cursor>
, &.{
.{
.label = "bar",
.kind = .Function,
.documentation = "Deprecated",
.deprecated = true,
},
});
try testCompletion(
\\fn bar() void {}
\\///Deprecated, use bar
\\const foo = bar;
\\const baz = <cursor>
, &.{
.{
.label = "bar",
.kind = .Function,
},
.{
.label = "foo",
.kind = .Function,
.documentation = "Deprecated, use bar",
.deprecated = true,
},
});
try testCompletion(
\\const S = struct {
\\ /// Deprecated
\\ foo: u32,
\\};
\\comptime {
\\ var s: S = undefined;
\\ s.<cursor>
\\}
, &.{
.{
.label = "foo",
.kind = .Field,
.documentation = " Deprecated",
.deprecated = true,
.detail = "u32",
},
});
}

test "declarations" {
try testCompletion(
\\const S = struct {
Expand Down
53 changes: 53 additions & 0 deletions tests/lsp_features/semantic_tokens.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,59 @@ test "deprecated" {
});
}

test "deprecated with doc comment" {
try testSemanticTokens(
\\/// Deprecated: use bar
\\const foo = 123;
, &.{
.{ "/// Deprecated: use bar", .comment, .{ .documentation = true } },
.{ "const", .keyword, .{} },
.{ "foo", .variable, .{ .declaration = true, .deprecated = true } },
.{ "=", .operator, .{} },
.{ "123", .number, .{} },
});
try testSemanticTokens(
\\/// DEPRECATED use bar
\\const foo = 123;
\\const bar = foo;
, &.{
.{ "/// DEPRECATED use bar", .comment, .{ .documentation = true } },
.{ "const", .keyword, .{} },
.{ "foo", .variable, .{ .declaration = true, .deprecated = true } },
.{ "=", .operator, .{} },
.{ "123", .number, .{} },

.{ "const", .keyword, .{} },
.{ "bar", .variable, .{ .declaration = true, .deprecated = true } },
.{ "=", .operator, .{} },
.{ "foo", .variable, .{ .deprecated = true } },
});
try testSemanticTokens(
\\const S = struct {
\\ /// deprecated, use bar
\\ const foo = 123;
\\};
\\const bar = S.foo;
, &.{
.{ "const", .keyword, .{} },
.{ "S", .namespace, .{ .declaration = true } },
.{ "=", .operator, .{} },
.{ "struct", .keyword, .{} },

.{ "/// deprecated, use bar", .comment, .{ .documentation = true } },
.{ "const", .keyword, .{} },
.{ "foo", .variable, .{ .declaration = true, .deprecated = true } },
.{ "=", .operator, .{} },
.{ "123", .number, .{} },

.{ "const", .keyword, .{} },
.{ "bar", .variable, .{ .declaration = true, .deprecated = true } },
.{ "=", .operator, .{} },
.{ "S", .namespace, .{} },
.{ "foo", .variable, .{ .deprecated = true } },
});
}

test "zon file" {
try testSemanticTokensOptions(
\\.{
Expand Down