Skip to content

Commit 64f021c

Browse files
committed
Insert explicit return expressions in functions/macros/do-blocks
This patch make sure that function and macro definitions, as well as do-blocks, end with an explicit `return` statement before the last expression in the body. The following exceptions are made: - If the last expression is a `for` or `while` loop (which both always evaluate to `nothing`) `return nothing` is added *after* the loop. - If the last expression is a `if` or `try` block the `return` is only added in case there is no `return` inside any of the branches. - If the last expression is a `let` or `begin` block the `return` is only added in case there is no `return` inside the block. - If the last expression is a function call, and the function name is `throw`, `rethrow`, or `error`, no `return` is added. This is because it is pretty obvious that these calls terminate the function without the explicit `return`. Closes #43.
1 parent 57e5cd3 commit 64f021c

File tree

5 files changed

+115
-25
lines changed

5 files changed

+115
-25
lines changed

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ This is a list of things that Runic currently is doing:
244244
- [Line width limit](#line-width-limit)
245245
- [Newlines in blocks](#newlines-in-blocks)
246246
- [Indentation](#indentation)
247+
- [Explicit `return`](#explicit-return)
247248
- [Spaces around operators, assignment, etc](#spaces-around-operators-assignment-etc)
248249
- [Spaces around keywords](#spaces-around-keywords)
249250
- [Multiline listlike expressions](#multiline-listlike-expressions)
@@ -399,6 +400,42 @@ x = a + b *
399400
d
400401
```
401402
403+
### Explicit `return`
404+
405+
Explicit `return` statements are ensured in function/macro definitions as well as in
406+
`do`-blocks by adding `return` in front of the last expression, with some exceptions listed
407+
below.
408+
409+
- If the last expression is a `for` or `while` loop (which both always evaluate to
410+
`nothing`) `return nothing` is added *after* the loop.
411+
- If the last expression is a `if` or `try` block the `return` is only added in case
412+
there is no `return` inside any of the branches.
413+
- If the last expression is a `let` or `begin` block the `return` is only added in case
414+
there is no `return` inside the block.
415+
- If the last expression is a function call, and the function name is `throw`, `rethrow`,
416+
or `error`, no `return` is added. This is because it is pretty obvious that these calls
417+
terminate the function without the explicit `return`.
418+
419+
```diff
420+
function f(n)
421+
- sum(rand(n))
422+
+ return sum(rand(n))
423+
end
424+
425+
macro m(args...)
426+
- :(generate_expr(args...))
427+
+ return :(generate_expr(args...))
428+
end
429+
430+
function g()
431+
- open("/dev/random", "r") do f
432+
- read(f, 8)
433+
+ return open("/dev/random", "r") do f
434+
+ return read(f, 8)
435+
end
436+
end
437+
```
438+
402439
### Spaces around operators, assignment, etc
403440
404441
Runic formats spaces around infix operators, assignments, comparison chains, and type

src/JuliaSyntax.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ function _show_green_node(io, node, indent, pos, str, show_trivia)
4848
p += x.span
4949
end
5050
end
51+
return
5152
end

src/ToggleableAsserts.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ end
3636
const toggle_lock = ReentrantLock()
3737

3838
function enable_assert(enable::Bool)
39-
@lock toggle_lock begin
39+
return @lock toggle_lock begin
4040
if assert_enabled() != enable
4141
@eval Runic assert_enabled() = $enable
4242
end

src/runestone.jl

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3371,24 +3371,58 @@ function return_node(ctx::Context, ret::Node)
33713371
return Node(JuliaSyntax.SyntaxHead(K"return", 0), kids)
33723372
end
33733373

3374+
function has_return(node::Node)
3375+
kids = verified_kids(node)
3376+
if kind(node) in KSet"let catch else finally"
3377+
idx = findfirst(x -> kind(x) === K"block", kids)::Int
3378+
if kind(node) === K"let"
3379+
idx = findnext(x -> kind(x) === K"block", kids, idx + 1)::Int
3380+
end
3381+
return has_return(kids[idx])
3382+
elseif kind(node) in KSet"try if elseif"
3383+
# Look for the initial try/if block and then for
3384+
# catch/else/finally (for try) or elseif/else (for if).
3385+
pred = function(x)
3386+
return !is_leaf(x) && kind(x) in KSet"catch else finally elseif block"
3387+
end
3388+
idx = findfirst(pred, kids)
3389+
while idx !== nothing
3390+
has_return(kids[idx]) && return true
3391+
idx = findnext(pred, kids, idx + 1)
3392+
end
3393+
return false
3394+
elseif kind(node) === K"block"
3395+
# Don't care whether this is the last expression,
3396+
# that is the job of a linter or something I guess.
3397+
return findfirst(x -> kind(x) === K"return", kids) !== nothing
3398+
else
3399+
error("unreachable")
3400+
end
3401+
end
3402+
33743403
function explicit_return_block(ctx, node)
33753404
@assert kind(node) === K"block"
33763405
kids = verified_kids(node)
3377-
rexpr_idx = findlast(!JuliaSyntax.is_whitespace, kids)
3378-
rexpr_idx === nothing && return nothing
3379-
rexpr = kids[rexpr_idx]
3406+
kids′ = kids
3407+
rexpr_idx = findlast(!JuliaSyntax.is_whitespace, kids′)
3408+
if rexpr_idx === nothing
3409+
# Empty block. TODO: Perhaps add `return nothing`?
3410+
return nothing
3411+
end
3412+
rexpr = kids′[rexpr_idx]
33803413
kind(rexpr) === K"return" && return nothing
33813414
if is_leaf(rexpr) ||
33823415
kind(rexpr) in KSet"call dotcall tuple vect ref hcat typed_hcat vcat typed_vcat \
33833416
? && || :: juxtapose <: >: comparison string . -> comprehension do macro \
3384-
typed_comprehension where parens curly function quote global local =" ||
3385-
is_string_macro(rexpr) ||
3386-
is_assignment(rexpr) ||
3417+
typed_comprehension where parens curly function quote global local = macrocall" ||
3418+
is_string_macro(rexpr) || is_assignment(rexpr) ||
3419+
(kind(rexpr) in KSet"let if try" && !has_return(rexpr)) ||
3420+
(is_begin_block(rexpr) && !has_return(rexpr)) ||
33873421
kind(rexpr) === K"quote" && JuliaSyntax.has_flags(rexpr, JuliaSyntax.COLON_QUOTE)
33883422
# The cases caught in this branch are simple, just wrap the last expression in a
33893423
# return node. Also make sure the previous node is a K"NewlineWs".
33903424
for i in 1:(rexpr_idx - 1)
3391-
accept_node!(ctx, kids[i])
3425+
accept_node!(ctx, kids[i])
33923426
end
33933427
# If this is a call node, and the call the function name is `throw` or `error` we
33943428
# bail because `return throw(...)` looks kinda stupid.
@@ -3403,12 +3437,12 @@ function explicit_return_block(ctx, node)
34033437
fname = String(read_bytes(ctx, call_kids[fname_idx]))
34043438
seek(ctx.fmt_io, p)
34053439
end
3406-
if fname === "throw" || fname === "error"
3440+
if fname === "throw" || fname === "rethrow" || fname === "error"
34073441
return nothing
34083442
end
34093443
end
34103444
# We will make changes so copy
3411-
kids′ = copy(kids)
3445+
kids′ = kids′ === kids ? copy(kids) : kids′
34123446
# Make sure the previous node is a K"NewlineWs"
34133447
if !kmatch(kids′, KSet"NewlineWs", rexpr_idx - 1)
34143448
spn = 0
@@ -3445,10 +3479,10 @@ function explicit_return_block(ctx, node)
34453479
return make_node(node, kids′)
34463480
elseif kind(rexpr) in KSet"for while"
34473481
# For `for` and `while` loops we add `return nothing` after the block.
3448-
@assert kind(kids[end]) === K"NewlineWs"
3482+
@assert kind(kids[end]) === K"NewlineWs"
34493483
@assert kind(last_leaf(rexpr)) === K"end"
34503484
insert_idx = lastindex(kids)
3451-
kids′ = copy(kids)
3485+
kids′ = kids′ === kids ? copy(kids) : kids′
34523486
for i in 1:(insert_idx - 1)
34533487
accept_node!(ctx, kids′[i])
34543488
end
@@ -3469,16 +3503,6 @@ function explicit_return_block(ctx, node)
34693503
)
34703504
insert!(kids′, insert_idx + 1, retnode)
34713505
return make_node(node, kids′)
3472-
elseif kind(rexpr) in KSet"if try let block macrocall $"
3473-
# These have been seen in the wild.
3474-
# TODO:
3475-
# - `if` and `try` could be recursed into, but is that really so nice? `return if`?
3476-
# Perhaps if no branches have `return`?
3477-
# `if` without `else` could have `return nothing` at the end.
3478-
# - `macrocall` could be inspected and have return if the argument is simple, e.g.
3479-
# `@inbounds x[i]` -> `return @inbounds x[i]`.
3480-
# - block (begin-end) and let could be recursed into?
3481-
# - `$` could be anything but only used in eval so not super common...
34823506
else
34833507
# error("Unhandled node in explicit_return_block: $(kind(rexpr))")
34843508
end

test/runtests.jl

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,8 +1238,17 @@ end
12381238
"f() do\n return x\n end", "f() do x\n return x\n end",
12391239
"function f()\n return x\n end",
12401240
"function ()\n return x\n end",
1241-
"quote\n x\n end",
1241+
"quote\n x\n end", "begin\n x\n end",
1242+
"let\n x\n end", "let x = 42\n x\n end",
12421243
"x = 1", "x += 1", "x -= 1", "global x = 1", "local x = 1",
1244+
"@inbounds x[i]", "@inline f(x)",
1245+
"if c\n x\n end",
1246+
"if c\n x\n else\n y\n end",
1247+
"if c\n x\n elseif d\n z\n else\n y\n end",
1248+
"try\n x\n catch\n y\n end",
1249+
"try\n x\n catch e\n y\n end",
1250+
"try\n x\n catch\n y\n finally\n z\n end",
1251+
"try\n x\n catch\n y\n else\n z\n finally\n z\n end",
12431252
)
12441253
@test format_string("$f\n $r\nend") == "$f\n return $r\nend"
12451254
@test format_string("$f\n x;$r\nend") == "$f\n x\n return $r\nend"
@@ -1249,15 +1258,34 @@ end
12491258
format_string("$f\n return $f\n return $r\n end\nend")
12501259
end
12511260
# If the last expression is a call to throw or error there should be no return
1252-
for r in ("throw(ArgumentError())", "error(\"foo\")")
1261+
for r in ("throw(ArgumentError())", "error(\"foo\")", "rethrow()")
12531262
@test format_string("$f\n $r\nend") == "$f\n $r\nend"
12541263
end
1255-
# These cases append `return nothing` to the end
1264+
# `for` and `while` append `return nothing` to the end
12561265
for r in ("for i in I\n end", "while i in I\n end")
12571266
@test format_string("$f\n $r\nend") == "$f\n $r\n return nothing\nend"
12581267
@test format_string("$f\n $r\n # comment\nend") ==
12591268
"$f\n $r\n # comment\n return nothing\nend"
12601269
end
1270+
# if/let/begin/try with a `return` inside should be left alone
1271+
for r in (
1272+
"if c\n return x\n end",
1273+
"if c\n return x\n else\n y\n end",
1274+
"if c\n x\n else\n return y\n end",
1275+
"if c\n return x\n elseif d\n y\n else\n y\n end",
1276+
"if c\n x\n elseif d\n return y\n else\n z\n end",
1277+
"if c\n x\n elseif d\n y\n else\n return z\n end",
1278+
"let\n return x\n end",
1279+
"let x = 1\n return x\n end",
1280+
"begin\n return x\n end",
1281+
"try\n return x\n catch\n y\n end",
1282+
"try\n x\n catch e\n return y\n end",
1283+
"try\n x\n catch\n y\n finally\n return z\n end",
1284+
"try\n x\n catch\n y\n else\n return z\n finally\n z\n end",
1285+
)
1286+
str = "$f\n $r\nend"
1287+
@test format_string(str) == str
1288+
end
12611289
end
12621290
end
12631291

0 commit comments

Comments
 (0)