Skip to content

Commit 87e8d47

Browse files
authored
feat(linter): add avoid-as rule (#262)
> AI generated bc im feeling lazy This pull request introduces a new linting rule to disallow the use of `@as()` when types can be inferred, along with the necessary changes to integrate this rule into the existing system. ### New Linting Rule: * Added a new rule `avoid-as` that disallows the use of `@as()` when types can be inferred. This rule is categorized as `pedantic` and is enabled by default as a warning. [[1]](diffhunk://#diff-eca5dc407c141ea5b0c0e57df9f7d5d859bb57158b92836db34c1cfa214aabe4R1-R42) [[2]](diffhunk://#diff-f0b74904761a925e1f9e35ab917c5a2eaf91b7afeb7673e69a854fbd3b62b47eR1-R201) ### Integration of the New Rule: * Updated `rules_config.zig` to include the new `avoid-as` rule in the `RulesConfig` struct. * Updated `rules.zig` to import the new `avoid_as.zig` file. ### Supporting Changes: * Added a snapshot file `avoid-as.snap` for the new rule to provide diagnostic messages and suggestions. * Enhanced the `Span` and `LabeledSpan` structs in `span.zig` to include conversion functions from various types, which are used in the new rule implementation. [[1]](diffhunk://#diff-35d3d2d0cf701ff9be24302e2c679f08f5f040b793d8a864702a1294fc0eaf20R64) [[2]](diffhunk://#diff-35d3d2d0cf701ff9be24302e2c679f08f5f040b793d8a864702a1294fc0eaf20R152-R172)
1 parent d141840 commit 87e8d47

File tree

6 files changed

+299
-0
lines changed

6 files changed

+299
-0
lines changed

docs/rules/avoid-as.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# `avoid-as`
2+
3+
> Category: pedantic
4+
>
5+
> Enabled by default?: Yes (warning)
6+
7+
## What This Rule Does
8+
9+
Disallows using `@as()` when types can be otherwise inferred.
10+
11+
Zig has powerful [Result Location Semantics](https://ziglang.org/documentation/master/#Result-Location-Semantics) for inferring what type
12+
something should be. This happens in function parameters, return types,
13+
and type annotations. `@as()` is a last resort when no other contextual
14+
information is available. In any other case, other type inference mechanisms
15+
should be used.
16+
17+
> [!NOTE]
18+
> Checks for function parameters and return types are not yet implemented.
19+
20+
## Examples
21+
22+
Examples of **incorrect** code for this rule:
23+
24+
```zig
25+
const x = @as(u32, 1);
26+
27+
fn foo(x: u32) u64 {
28+
return @as(u64, x); // type is inferred from return type
29+
}
30+
foo(@as(u32, 1)); // type is inferred from function signature
31+
```
32+
33+
Examples of **correct** code for this rule:
34+
35+
```zig
36+
const x: u32 = 1;
37+
38+
fn foo(x: u32) void {
39+
// ...
40+
}
41+
foo(1);
42+
```

src/linter/config/rules_config.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ pub const RulesConfig = struct {
1515
unused_decls: RuleConfig(rules.UnusedDecls) = .{},
1616
useless_error_return: RuleConfig(rules.UselessErrorReturn) = .{},
1717
empty_file: RuleConfig(rules.EmptyFile) = .{},
18+
avoid_as: RuleConfig(rules.AvoidAs) = .{},
1819
};

src/linter/rules.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ pub const UnsafeUndefined = @import("./rules/unsafe_undefined.zig");
99
pub const UnusedDecls = @import("./rules/unused_decls.zig");
1010
pub const UselessErrorReturn = @import("./rules/useless_error_return.zig");
1111
pub const EmptyFile = @import("./rules/empty_file.zig");
12+
pub const AvoidAs = @import("./rules/avoid_as.zig");

src/linter/rules/avoid_as.zig

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
//! ## What This Rule Does
2+
//!
3+
//! Disallows using `@as()` when types can be otherwise inferred.
4+
//!
5+
//! Zig has powerful [Result Location Semantics](https://ziglang.org/documentation/master/#Result-Location-Semantics) for inferring what type
6+
//! something should be. This happens in function parameters, return types,
7+
//! and type annotations. `@as()` is a last resort when no other contextual
8+
//! information is available. In any other case, other type inference mechanisms
9+
//! should be used.
10+
//!
11+
//! > [!NOTE]
12+
//! > Checks for function parameters and return types are not yet implemented.
13+
//!
14+
//! ## Examples
15+
//!
16+
//! Examples of **incorrect** code for this rule:
17+
//! ```zig
18+
//! const x = @as(u32, 1);
19+
//!
20+
//! fn foo(x: u32) u64 {
21+
//! return @as(u64, x); // type is inferred from return type
22+
//! }
23+
//! foo(@as(u32, 1)); // type is inferred from function signature
24+
//! ```
25+
//!
26+
//! Examples of **correct** code for this rule:
27+
//! ```zig
28+
//! const x: u32 = 1;
29+
//!
30+
//! fn foo(x: u32) void {
31+
//! // ...
32+
//! }
33+
//! foo(1);
34+
//! ```
35+
36+
const std = @import("std");
37+
const util = @import("util");
38+
const semantic = @import("../../semantic.zig");
39+
const Semantic = semantic.Semantic;
40+
const _rule = @import("../rule.zig");
41+
const _span = @import("../../span.zig");
42+
43+
const Ast = std.zig.Ast;
44+
const Node = Ast.Node;
45+
const LinterContext = @import("../lint_context.zig");
46+
const Rule = _rule.Rule;
47+
const NodeWrapper = _rule.NodeWrapper;
48+
const Fix = @import("../fix.zig").Fix;
49+
50+
const Error = @import("../../Error.zig");
51+
const Cow = util.Cow(false);
52+
53+
// Rule metadata
54+
const AvoidAs = @This();
55+
pub const meta: Rule.Meta = .{
56+
.name = "avoid-as",
57+
.category = .pedantic,
58+
.default = .warning,
59+
.fix = .safe_fix,
60+
};
61+
62+
fn preferTypeAnnotationDiagnostic(ctx: *LinterContext, as_tok: Ast.TokenIndex) Error {
63+
var e = ctx.diagnostic(
64+
"Prefer using type annotations over @as().",
65+
.{ctx.spanT(as_tok)},
66+
);
67+
e.help = Cow.static("Use a type annotation instead.");
68+
return e;
69+
}
70+
71+
fn lolTheresAlreadyATypeAnnotationDiagnostic(ctx: *LinterContext, as_tok: Ast.TokenIndex) Error {
72+
var e = ctx.diagnostic(
73+
"Unnecessary use of @as().",
74+
.{ctx.spanT(as_tok)},
75+
);
76+
e.help = Cow.static("Remove the @as() call.");
77+
return e;
78+
}
79+
80+
// Runs on each node in the AST. Useful for syntax-based rules.
81+
pub fn runOnNode(_: *const AvoidAs, wrapper: NodeWrapper, ctx: *LinterContext) void {
82+
const node = wrapper.node;
83+
if (node.tag != .builtin_call_two and node.tag != .builtin_call_comma) {
84+
return;
85+
}
86+
87+
const builtin_name = ctx.semantic.tokenSlice(node.main_token);
88+
if (!std.mem.eql(u8, builtin_name, "@as")) {
89+
return;
90+
}
91+
92+
const tags: []const Node.Tag = ctx.ast().nodes.items(.tag);
93+
const datas: []const Node.Data = ctx.ast().nodes.items(.data);
94+
const parent = ctx.links().getParent(wrapper.idx) orelse return;
95+
96+
switch (tags[parent]) {
97+
.simple_var_decl => {
98+
const data = datas[parent];
99+
const ty_annotation = data.lhs;
100+
const diagnostic = if (ty_annotation == Semantic.NULL_NODE)
101+
preferTypeAnnotationDiagnostic(ctx, node.main_token)
102+
else
103+
lolTheresAlreadyATypeAnnotationDiagnostic(ctx, node.main_token);
104+
105+
ctx.reportWithFix(
106+
VarFixer{ .var_decl = parent, .var_decl_data = data, .as_args = node.data },
107+
diagnostic,
108+
VarFixer.replaceWithTypeAnnotation,
109+
);
110+
},
111+
112+
else => {},
113+
}
114+
}
115+
116+
const VarFixer = struct {
117+
/// this is a simple_var_decl node
118+
var_decl: Ast.Node.Index,
119+
var_decl_data: Node.Data,
120+
/// `@as(lhs, rhs)`
121+
as_args: Ast.Node.Data,
122+
123+
fn replaceWithTypeAnnotation(this: VarFixer, builder: Fix.Builder) !Fix {
124+
// invalid, @as() has no args: `const x = @as();`
125+
if (this.as_args.lhs == Semantic.NULL_NODE or this.as_args.rhs == Semantic.NULL_NODE) {
126+
@branchHint(.unlikely);
127+
return builder.noop();
128+
}
129+
130+
const nodes = builder.ctx.ast().nodes;
131+
const toks = builder.ctx.ast().tokens;
132+
const tok_tags: []const Semantic.Token.Tag = toks.items(.tag);
133+
134+
const ty_annotation = this.var_decl_data.lhs;
135+
if (ty_annotation == Semantic.NULL_NODE) {
136+
// `const` or `var`
137+
var tok = nodes.items(.main_token)[this.var_decl];
138+
const is_const = tok_tags[tok] == .keyword_const;
139+
tok += 1; // next tok is the identifier
140+
util.debugAssert(tok_tags[tok] == .identifier, "Expected identifier, got {}", .{tok_tags[tok]});
141+
142+
const ident = builder.ctx.semantic.tokenSlice(tok);
143+
const ty_text = builder.snippet(.node, this.as_args.lhs);
144+
const expr_text = builder.snippet(.node, this.as_args.rhs);
145+
return builder.replace(
146+
builder.spanCovering(.node, this.var_decl),
147+
try Cow.fmt(
148+
builder.allocator,
149+
"{s} {s}: {s} = {s}",
150+
.{
151+
if (is_const) "const" else "var",
152+
ident,
153+
ty_text,
154+
expr_text,
155+
},
156+
),
157+
);
158+
} else {
159+
const expr = builder.snippet(.node, this.as_args.rhs);
160+
return builder.replace(
161+
builder.spanCovering(.node, this.var_decl_data.rhs),
162+
Cow.initBorrowed(expr),
163+
);
164+
}
165+
}
166+
};
167+
168+
// Used by the Linter to register the rule so it can be run.
169+
pub fn rule(self: *AvoidAs) Rule {
170+
return Rule.init(self);
171+
}
172+
173+
const RuleTester = @import("../tester.zig");
174+
test AvoidAs {
175+
const t = std.testing;
176+
177+
var avoid_as = AvoidAs{};
178+
var runner = RuleTester.init(t.allocator, avoid_as.rule());
179+
defer runner.deinit();
180+
181+
// Code your rule should pass on
182+
const pass = &[_][:0]const u8{
183+
// TODO: add test cases
184+
"const x = 1;",
185+
"const x: u32 = 1;",
186+
};
187+
188+
// Code your rule should fail on
189+
const fail = &[_][:0]const u8{
190+
// TODO: add test cases
191+
"const x = @as(u32, 1);",
192+
"const x: u32 = @as(u32, 1);",
193+
};
194+
195+
const fix = &[_]RuleTester.FixCase{
196+
.{
197+
.src = "const x = @as(u32, 1);",
198+
.expected = "const x: u32 = 1;",
199+
},
200+
.{
201+
.src = "var x = @as(u32, 1);",
202+
.expected = "var x: u32 = 1;",
203+
},
204+
.{
205+
.src = "const x: u32 = @as(u32, 1);",
206+
.expected = "const x: u32 = 1;",
207+
},
208+
.{
209+
.src = "var x: u32 = @as(u32, 1);",
210+
.expected = "var x: u32 = 1;",
211+
},
212+
};
213+
214+
try runner
215+
.withPass(pass)
216+
.withFail(fail)
217+
.withFix(fix)
218+
.run();
219+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
𝙭 avoid-as: Prefer using type annotations over @as().
2+
╭─[avoid-as.zig:1:11]
3+
1const x = @as(u32, 1);
4+
· ───
5+
╰────
6+
help: Use a type annotation instead.
7+
8+
𝙭 avoid-as: Unnecessary use of @as().
9+
╭─[avoid-as.zig:1:16]
10+
1const x: u32 = @as(u32, 1);
11+
· ───
12+
╰────
13+
help: Remove the @as() call.
14+

src/span.zig

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ pub const Span = struct {
6161
pub fn from(value: anytype) Span {
6262
return switch (@TypeOf(value)) {
6363
Span => value, // base case
64+
LabeledSpan => value.span,
6465
std.zig.Ast.Span => .{ .start = value.start, .end = value.end },
6566
std.zig.Token.Loc => .{ .start = @intCast(value.start), .end = @intCast(value.end) },
6667
[2]u32 => .{ .start = value[0], .end = value[1] },
@@ -148,6 +149,27 @@ pub const LabeledSpan = struct {
148149
.span = .{ .start = start, .end = end },
149150
};
150151
}
152+
pub fn from(value: anytype) LabeledSpan {
153+
return switch (@TypeOf(value)) {
154+
LabeledSpan => value, // base case
155+
Span => .{ .span = value },
156+
std.zig.Ast.Span => .{ .span = Span.from(value) },
157+
std.zig.Token.Loc => .{ .span = Span.from(value) },
158+
[2]u32 => .{ .span = Span.from(value) },
159+
else => |T| {
160+
const info = @typeInfo(T);
161+
switch (info) {
162+
.@"struct", .@"enum" => {
163+
if (@hasField(T, "span")) {
164+
return LabeledSpan.from(@field(value, "span"));
165+
}
166+
},
167+
else => {},
168+
}
169+
@compileError("Cannot convert type " ++ @typeName(T) ++ "into a LabeledSpan.");
170+
},
171+
};
172+
}
151173
};
152174

153175
pub const Location = struct {

0 commit comments

Comments
 (0)