Skip to content

Commit 90dca75

Browse files
committed
lint: allow return try for optional error payloads
Z017 flags return try expressions as redundant, but Zig can require try when the enclosing function returns an optional error-union payload. In cases like !?Thing, try unwraps !Thing to Thing before return coerces the payload to ?Thing. Skip the diagnostic for optional error-union return payloads and document the exception. Add regression coverage so the normal redundant case still warns while !?Thing returns do not.
1 parent 7ac5ac2 commit 90dca75

3 files changed

Lines changed: 67 additions & 3 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Directories are scanned recursively for `.zig` files.
3838
| Z014 | Error set names should be PascalCase |
3939
| Z015 | Public function exposes private error set |
4040
| Z016 | Split compound assert: `assert(a and b)``assert(a); assert(b);` |
41-
| Z017 | Redundant `try` in return: `return try expr``return expr` |
41+
| Z017 | Redundant `try` in return when no payload coercion is needed |
4242
| Z018 | Redundant `@as` when type is already known from context |
4343
| Z019 | `@This()` in named struct; use the type name instead |
4444
| Z020 | Inline `@This()`; assign to a constant first |

docs/rules/Z017.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ enabled: true
66

77
# Z017: Redundant try in return
88

9-
Detects `return try expr` where `try` is redundant. When returning an error union, `return expr` handles error propagation automatically.
9+
Detects `return try expr` where `try` is redundant. When returning an error union with the same payload type, `return expr` handles error propagation automatically.
1010

1111
## Examples
1212

@@ -38,6 +38,20 @@ fn process() !void {
3838
}
3939
```
4040

41+
The `try` can also be needed when the function return type performs an additional payload coercion, such as returning `Thing` as `?Thing` in an `!?Thing` function:
42+
43+
```zig
44+
const Thing = struct {};
45+
46+
fn allocThing() !Thing {
47+
return .{};
48+
}
49+
50+
fn maybeThing() !?Thing {
51+
return try allocThing();
52+
}
53+
```
54+
4155
## Rationale
4256

43-
In Zig, `return expr` already propagates errors when the function returns an error union. Adding `try` is redundant and adds visual noise. The only purpose of `try` is to convert an error union to its payload type for intermediate operations.
57+
In Zig, `return expr` already propagates errors when the function returns a matching error union. Adding `try` is redundant and adds visual noise. Keep `try` when it converts an error union to its payload for another coercion or intermediate operation.

src/Linter.zig

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1884,6 +1884,10 @@ fn checkReturnTry(self: *Linter, return_node: Ast.Node.Index, return_expr: Ast.N
18841884
// Check if the return expression is a try
18851885
if (self.tree.nodeTag(return_expr) != .@"try") return;
18861886

1887+
// In `!?T` functions, `try` can be required to coerce `!T` into `!?T`:
1888+
// the `try` unwraps the error union, then `return` coerces `T` to `?T`.
1889+
if (self.currentReturnTypeMayNeedTryForOptionalPayload()) return;
1890+
18871891
// Get the inner expression being tried
18881892
const try_expr = self.tree.nodeData(return_expr).node;
18891893
const expr_source = self.getNodeSource(try_expr);
@@ -1893,6 +1897,23 @@ fn checkReturnTry(self: *Linter, return_node: Ast.Node.Index, return_expr: Ast.N
18931897
self.report(loc, .Z017, truncated);
18941898
}
18951899

1900+
fn currentReturnTypeMayNeedTryForOptionalPayload(self: *Linter) bool {
1901+
const fn_return_type = self.current_fn_return_type.unwrap() orelse return false;
1902+
const tag = self.tree.nodeTag(fn_return_type);
1903+
1904+
// Bare `!?T` function returns store only the payload node in FnProto.return_type;
1905+
// the `!` is the token immediately before the optional payload.
1906+
if (tag == .optional_type) {
1907+
const first_token = self.tree.firstToken(fn_return_type);
1908+
return first_token > 0 and self.tree.tokenTag(first_token - 1) == .bang;
1909+
}
1910+
1911+
if (tag != .error_union) return false;
1912+
1913+
const data = self.tree.nodeData(fn_return_type).node_and_node;
1914+
return self.tree.nodeTag(data[1]) == .optional_type;
1915+
}
1916+
18961917
fn checkCatchReturnAll(self: *Linter) void {
18971918
for (0..self.tree.nodes.len) |i| {
18981919
const node: Ast.Node.Index = @enumFromInt(i);
@@ -4377,6 +4398,35 @@ test "Z016: simple assert is ok" {
43774398
}
43784399
}
43794400

4401+
test "Z017: detect redundant try in return" {
4402+
var linter: Linter = .init(std.testing.allocator,
4403+
\\fn bar() !u32 {
4404+
\\ return 1;
4405+
\\}
4406+
\\fn foo() !u32 {
4407+
\\ return try bar();
4408+
\\}
4409+
, "test.zig", null);
4410+
defer linter.deinit();
4411+
linter.lint();
4412+
try std.testing.expectEqual(1, linter.diagnosticCount(.Z017));
4413+
}
4414+
4415+
test "Z017: allow try when error union payload coerces to optional return" {
4416+
var linter: Linter = .init(std.testing.allocator,
4417+
\\const Thing = struct {};
4418+
\\fn allocThing() !Thing {
4419+
\\ return .{};
4420+
\\}
4421+
\\fn foo() !?Thing {
4422+
\\ return try allocThing();
4423+
\\}
4424+
, "test.zig", null);
4425+
defer linter.deinit();
4426+
linter.lint();
4427+
try std.testing.expectEqual(0, linter.diagnosticCount(.Z017));
4428+
}
4429+
43804430
test "Z019: @This() in named struct should warn" {
43814431
var linter: Linter = .init(std.testing.allocator,
43824432
\\const Foo = struct {

0 commit comments

Comments
 (0)