Skip to content

Commit ff383c0

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`. Since adding `return` changes the expression that a macro will see, this rule is disabled for function definitions inside of macros with the exception of some known ones from Base (e.g. `@inline`, `@generated`, ...). Closes #43.
1 parent 13b6402 commit ff383c0

File tree

9 files changed

+399
-23
lines changed

9 files changed

+399
-23
lines changed

README.md

Lines changed: 48 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,53 @@ 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 already obvious that these calls
417+
terminate the function and don't return any value.
418+
419+
Note that adding `return` changes the expression in a way that is visible to macros.
420+
Therefore it is, in general, not valid to add `return` to a function defined inside a macro
421+
since it isn't possible to know what the macro will expand to. For this reason this
422+
formatting rule is disabled for functions defined inside macros with the exception of some
423+
known and safe ones from Base (e.g. `@inline`, `@generated`, ...).
424+
425+
For the same reason mentioned above, if the last expression in a function is a macro call it
426+
isn't valid to step in and add `return` inside. Instead the `return` will be added in front
427+
of the macro call like any other expression.
428+
429+
Examples:
430+
```diff
431+
function f(n)
432+
- sum(rand(n))
433+
+ return sum(rand(n))
434+
end
435+
436+
macro m(args...)
437+
- :(generate_expr(args...))
438+
+ return :(generate_expr(args...))
439+
end
440+
441+
function g()
442+
- open("/dev/random", "r") do f
443+
- read(f, 8)
444+
+ return open("/dev/random", "r") do f
445+
+ return read(f, 8)
446+
end
447+
end
448+
```
449+
402450
### Spaces around operators, assignment, etc
403451
404452
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/Runic.jl

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ function Base.show(io::IO, ::MIME"text/plain", node::Node)
6161
show(io, node)
6262
println(io)
6363
_show_green_node(io, node, "", 1, nothing, true)
64+
return nothing
6465
end
6566

6667
function Base.show(io::IO, node::Node)
@@ -137,6 +138,7 @@ mutable struct Context
137138
next_sibling::Union{Node, Nothing}
138139
# parent::Union{Node, Nothing}
139140
lineage_kinds::Vector{JuliaSyntax.Kind}
141+
lineage_macros::Vector{String}
140142
end
141143

142144
function Context(
@@ -167,11 +169,12 @@ function Context(
167169
call_depth = 0
168170
prev_sibling = next_sibling = nothing
169171
lineage_kinds = JuliaSyntax.Kind[]
172+
lineage_macros = String[]
170173
format_on = true
171174
return Context(
172175
src_str, src_tree, src_io, fmt_io, fmt_tree, quiet, verbose, assert, debug, check,
173176
diff, filemode, indent_level, call_depth, format_on, prev_sibling, next_sibling,
174-
lineage_kinds
177+
lineage_kinds, lineage_macros
175178
)
176179
end
177180

@@ -306,6 +309,9 @@ function format_node_with_kids!(ctx::Context, node::Node)
306309
ctx.prev_sibling = nothing
307310
ctx.next_sibling = nothing
308311
push!(ctx.lineage_kinds, kind(node))
312+
if kind(node) === K"macrocall"
313+
push!(ctx.lineage_macros, macrocall_name(ctx, node))
314+
end
309315

310316
# The new node parts. `kids′` aliases `kids` and only copied below if any of the
311317
# nodes change ("copy-on-write").
@@ -381,6 +387,9 @@ function format_node_with_kids!(ctx::Context, node::Node)
381387
ctx.prev_sibling = prev_sibling
382388
ctx.next_sibling = next_sibling
383389
pop!(ctx.lineage_kinds)
390+
if kind(node) === K"macrocall"
391+
pop!(ctx.lineage_macros)
392+
end
384393
ctx.call_depth -= 1
385394
# Return a new node if any of the kids changed
386395
if any_kid_changed
@@ -435,6 +444,7 @@ function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode}
435444
@return_something no_spaces_around_colon_etc(ctx, node)
436445
@return_something parens_around_op_calls_in_colon(ctx, node)
437446
@return_something for_loop_use_in(ctx, node)
447+
@return_something explicit_return(ctx, node)
438448
@return_something braces_around_where_rhs(ctx, node)
439449
@return_something indent_multiline_strings(ctx, node)
440450
@return_something four_space_indent(ctx, node)

src/ToggleableAsserts.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ assert_enabled() = true
3030

3131
macro assert(expr)
3232
code = macroexpand_assert(expr)
33-
:(assert_enabled() ? $(code) : nothing)
33+
return :(assert_enabled() ? $(code) : nothing)
3434
end
3535

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/chisels.jl

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,57 @@ function kmatch(kids, kinds, i = firstindex(kids))
746746
return true
747747
end
748748

749+
# Extract the macro name as written in the source.
750+
function macrocall_name(ctx, node)
751+
@assert kind(node) === K"macrocall"
752+
kids = verified_kids(node)
753+
pred = x -> kind(x) in KSet"MacroName StringMacroName CmdMacroName core_@cmd"
754+
mkind = kind(first_leaf_predicate(node, pred)::Node)
755+
if kmatch(kids, KSet"@ MacroName")
756+
p = position(ctx.fmt_io)
757+
bytes = read(ctx.fmt_io, span(kids[1]) + span(kids[2]))
758+
seek(ctx.fmt_io, p)
759+
return String(bytes)
760+
elseif kmatch(kids, KSet".") || kmatch(kids, KSet"CmdMacroName") ||
761+
kmatch(kids, KSet"StringMacroName")
762+
bytes = read_bytes(ctx, kids[1])
763+
if mkind === K"CmdMacroName"
764+
append!(bytes, "_cmd")
765+
elseif mkind === K"StringMacroName"
766+
append!(bytes, "_str")
767+
end
768+
return String(bytes)
769+
elseif kmatch(kids, KSet"core_@cmd")
770+
bytes = read_bytes(ctx, kids[1])
771+
@assert length(bytes) == 0
772+
return "core_@cmd"
773+
else
774+
# Don't bother looking in more complex expressions, just return unknown
775+
return "<unknown macro>"
776+
end
777+
end
778+
779+
# Inserting `return` modifies the AST in a way that is visible to macros.. In general it is
780+
# never safe to change the AST inside a macro, but we make an exception for some common
781+
# "known" macros in order to be able to format functions that e.g. have an `@inline`
782+
# annotation in front.
783+
const MACROS_SAFE_TO_INSERT_RETURN = let set = Set{String}()
784+
for m in (
785+
"inline", "noinline", "propagate_inbounds", "generated", "eval",
786+
"assume_effects", "doc",
787+
)
788+
push!(set, "@$m", "Base.@$m")
789+
end
790+
push!(set, "Core.@doc")
791+
set
792+
end
793+
function safe_to_insert_return(ctx, node)
794+
for m in ctx.lineage_macros
795+
m in MACROS_SAFE_TO_INSERT_RETURN || return false
796+
end
797+
return true
798+
end
799+
749800
##########################
750801
# Utilities for IOBuffer #
751802
##########################

src/debug.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ function Base.showerror(io::IO, err::AssertionError)
2525
"please file an issue with a reproducible example at " *
2626
"https://github.com/fredrikekre/Runic.jl/issues/new."
2727
)
28+
return nothing
2829
end
2930

3031
function macroexpand_assert(expr)

src/main.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ function maybe_expand_directory!(outfiles, dir)
8484
end
8585
end
8686
end
87+
return nothing
8788
end
8889

8990
function main(argv)
@@ -299,6 +300,7 @@ function main(argv)
299300
cmd = setenv(ignorestatus(cmd); dir = dir)
300301
cmd = pipeline(cmd, stdout = stderr, stderr = stderr)
301302
run(cmd)
303+
return nothing
302304
end
303305
end
304306

0 commit comments

Comments
 (0)