Repository: domluna/JuliaFormatter.jl
Pull request #49 (2019-09-07 20:45:57)

Add format github action
whyyy is this soo hard


Patch:
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
new file mode 100644
index 00000000..8163fce0
--- /dev/null
+++ b/.github/workflows/format.yml
@@ -0,0 +1,12 @@
+name: Format
+
+on: [push]
+
+jobs:
+  format:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@master
+    - uses: domluna/JuliaFormatter-action@master
+      with:
+        args: -v .
diff --git a/Project.toml b/Project.toml
index 996d40e8..707aac14 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,7 +1,7 @@
 name = "JuliaFormatter"
 uuid = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
 authors = ["Dominique Luna <dluna132@gmail.com>"]
-version = "0.1.8"
+version = "0.1.10"
 
 [deps]
 CSTParser = "00ebfdb7-1f24-5e51-bd34-a7502290713f"
diff --git a/src/JuliaFormatter.jl b/src/JuliaFormatter.jl
index 38ee8322..e5a4ffd9 100644
--- a/src/JuliaFormatter.jl
+++ b/src/JuliaFormatter.jl
@@ -3,7 +3,7 @@ module JuliaFormatter
 using CSTParser
 import CSTParser.Tokenize.Tokens
 
-export format, format_file, format_dir
+export format, format_text, format_file, format_dir
 
 is_str_or_cmd(x::Tokens.Kind) =
     x in (Tokens.CMD, Tokens.TRIPLE_CMD, Tokens.STRING, Tokens.TRIPLE_STRING)
@@ -17,16 +17,25 @@ struct Document
     # mapping the offset in the file to the raw literal
     # string and what lines it starts and ends at.
     lit_strings::Dict{Int,Tuple{Int,Int,String}}
-    comments::Dict{Int,String}
+    comments::Dict{Int,Tuple{Int,String}}
+
+    # CSTParser does not detect semicolons.
+    # It's useful to know where these are for
+    # a few node types.
+    semicolons::Set{Int}
 end
+
 function Document(text::AbstractString)
     ranges = UnitRange{Int}[]
     line_to_range = Dict{Int,UnitRange{Int}}()
     lit_strings = Dict{Int,Tuple{Int,Int,String}}()
-    comments = Dict{Int,String}()
+    comments = Dict{Int,Tuple{Int,String}}()
+    semicolons = Set{Int}()
+    # dummy initial token
+    prev_tok = Tokens.Token()
 
     for t in CSTParser.Tokenize.tokenize(text)
-        if t.kind == Tokens.WHITESPACE
+        if t.kind === Tokens.WHITESPACE
             offset = t.startbyte
             for c in t.val
                 if c == '\n'
@@ -35,7 +44,7 @@ function Document(text::AbstractString)
                 end
                 offset += 1
             end
-        elseif t.kind == Tokens.ENDMARKER
+        elseif t.kind === Tokens.ENDMARKER
             s = length(ranges) > 0 ? last(ranges[end]) + 1 : 1
             push!(ranges, s:t.startbyte)
         elseif is_str_or_cmd(t.kind)
@@ -48,33 +57,46 @@ function Document(text::AbstractString)
                     push!(ranges, s:offset+nl)
                 end
             end
-        elseif t.kind == Tokens.COMMENT
+        elseif t.kind === Tokens.COMMENT
+            ws = 0
+            if prev_tok.kind === Tokens.WHITESPACE
+                # Handles the case where the value of the
+                # WHITESPACE token is like " \n ".
+                i = findlast(c -> c == '\n', prev_tok.val)
+                i === nothing && (i = 1)
+                ws = count(c -> c == ' ', prev_tok.val[i:end])
+            end
             if t.startpos[1] == t.endpos[1]
-                comments[t.startpos[1]] = t.val
+                # Determine the number of spaces prior to a possible inline comment
+                comments[t.startpos[1]] = (ws, t.val)
             else
                 sl = t.startpos[1]
                 offset = t.startbyte
-                comment_offset = 1
+                cs = ""
                 for (i, c) in enumerate(t.val)
+                    cs *= c
                     if c == '\n'
                         s = length(ranges) > 0 ? last(ranges[end]) + 1 : 1
                         push!(ranges, s:offset+1)
-                        comments[sl] = t.val[comment_offset:i-1]
+                        comments[sl] = (ws, cs)
                         sl += 1
-                        comment_offset = i + 1
+                        cs = ""
                     end
                     offset += 1
                 end
                 # last comment
-                comments[sl] = t.val[comment_offset:end]
+                comments[sl] = (ws, cs)
             end
+        elseif t.kind === Tokens.SEMICOLON
+            push!(semicolons, t.startpos[1])
         end
+        prev_tok = t
     end
     for (l, r) in enumerate(ranges)
         line_to_range[l] = r
     end
-    # @debug "" lit_strings comments
-    Document(text, ranges, line_to_range, lit_strings, comments)
+    # @info "" comments ranges
+    Document(text, ranges, line_to_range, lit_strings, comments, semicolons)
 end
 
 mutable struct State
@@ -88,10 +110,10 @@ end
 State(doc, indent_size, margin) = State(doc, indent_size, 0, 1, 0, margin)
 
 @inline nspaces(s::State) = s.indent
-@inline getline(d::Document, line::Int) = d.text[d.line_to_range[line]]
-@inline hascomment(d::Document, line::Int) = haskey(d.comments, line)
+@inline hascomment(d::Document, line::Integer) = haskey(d.comments, line)
+@inline has_semicolon(d::Document, line::Integer) = line in d.semicolons
 
-@inline function cursor_loc(s::State, offset::Int)
+@inline function cursor_loc(s::State, offset::Integer)
     for (l, r) in enumerate(s.doc.ranges)
         if offset in r
             return (l, offset - first(r) + 1, length(r))
@@ -127,9 +149,12 @@ function format_text(text::AbstractString; indent::Integer = 4, margin::Integer
     x, ps = CSTParser.parse(CSTParser.ParseState(text), true)
     ps.errored && error("Parsing error for input $text")
 
+    # no actual code
+    x.args[1].kind === Tokens.NOTHING && length(x) == 1 && return text
+
     d = Document(text)
     # If "nofmt" occurs in a comment on line 1 do not format
-    occursin("nofmt", get(d.comments, 1, "")) && (return text)
+    occursin("nofmt", get(d.comments, 1, (0, ""))[2]) && (return text)
 
     s = State(d, indent, margin)
     t = pretty(x, s)
diff --git a/src/nest.jl b/src/nest.jl
index 3fa70ab0..e56c0461 100644
--- a/src/nest.jl
+++ b/src/nest.jl
@@ -42,9 +42,15 @@ function add_indent!(x, s, indent)
 end
 
 function nest!(nodes::Vector{PTree}, s::State, indent; extra_width = 0)
-    for n in nodes
-        if n.typ === NEWLINE
+    for (i, n) in enumerate(nodes)
+        if n.typ === NEWLINE && nodes[i+1].typ === CSTParser.Block
+            s.line_offset = nodes[i+1].indent
+        elseif n.typ === NOTCODE && nodes[i+1].typ === CSTParser.Block
+            s.line_offset = nodes[i+1].indent
+        elseif n.typ === NEWLINE
             s.line_offset = indent
+        elseif is_leaf(n)
+            s.line_offset += length(n)
         else
             nest!(n, s, extra_width = extra_width)
         end
@@ -77,6 +83,8 @@ function nest!(x::PTree, s::State; extra_width = 0)
         n_call!(x, s, extra_width = extra_width)
     elseif x.typ === CSTParser.MacroCall
         n_macrocall!(x, s, extra_width = extra_width)
+    elseif x.typ === CSTParser.Ref
+        n_call!(x, s, extra_width = extra_width)
     elseif x.typ === CSTParser.ChainOpCall
         n_chainopcall!(x, s, extra_width = extra_width)
     elseif x.typ === CSTParser.Comparison
@@ -97,23 +105,27 @@ function nest!(x::PTree, s::State; extra_width = 0)
         n_braces!(x, s, extra_width = extra_width)
     elseif x.typ === CSTParser.InvisBrackets
         n_invisbrackets!(x, s, extra_width = extra_width)
+    elseif x.typ === CSTParser.UnaryOpCall && x.nodes[2].typ === CSTParser.OPERATOR
+        nest!(x.nodes[1], s, extra_width = extra_width + length(x.nodes[2]))
+        nest!(x.nodes[2], s)
+    elseif x.typ === CSTParser.Do
+        n_do!(x, s, extra_width = extra_width)
     else
-        for (i, n) in enumerate(x.nodes)
-            if n.typ === NEWLINE && x.nodes[i+1].typ === CSTParser.Block
-                s.line_offset = x.nodes[i+1].indent
-            elseif n.typ === NEWLINE
-                s.line_offset = x.indent
-            elseif n.typ === NOTCODE && x.nodes[i+1].typ === CSTParser.Block
-                s.line_offset = x.nodes[i+1].indent
-            elseif is_leaf(n)
-                s.line_offset += length(n)
-            else
-                nest!(n, s, extra_width = extra_width)
-            end
-        end
+        nest!(x.nodes, s, x.indent, extra_width = extra_width)
     end
 end
 
+function n_do!(x, s; extra_width = 0)
+    ew = sum(length.(x.nodes[2:3]))
+    # make sure there are nodes after "do"
+    if x.nodes[4].typ === WHITESPACE
+        ew += length(x.nodes[4])
+        ew += length(x.nodes[5])
+    end
+    nest!(x.nodes[1], s, extra_width = extra_width + ew)
+    nest!(x.nodes[2:end], s, x.indent, extra_width = extra_width)
+end
+
 function n_invisbrackets!(x, s; extra_width = 0)
     for (i, n) in enumerate(x.nodes)
         if n.typ === NEWLINE && x.nodes[i+1].typ === CSTParser.Block
@@ -190,6 +202,10 @@ function n_tuple!(x, s; extra_width = 0)
             elseif n.typ === PLACEHOLDER
                 x.nodes[i] = Newline()
                 s.line_offset = x.indent
+            elseif n.typ === TRAILINGCOMMA
+                x.nodes[i].val = ","
+                x.nodes[i].len = 1
+                nest!(x.nodes[i], s)
             elseif opener && (i == 1 || i == length(x.nodes))
                 nest!(n, s)
             else
@@ -230,7 +246,7 @@ n_params!(x, s; extra_width = 0) = n_tuple!(x, s, extra_width = extra_width)
 function n_call!(x, s; extra_width = 0)
     line_width = s.line_offset + length(x) + extra_width
     idx = findlast(n -> n.typ === PLACEHOLDER, x.nodes)
-    # @info "ENTERING" x.typ s.line_offset length(x) extra_width
+    # @info "ENTERING" x.typ s.line_offset length(x) extra_width s.margin
     if idx !== nothing && (line_width > s.margin || x.force_nest)
         x.nodes[end].indent = x.indent
         line_offset = s.line_offset
@@ -251,11 +267,17 @@ function n_call!(x, s; extra_width = 0)
             elseif n.typ === PLACEHOLDER
                 x.nodes[i] = Newline()
                 s.line_offset = x.indent
+            elseif n.typ === TRAILINGCOMMA
+                x.nodes[i].val = ","
+                x.nodes[i].len = 1
+                nest!(x.nodes[i], s)
             elseif i == 1 || i == length(x.nodes)
+                n.typ === CSTParser.Parameters && (n.force_nest = true)
                 nest!(n, s, extra_width = 1)
             else
                 diff = x.indent - x.nodes[i].indent
                 add_indent!(n, s, diff)
+                n.typ === CSTParser.Parameters && (n.force_nest = true)
                 nest!(n, s, extra_width = 1)
             end
         end
@@ -295,10 +317,11 @@ function n_wherecall!(x, s; extra_width = 0)
             x.nodes[end].indent = x.indent
         end
 
-        over = s.line_offset + Blen + extra_width > s.margin
+        over = (s.line_offset + Blen + extra_width > s.margin) || x.force_nest
         # line_offset = s.line_offset
         x.indent += s.indent_size
 
+        last_typ::Union{CSTParser.Head,Nothing} = nothing
         for (i, n) in enumerate(x.nodes[idx+1:end])
             if n.typ === NEWLINE
                 s.line_offset = x.indent
@@ -308,22 +331,33 @@ function n_wherecall!(x, s; extra_width = 0)
                     x.nodes[end].indent = s.line_offset
                 end
                 nest!(n, s)
-            elseif n.typ === PLACEHOLDER && (over || x.force_nest)
+            elseif n.typ === PLACEHOLDER && over
                 x.nodes[i+idx] = Newline()
                 s.line_offset = x.indent
+            elseif n.typ === TRAILINGCOMMA && over
+                x.nodes[i+idx].val = ","
+                x.nodes[i+idx].len = 1
+                nest!(x.nodes[i+idx], s)
             elseif has_braces
                 nest!(n, s, extra_width = 1 + extra_width)
             else
                 nest!(n, s, extra_width = extra_width)
             end
+            (!is_leaf(n) || n.typ === CSTParser.IDENTIFIER) && (last_typ = n.typ)
         end
 
+        # @info "" s.line_offset x.typ extra_width has_braces
+
         # Properly reset line offset in the case the last
         # argument is an IDENTIFIER.
-        if over && has_braces && x.nodes[end-2].typ === CSTParser.IDENTIFIER
+        # if over && has_braces && last_typ === CSTParser.IDENTIFIER && x.nodes[end-2].typ === TRAILINGCOMMA
+        if over &&
+           has_braces &&
+           last_typ === CSTParser.IDENTIFIER && x.nodes[end-2].typ === TRAILINGCOMMA
             s.line_offset = x.nodes[end].indent + 1
         end
-    # @debug "" s.line_offset x.typ extra_width
+
+        # @info "" s.line_offset x.typ extra_width has_braces
     else
         nest!(x.nodes, s, x.indent, extra_width = extra_width)
     end
diff --git a/src/pretty.jl b/src/pretty.jl
index ae2ddd9a..877eb9ec 100644
--- a/src/pretty.jl
+++ b/src/pretty.jl
@@ -1,6 +1,15 @@
 # Creates a _prettified_ version of a CST.
 
-@enum(PLeaf, NEWLINE, SEMICOLON, WHITESPACE, PLACEHOLDER, NOTCODE, INLINECOMMENT)
+@enum(
+    PLeaf,
+    NEWLINE,
+    SEMICOLON,
+    WHITESPACE,
+    PLACEHOLDER,
+    NOTCODE,
+    INLINECOMMENT,
+    TRAILINGCOMMA,
+)
 
 mutable struct PTree
     typ::Union{CSTParser.Head,PLeaf}
@@ -14,15 +23,20 @@ mutable struct PTree
     force_nest::Bool
 end
 
-PTree(x::CSTParser.EXPR, indent::Int) =
+PTree(x::CSTParser.EXPR, indent::Integer) =
     PTree(x.typ, -1, -1, indent, 0, nothing, PTree[], Ref(x), false)
 
-function PTree(x::CSTParser.EXPR, startline::Int, endline::Int, val::AbstractString)
+function PTree(x::CSTParser.EXPR, startline::Integer, endline::Integer, val::AbstractString)
     PTree(x.typ, startline, endline, 0, length(val), val, nothing, Ref(x), false)
 end
 
+function PTree(x::CSTParser.Head, startline::Integer, endline::Integer, val::AbstractString)
+    PTree(x, startline, endline, 0, length(val), val, nothing, nothing, false)
+end
+
 Newline() = PTree(NEWLINE, -1, -1, 0, 0, "\n", nothing, nothing, false)
 Semicolon() = PTree(SEMICOLON, -1, -1, 0, 1, ";", nothing, nothing, false)
+TrailingComma() = PTree(TRAILINGCOMMA, -1, -1, 0, 0, "", nothing, nothing, false)
 Whitespace(n) = PTree(WHITESPACE, -1, -1, 0, n, " "^n, nothing, nothing, false)
 Placeholder(n) = PTree(PLACEHOLDER, -1, -1, 0, n, " "^n, nothing, nothing, false)
 Notcode(startline, endline) =
@@ -36,7 +50,9 @@ empty_start(x::PTree) = x.startline == 1 && x.endline == 1 && x.val == ""
 
 is_punc(x) = CSTParser.ispunctuation(x)
 is_end(x) = x.typ === CSTParser.KEYWORD && x.val == "end"
-is_colon(x::PTree) = x.typ === CSTParser.OPERATOR && x.val == ":"
+is_colon(x) = x.typ === CSTParser.OPERATOR && x.val == ":"
+is_comma(x::PTree) =
+    (x.typ === CSTParser.PUNCTUATION && x.val == ",") || x.typ === TRAILINGCOMMA
 is_comment(x::PTree) = x.typ === INLINECOMMENT || x.typ === NOTCODE
 
 is_colon_op(x) =
@@ -57,13 +73,39 @@ function parent_is(x, typs...)
 end
 
 function add_node!(t::PTree, n::PTree, s::State; join_lines = false, max_padding = -1)
-    if n.typ isa PLeaf
-        t.len += length(n)
-        # Don't want to alter the startline/endline of these types
-        if n.typ !== NOTCODE && n.typ !== INLINECOMMENT
+    if n.typ === SEMICOLON
+        join_lines = true
+        loc = s.offset > length(s.doc.text) && t.typ === CSTParser.TopLevel ?
+              loc = cursor_loc(s, s.offset - 1) : cursor_loc(s)
+        for l = t.endline:loc[1]
+            if has_semicolon(s.doc, l)
+                # @info "found semicolon" l
+                n.startline = l
+                n.endline = l
+                break
+            end
+        end
+        # @info "" t.endline n.endline loc[1]
+
+        # If there's no semicolon, treat it
+        # as a PLeaf
+        if n.startline == -1
+            t.len += length(n)
             n.startline = t.startline
             n.endline = t.endline
+            push!(t.nodes, n)
+            return
         end
+    elseif n.typ === TRAILINGCOMMA && is_comma(t.nodes[end])
+        t.nodes[end] = n
+        return
+    elseif n.typ === NOTCODE || n.typ === INLINECOMMENT
+        push!(t.nodes, n)
+        return
+    elseif n.typ isa PLeaf
+        t.len += length(n)
+        n.startline = t.startline
+        n.endline = t.endline
         push!(t.nodes, n)
         return
     end
@@ -84,7 +126,7 @@ function add_node!(t::PTree, n::PTree, s::State; join_lines = false, max_padding
     end
 
     if !is_prev_newline(t.nodes[end])
-        current_line = t.nodes[end].endline
+        current_line = t.endline
         notcode_startline = current_line + 1
         notcode_endline = n.startline - 1
         nt = t.nodes[end].typ
@@ -94,6 +136,10 @@ function add_node!(t::PTree, n::PTree, s::State; join_lines = false, max_padding
             # nesting is forced in an effort to preserve them.
             t.force_nest = true
 
+            # If the previous node type is WHITESPACE - reset it.
+            # This fixes cases similar to the one shown in issue #51.
+            nt === WHITESPACE && (t.nodes[end] = Whitespace(0))
+
             hs = hascomment(s.doc, current_line)
             hs && add_node!(t, InlineComment(current_line), s)
             if nt !== PLACEHOLDER
@@ -116,6 +162,10 @@ function add_node!(t::PTree, n::PTree, s::State; join_lines = false, max_padding
             idx = length(t.nodes)
             t.nodes[idx-1], t.nodes[idx] = t.nodes[idx], t.nodes[idx-1]
         end
+
+        if n.typ === CSTParser.Parameters && n.force_nest == true
+            t.force_nest = true
+        end
     end
 
     if n.startline < t.startline || t.startline == -1
@@ -309,7 +359,7 @@ function pretty(x::CSTParser.EXPR, s::State)
             pretty(a, s),
             s,
             join_lines = !is_fileh,
-            max_padding = is_fileh ? 0 : -1
+            max_padding = is_fileh ? 0 : -1,
         )
     end
     t
@@ -385,8 +435,13 @@ end
 function p_literal(x, s)
     loc = cursor_loc(s)
     if !is_str_or_cmd(x.kind)
+        val = x.val
+        if x.kind === Tokens.FLOAT && x.val[end] == '.'
+            # If a floating point ends in `.`, add trailing zero.
+            val *= '0'
+        end
         s.offset += x.fullspan
-        return PTree(x, loc[1], loc[1], x.val)
+        return PTree(x, loc[1], loc[1], val)
     end
 
     # Strings are unescaped by CSTParser
@@ -395,7 +450,7 @@ function p_literal(x, s)
     # So we'll just look at the source directly!
     str_info = get(s.doc.lit_strings, s.offset - 1, nothing)
 
-    # Tokenize treats the `ix` part of r"^(=?[^=]+)=(.*)$"ix as an 
+    # Tokenize treats the `ix` part of r"^(=?[^=]+)=(.*)$"ix as an
     # IDENTIFIER where as CSTParser parses it as a LITERAL.
     # An IDENTIFIER won't show up in the string literal lookup table.
     if str_info === nothing && x.parent.typ === CSTParser.x_Str
@@ -507,6 +562,7 @@ function p_macrocall(x, s)
             add_node!(t, n, s, join_lines = true)
             add_node!(t, Placeholder(0), s)
         elseif is_closer(n)
+            add_node!(t, TrailingComma(), s)
             add_node!(t, Placeholder(0), s)
             add_node!(t, n, s, join_lines = true)
         elseif CSTParser.is_comma(a) && i < length(x) && !is_punc(x.args[i+1])
@@ -617,7 +673,7 @@ function p_function(x, s)
                 t,
                 p_block(x.args[3], s, ignore_single_line = true),
                 s,
-                max_padding = s.indent_size
+                max_padding = s.indent_size,
             )
             s.indent -= s.indent_size
             add_node!(t, pretty(x.args[4], s), s)
@@ -646,7 +702,7 @@ function p_struct(x, s)
             t,
             p_block(x.args[3], s, ignore_single_line = true),
             s,
-            max_padding = s.indent_size
+            max_padding = s.indent_size,
         )
         s.indent -= s.indent_size
         add_node!(t, pretty(x.args[4], s), s)
@@ -671,7 +727,7 @@ function p_mutable(x, s)
             t,
             p_block(x.args[4], s, ignore_single_line = true),
             s,
-            max_padding = s.indent_size
+            max_padding = s.indent_size,
         )
         s.indent -= s.indent_size
         add_node!(t, pretty(x.args[5], s), s)
@@ -735,7 +791,7 @@ function p_begin(x, s)
             t,
             p_block(x.args[2], s, ignore_single_line = true),
             s,
-            max_padding = s.indent_size
+            max_padding = s.indent_size,
         )
         s.indent -= s.indent_size
         add_node!(t, pretty(x.args[3], s), s)
@@ -757,7 +813,7 @@ function p_quote(x, s)
                 t,
                 p_block(x.args[2], s, ignore_single_line = true),
                 s,
-                max_padding = s.indent_size
+                max_padding = s.indent_size,
             )
             s.indent -= s.indent_size
             add_node!(t, pretty(x.args[3], s), s)
@@ -792,7 +848,7 @@ function p_let(x, s)
             t,
             p_block(x.args[3], s, ignore_single_line = true),
             s,
-            max_padding = s.indent_size
+            max_padding = s.indent_size,
         )
         s.indent -= s.indent_size
         add_node!(t, pretty(x.args[end], s), s)
@@ -854,7 +910,7 @@ function p_loop(x, s)
         t,
         p_block(x.args[3], s, ignore_single_line = true),
         s,
-        max_padding = s.indent_size
+        max_padding = s.indent_size,
     )
     s.indent -= s.indent_size
     add_node!(t, pretty(x.args[4], s), s)
@@ -877,7 +933,7 @@ function p_do(x, s)
             t,
             p_block(x.args[4], s, ignore_single_line = true),
             s,
-            max_padding = s.indent_size
+            max_padding = s.indent_size,
         )
         s.indent -= s.indent_size
     end
@@ -898,7 +954,7 @@ function p_try(x, s)
                 t,
                 p_block(a, s, ignore_single_line = true),
                 s,
-                max_padding = s.indent_size
+                max_padding = s.indent_size,
             )
             s.indent -= s.indent_size
         else
@@ -925,7 +981,7 @@ function p_if(x, s)
             t,
             p_block(x.args[3], s, ignore_single_line = true),
             s,
-            max_padding = s.indent_size
+            max_padding = s.indent_size,
         )
         s.indent -= s.indent_size
 
@@ -945,7 +1001,7 @@ function p_if(x, s)
                     t,
                     p_block(x.args[5], s, ignore_single_line = true),
                     s,
-                    max_padding = s.indent_size
+                    max_padding = s.indent_size,
                 )
                 s.indent -= s.indent_size
             end
@@ -962,7 +1018,7 @@ function p_if(x, s)
             t,
             p_block(x.args[2], s, ignore_single_line = true),
             s,
-            max_padding = s.indent_size
+            max_padding = s.indent_size,
         )
         s.indent -= s.indent_size
 
@@ -983,7 +1039,7 @@ function p_if(x, s)
                     t,
                     p_block(x.args[4], s, ignore_single_line = true),
                     s,
-                    max_padding = s.indent_size
+                    max_padding = s.indent_size,
                 )
                 s.indent -= s.indent_size
             end
@@ -1105,7 +1161,7 @@ function p_binarycall(x, s; nonest = false, nospace = false)
     narg2 && (t.force_nest = true)
     nest = (nestable(x) && !nonest) || narg2
 
-    if op.fullspan == 0
+    if op.fullspan == 0 && x.args[3].typ === CSTParser.IDENTIFIER
         # do nothing
     elseif (CSTParser.precedence(op) in (8, 13, 14, 16) && op.kind !== Tokens.ANON_FUNC) ||
            nospace
@@ -1155,28 +1211,41 @@ function p_wherecall(x, s)
     add_node!(t, Placeholder(0), s)
 
     multi_arg = length(CSTParser.get_where_params(x)) > 1
-    in_braces = CSTParser.is_lbrace(x.args[3])
+    add_braces = !CSTParser.is_lbrace(x.args[3]) &&
+                 x.parent.typ !== CSTParser.Curly && x.args[3].typ !== CSTParser.Curly
+    add_braces && add_node!(
+        t,
+        PTree(CSTParser.PUNCTUATION, t.endline, t.endline, "{"),
+        s,
+        join_lines = true,
+    )
 
     # @debug "" multi_arg in_braces x.args[3].val == "{" x.args[end].val
-
-    for a in x.args[3:end]
+    for (i, a) in enumerate(x.args[3:end])
         if is_opener(a) && multi_arg
             add_node!(t, pretty(a, s), s, join_lines = true)
             add_node!(t, Placeholder(0), s)
             s.indent += s.indent_size
         elseif is_closer(a) && multi_arg
+            add_node!(t, TrailingComma(), s)
             add_node!(t, Placeholder(0), s)
             add_node!(t, pretty(a, s), s, join_lines = true)
             s.indent -= s.indent_size
-        elseif CSTParser.is_comma(a)
+        elseif CSTParser.is_comma(a) && !is_punc(x.args[i+3])
             add_node!(t, pretty(a, s), s, join_lines = true)
             add_node!(t, Placeholder(0), s)
-        elseif in_braces && a.typ === CSTParser.BinaryOpCall
+        elseif a.typ === CSTParser.BinaryOpCall
             add_node!(t, p_binarycall(a, s, nospace = true), s, join_lines = true)
         else
             add_node!(t, pretty(a, s), s, join_lines = true)
         end
     end
+    add_braces && add_node!(
+        t,
+        PTree(CSTParser.PUNCTUATION, t.endline, t.endline, "}"),
+        s,
+        join_lines = true,
+    )
     t
 end
 
@@ -1218,9 +1287,10 @@ function p_curly(x, s)
 
     for (i, a) in enumerate(x.args[3:end])
         if i + 2 == length(x) && multi_arg
+            add_node!(t, TrailingComma(), s)
             add_node!(t, Placeholder(0), s)
             add_node!(t, pretty(a, s), s, join_lines = true)
-        elseif CSTParser.is_comma(a) && i < length(x) - 3 && !is_punc(x.args[i+1])
+        elseif CSTParser.is_comma(a) && i < length(x) - 3 && !is_punc(x.args[i+3])
             add_node!(t, pretty(a, s), s, join_lines = true)
             add_node!(t, Placeholder(0), s)
         else
@@ -1243,9 +1313,15 @@ function p_call(x, s)
 
     for (i, a) in enumerate(x.args[3:end])
         if i + 2 == length(x) && multi_arg
+            pn = x.args[i+1]
+            if pn.typ !== CSTParser.Parameters
+                add_node!(t, TrailingComma(), s)
+            elseif pn.typ === CSTParser.Parameters && !CSTParser.is_comma(pn.args[end])
+                add_node!(t, TrailingComma(), s)
+            end
             add_node!(t, Placeholder(0), s)
             add_node!(t, pretty(a, s), s, join_lines = true)
-        elseif CSTParser.is_comma(a) && i < length(x) - 3 && !is_punc(x.args[i+1])
+        elseif CSTParser.is_comma(a) && i < length(x) - 3 && !is_punc(x.args[i+3])
             add_node!(t, pretty(a, s), s, join_lines = true)
             add_node!(t, Placeholder(1), s)
         else
@@ -1268,14 +1344,14 @@ function p_invisbrackets(x, s; nonest = false, nospace = false)
                 t,
                 p_binarycall(a, s, nonest = nonest, nospace = nospace),
                 s,
-                join_lines = true
+                join_lines = true,
             )
         elseif a.typ === CSTParser.InvisBrackets
             add_node!(
                 t,
                 p_invisbrackets(a, s, nonest = nonest, nospace = nospace),
                 s,
-                join_lines = true
+                join_lines = true,
             )
         elseif is_opener(a) && multi_arg
             add_node!(t, pretty(a, s), s, join_lines = true)
@@ -1309,6 +1385,7 @@ function p_tuple(x, s)
             add_node!(t, n, s, join_lines = true)
             add_node!(t, Placeholder(0), s)
         elseif is_closer(n) && multi_arg
+            add_node!(t, TrailingComma(), s)
             add_node!(t, Placeholder(0), s)
             add_node!(t, n, s, join_lines = true)
         elseif CSTParser.is_comma(a) && i < length(x) && !is_punc(x.args[i+1])
@@ -1334,6 +1411,7 @@ function p_braces(x, s)
             add_node!(t, n, s, join_lines = true)
             add_node!(t, Placeholder(0), s)
         elseif i == length(x) && multi_arg
+            add_node!(t, TrailingComma(), s)
             add_node!(t, Placeholder(0), s)
             add_node!(t, n, s, join_lines = true)
         elseif CSTParser.is_comma(a) && i < length(x) && !is_punc(x.args[i+1])
@@ -1358,6 +1436,7 @@ function p_vect(x, s)
             add_node!(t, n, s, join_lines = true)
             add_node!(t, Placeholder(0), s)
         elseif i == length(x) && multi_arg
+            add_node!(t, TrailingComma(), s)
             add_node!(t, Placeholder(0), s)
             add_node!(t, n, s, join_lines = true)
         elseif CSTParser.is_comma(a) && i < length(x) && !is_punc(x.args[i+1])
@@ -1408,23 +1487,31 @@ end
 # Ref
 function p_ref(x, s)
     t = PTree(x, nspaces(s))
+    multi_arg = length(x) > 5
     for (i, a) in enumerate(x)
-        if CSTParser.is_comma(a)
+        if is_closer(a) && multi_arg
+            add_node!(t, TrailingComma(), s)
+            add_node!(t, Placeholder(0), s)
             add_node!(t, pretty(a, s), s, join_lines = true)
-            add_node!(t, Whitespace(1), s)
+        elseif is_opener(a) && multi_arg
+            add_node!(t, pretty(a, s), s, join_lines = true)
+            add_node!(t, Placeholder(0), s)
+        elseif CSTParser.is_comma(a) && i < length(x) && !is_punc(x.args[i+1])
+            add_node!(t, pretty(a, s), s, join_lines = true)
+            add_node!(t, Placeholder(1), s)
         elseif a.typ === CSTParser.BinaryOpCall
             add_node!(
                 t,
                 p_binarycall(a, s, nonest = true, nospace = true),
                 s,
-                join_lines = true
+                join_lines = true,
             )
         elseif a.typ === CSTParser.InvisBrackets
             add_node!(
                 t,
                 p_invisbrackets(a, s, nonest = true, nospace = true),
                 s,
-                join_lines = true
+                join_lines = true,
             )
         else
             add_node!(t, pretty(a, s), s, join_lines = true)
diff --git a/src/print.jl b/src/print.jl
index 25c74c34..d3eae596 100644
--- a/src/print.jl
+++ b/src/print.jl
@@ -46,26 +46,24 @@ function print_tree(io::IOBuffer, x::PTree, s::State)
 end
 
 @inline function print_notcode(io, x, s)
-    prev_nl = true
     for l = x.startline:x.endline
-        v = getline(s.doc, l)
+        ws, v = get(s.doc.comments, l, (0, "\n"))
         v == "" && continue
-        # @info "comment line" l v
         if l == x.endline && v[end] == '\n'
             v = v[1:prevind(v, end)]
         end
+        ws > 0 && write(io, repeat(" ", ws))
         write(io, v)
-        prev_nl = v == "\n" ? true : false
+        if l != x.endline && v[end] != '\n'
+            write(io, "\n")
+        end
     end
 end
 
 @inline function print_inlinecomment(io, x, s)
-    v = get(s.doc.comments, x.startline, "")
+    ws, v = get(s.doc.comments, x.startline, (0, ""))
     isempty(v) && return
-    v = getline(s.doc, x.startline)
-    idx = findlast(c -> c == '#', v)
-    idx === nothing && return
-    idx = findlast(c -> !isspace(c), v[1:prevind(v, idx)])
-    v = v[end] == '\n' ? v[nextind(v, idx):prevind(v, end)] : v[nextind(v, idx):end]
+    v = v[end] == '\n' ? v[nextind(v, 1):prevind(v, end)] : v
+    ws > 0 && write(io, repeat(" ", ws))
     write(io, v)
 end


Test patch:
diff --git a/test/files/Docs.jl b/test/files/Docs.jl
new file mode 100644
index 00000000..c59be3b6
--- /dev/null
+++ b/test/files/Docs.jl
@@ -0,0 +1,640 @@
+# This file is a part of Julia. License is MIT: https://julialang.org/license
+
+"""
+    Docs
+
+The `Docs` module provides the `@doc` macro which can be used to set and retrieve
+documentation metadata for Julia objects.
+
+Please see the manual section on documentation for more
+information.
+"""
+module Docs
+
+@nospecialize # don't specialize on any arguments of the methods declared herein
+
+"""
+# Documentation
+
+Functions, methods and types can be documented by placing a string before the definition:
+
+    \"""
+    # The Foo Function
+    `foo(x)`: Foo the living hell out of `x`.
+    \"""
+    foo(x) = ...
+
+The `@doc` macro can be used directly to both set and retrieve documentation / metadata. By
+default, documentation is written as Markdown, but any object can be placed before the
+arrow. For example:
+
+    @doc "blah" ->
+    function foo() ...
+
+The `->` is not required if the object is on the same line, e.g.
+
+    @doc "foo" foo
+
+## Documenting objects after they are defined
+You can document an object after its definition by
+
+    @doc "foo" function_to_doc
+    @doc "bar" TypeToDoc
+
+For macros, the syntax is `@doc "macro doc" :(@Module.macro)` or `@doc "macro doc"
+:(string_macro"")` for string macros. Without the quote `:()` the expansion of the macro
+will be documented.
+
+## Retrieving Documentation
+You can retrieve docs for functions, macros and other objects as follows:
+
+    @doc foo
+    @doc @time
+    @doc md""
+
+## Functions & Methods
+Placing documentation before a method definition (e.g. `function foo() ...` or `foo() = ...`)
+will cause that specific method to be documented, as opposed to the whole function. Method
+docs are concatenated together in the order they were defined to provide docs for the
+function.
+"""
+:(Core.@doc)
+
+include("bindings.jl")
+
+import .Base.Meta: quot, isexpr
+import .Base: Callable, with_output_color
+using .Base: RefValue
+import ..CoreDocs: lazy_iterpolate
+
+export doc
+
+# Basic API / Storage
+
+const modules = Module[]
+const META = gensym(:meta)
+
+meta(m::Module) = isdefined(m, META) ? getfield(m, META) : IdDict()
+
+function initmeta(m::Module)
+    if !isdefined(m, META)
+        Core.eval(m, :(const $META = $(IdDict())))
+        push!(modules, m)
+    end
+    nothing
+end
+
+function signature!(tv, expr::Expr)
+    is_macrocall = isexpr(expr, :macrocall)
+    if is_macrocall || isexpr(expr, :call)
+        sig = :(Union{Tuple{}})
+        first_arg = is_macrocall ? 3 : 2 # skip function arguments
+        for arg in expr.args[first_arg:end]
+            isexpr(arg, :parameters) && continue
+            if isexpr(arg, :kw) # optional arg
+                push!(sig.args, :(Tuple{$(sig.args[end].args[2:end]...)}))
+            end
+            push!(sig.args[end].args, argtype(arg))
+        end
+        if isexpr(expr.args[1], :curly) && isempty(tv)
+            append!(tv, tvar.(expr.args[1].args[2:end]))
+        end
+        for i = length(tv):-1:1
+            push!(sig.args, :(Tuple{$(tv[i].args[1])}))
+        end
+        for i = length(tv):-1:1
+            sig = Expr(:where, sig, tv[i])
+        end
+        return sig
+    elseif isexpr(expr, :where)
+        append!(tv, tvar.(expr.args[2:end]))
+        return signature!(tv, expr.args[1])
+    else
+        return signature!(tv, expr.args[1])
+    end
+end
+signature!(tv, @nospecialize(other)) = :(Union{})
+signature(expr::Expr) = signature!([], expr)
+signature(@nospecialize other) = signature!([], other)
+
+function argtype(expr::Expr)
+    isexpr(expr, :(::)) && return expr.args[end]
+    isexpr(expr, :(...)) && return :(Vararg{$(argtype(expr.args[1]))})
+    return argtype(expr.args[1])
+end
+argtype(@nospecialize other) = :Any
+
+tvar(x::Expr) = x
+tvar(s::Symbol) = :($s <: Any)
+
+# Docsystem types.
+# ================
+
+"""
+    Docs.DocStr
+
+Stores the contents of a single docstring as well as related metadata.
+
+Both the raw text, `.text`, and the parsed markdown, `.object`, are tracked by this type.
+Parsing of the raw text is done lazily when a request is made to render the docstring,
+which helps to reduce total precompiled image size.
+
+The `.data` fields stores several values related to the docstring, such as: path,
+linenumber, source code, and fielddocs.
+"""
+mutable struct DocStr
+    text::Core.SimpleVector
+    object::Any
+    data::Dict{Symbol,Any}
+end
+
+function docstr(binding::Binding, typesig = Union{})
+    @nospecialize typesig
+    for m in modules
+        dict = meta(m)
+        if haskey(dict, binding)
+            docs = dict[binding].docs
+            if haskey(docs, typesig)
+                return docs[typesig]
+            end
+        end
+    end
+    error("could not find matching docstring for '$binding :: $typesig'.")
+end
+docstr(object, data = Dict()) = _docstr(object, data)
+
+_docstr(vec::Core.SimpleVector, data) = DocStr(vec, nothing, data)
+_docstr(str::AbstractString, data) = DocStr(Core.svec(str), nothing, data)
+_docstr(object, data) = DocStr(Core.svec(), object, data)
+
+_docstr(doc::DocStr, data) = (doc.data = merge(data, doc.data); doc)
+
+macro ref(x)
+    binding = bindingexpr(namify(x))
+    typesig = signature(x)
+    return esc(docexpr(__source__, __module__, binding, typesig))
+end
+
+docexpr(__source__, __module__, args...) = Expr(:call, docstr, args...)
+
+"""
+    MultiDoc
+
+Stores a collection of docstrings for related objects, ie. a `Function`/`DataType` and
+associated `Method` objects.
+
+Each documented object in a `MultiDoc` is referred to by it's signature which is represented
+by a `Union` of `Tuple` types. For example, the following `Method` definition
+
+    f(x, y) = ...
+
+is stored as `Tuple{Any, Any}` in the `MultiDoc` while
+
+    f(x::T, y = ?) where {T} = ...
+
+is stored as `Union{Tuple{T, Any}, Tuple{T}} where T`.
+
+Note: The `Function`/`DataType` object's signature is always `Union{}`.
+"""
+mutable struct MultiDoc
+    "Ordered (via definition order) vector of object signatures."
+    order::Vector{Type}
+    "Documentation for each object. Keys are signatures."
+    docs::IdDict{Any,Any}
+
+    MultiDoc() = new(Type[], IdDict())
+end
+
+# Docstring registration.
+# =======================
+
+"""
+    Docs.doc!(__module__, binding, str, sig)
+
+Adds a new docstring `str` to the docsystem of `__module__` for `binding` and signature `sig`.
+"""
+function doc!(__module__::Module, b::Binding, str::DocStr, @nospecialize sig = Union{})
+    initmeta(__module__)
+    m = get!(meta(__module__), b, MultiDoc())
+    if haskey(m.docs, sig)
+        # We allow for docstrings to be updated, but print a warning since it is possible
+        # that over-writing a docstring *may* have been accidental.  The warning
+        # is suppressed for symbols in Main, for interactive use (#23011).
+        __module__ == Main || @warn "Replacing docs for `$b :: $sig` in module `$(__module__)`"
+    else
+        # The ordering of docstrings for each Binding is defined by the order in which they
+        # are initially added. Replacing a specific docstring does not change it's ordering.
+        push!(m.order, sig)
+    end
+    m.docs[sig] = str
+    str.data[:binding] = b
+    str.data[:typesig] = sig
+    return b
+end
+
+# Docstring lookup.
+# =================
+
+"""
+    getdoc(obj)
+    getdoc(obj, sig)
+
+Return a custom docstring object associated with the object `obj` and, optionally, the tuple
+type signature `sig`. See `MultiDoc` docs for an explanation of the possible values of `sig`.
+
+The returned object can either be a markdown object generated by `Markdown.parse` or some
+other custom type used to display non-markdown formatted documentation.
+
+A return value of `nothing` can be used to signify to the docsystem that no documentation
+was found for `obj`, in which case the docsystem will fall back to searching for the
+`Binding` associated with `obj` instead.
+"""
+function getdoc end
+
+getdoc(@nospecialize(x), @nospecialize(sig)) = getdoc(x)
+getdoc(@nospecialize(x)) = nothing
+
+# Utilities.
+# ==========
+
+"""
+`catdoc(xs...)`: Combine the documentation metadata `xs` into a single meta object.
+"""
+catdoc() = nothing
+catdoc(xs...) = vcat(xs...)
+
+const keywords = Dict{Symbol,DocStr}()
+
+function unblock(@nospecialize ex)
+    isexpr(ex, :block) || return ex
+    exs = filter(ex -> !(isa(ex, LineNumberNode) || isexpr(ex, :line)), ex.args)
+    length(exs) == 1 || return ex
+    return unblock(exs[1])
+end
+
+uncurly(@nospecialize ex) = isexpr(ex, :curly) ? ex.args[1] : ex
+
+namify(@nospecialize x) = astname(x, isexpr(x, :macro))
+
+function astname(x::Expr, ismacro::Bool)
+    if isexpr(x, :.)
+        ismacro ? macroname(x) : x
+    # Call overloading, e.g. `(a::A)(b) = b` or `function (a::A)(b) b end` should document `A(b)`
+    elseif (isexpr(x, :function) || isexpr(x, :(=))) &&
+           isexpr(x.args[1], :call) && isexpr(x.args[1].args[1], :(::))
+        return astname(x.args[1].args[1].args[end], ismacro)
+    else
+        n = isexpr(x, (:module, :struct)) ? 2 : 1
+        astname(x.args[n], ismacro)
+    end
+end
+astname(q::QuoteNode, ismacro::Bool) = astname(q.value, ismacro)
+astname(s::Symbol, ismacro::Bool) = ismacro ? macroname(s) : s
+astname(@nospecialize(other), ismacro::Bool) = other
+
+macroname(s::Symbol) = Symbol('@', s)
+macroname(x::Expr) = Expr(x.head, x.args[1], macroname(x.args[end].value))
+
+isfield(@nospecialize x) =
+    isexpr(x, :.) &&
+    (isa(x.args[1], Symbol) || isfield(x.args[1])) &&
+    (isa(x.args[2], QuoteNode) || isexpr(x.args[2], :quote))
+
+# @doc expression builders.
+# =========================
+
+"""
+    Docs.metadata(source, module, expr, ismodule)
+
+Build a `Dict` expression containing metadata captured from the expression `expr`.
+
+Fields that may be included in the returned `Dict`:
+
+- `:path`:       Symbol representing the file where `expr` is defined.
+- `:linenumber`: Linenumber where `expr` is defined.
+- `:module`:     Module where the docstring is defined.
+- `:fields`:     `Dict` of all field docs found in `expr`. Only for concrete types.
+"""
+function metadata(__source__, __module__, expr, ismodule)
+    args = []
+    # Filename and linenumber of the docstring.
+    __file__ = isa(__source__.file, Symbol) ? String(__source__.file) : ""
+    push!(args, Pair(:path, __file__))
+    push!(args, Pair(:linenumber, __source__.line))
+    # Module in which the docstring is defined.
+    if ismodule # Module docs go inside the module with name `expr`
+        push!(args, :($Pair(:module, $expr)))
+    else
+        push!(args, Pair(:module, __module__))
+    end
+    if isexpr(expr, :struct)
+        # Field docs for concrete types.
+        fields = []
+        last_docstr = nothing
+        for each in expr.args[3].args
+            if isa(each, Symbol) || isexpr(each, :(::))
+                # a field declaration
+                if last_docstr !== nothing
+                    push!(fields, (namify(each), last_docstr))
+                    last_docstr = nothing
+                end
+            elseif isexpr(each, :function) || isexpr(each, :(=))
+                break
+            elseif isa(each, String) ||
+                   isexpr(each, :string) ||
+                   isexpr(each, :call) ||
+                   (isexpr(each, :macrocall) && each.args[1] === Symbol("@doc_str"))
+                # forms that might be doc strings
+                last_docstr = each
+            end
+        end
+        dict = :($(Dict)($([:($(Pair)($(quot(f)), $d)) for (f, d) in fields]...)))
+        push!(args, :($(Pair)(:fields, $dict)))
+    end
+    return :($(Dict)($(args...)))
+end
+
+function keyworddoc(__source__, __module__, str, def::Base.BaseDocs.Keyword)
+    @nospecialize str
+    docstr = esc(docexpr(
+        __source__,
+        __module__,
+        lazy_iterpolate(str),
+        metadata(__source__, __module__, def, false),
+    ))
+    return :($setindex!($(keywords), $docstr, $(esc(quot(def.name)))); nothing)
+end
+
+function objectdoc(__source__, __module__, str, def, expr, sig = :(Union{}))
+    @nospecialize str def expr sig
+    binding = esc(bindingexpr(namify(expr)))
+    docstr = esc(docexpr(
+        __source__,
+        __module__,
+        lazy_iterpolate(str),
+        metadata(__source__, __module__, expr, false),
+    ))
+    # Note: we want to avoid introducing line number nodes here (issue #24468)
+    return Expr(:block, esc(def), :($(doc!)($__module__, $binding, $docstr, $(esc(sig)))))
+end
+
+function calldoc(__source__, __module__, str, def::Expr)
+    @nospecialize str
+    args = def.args[2:end]
+    if isempty(args) || all(validcall, args)
+        objectdoc(__source__, __module__, str, nothing, def, signature(def))
+    else
+        docerror(def)
+    end
+end
+validcall(x) = isa(x, Symbol) || isexpr(x, (:(::), :..., :kw, :parameters))
+
+function moduledoc(__source__, __module__, meta, def, def′::Expr)
+    @nospecialize meta def
+    name = namify(def′)
+    docex = Expr(
+        :call,
+        doc!,
+        name,
+        bindingexpr(name),
+        docexpr(
+            __source__,
+            name,
+            lazy_iterpolate(meta),
+            metadata(__source__, __module__, name, true),
+        ),
+    )
+    if def === nothing
+        esc(:(Core.eval($name, $(quot(docex)))))
+    else
+        def = unblock(def)
+        block = def.args[3].args
+        if !def.args[1]
+            isempty(block) && error("empty baremodules are not documentable.")
+            insert!(block, 2, :(import Base: @doc))
+        end
+        push!(block, docex)
+        esc(Expr(:toplevel, def))
+    end
+end
+
+# Shares a single doc, `meta`, between several expressions from the tuple expression `ex`.
+function multidoc(__source__, __module__, meta, ex::Expr, define::Bool)
+    @nospecialize meta
+    out = Expr(:block)
+    str = docexpr(
+        __source__,
+        __module__,
+        lazy_iterpolate(meta),
+        metadata(__source__, __module__, ex, false),
+    )
+    ref = RefValue{DocStr}()
+    first = true
+    for arg in ex.args
+        # The first `arg` to be documented needs to also create the docstring for the group
+        # (after doing the action defined by the argument).
+        # Subsequent `arg`s just need `ref` to be able to find the docstring without having
+        # to create an entirely new one each.
+        if first
+            first = false
+            docstr = :($getindex($setindex!($(ref), $str)))
+        else
+            docstr = :($getindex($(ref)))
+        end
+        push!(out.args, docm(__source__, __module__, docstr, arg, define))
+    end
+    return out
+end
+
+"""
+    @__doc__(ex)
+
+Low-level macro used to mark expressions returned by a macro that should be documented. If
+more than one expression is marked then the same docstring is applied to each expression.
+
+    macro example(f)
+        quote
+            \$(f)() = 0
+            @__doc__ \$(f)(x) = 1
+            \$(f)(x, y) = 2
+        end |> esc
+    end
+
+`@__doc__` has no effect when a macro that uses it is not documented.
+"""
+:(Core.@__doc__)
+
+function __doc__!(meta, def, define::Bool)
+    @nospecialize meta def
+    # Two cases must be handled here to avoid redefining all definitions contained in `def`:
+    if define
+        # `def` has not been defined yet (this is the common case, i.e. when not generating
+        # the Base image). We just need to convert each `@__doc__` marker to an `@doc`.
+        finddoc(def) do each
+            each.head = :macrocall
+            each.args = [Symbol("@doc"), nothing, meta, each.args[end], define] # TODO: forward line number info
+        end
+    else
+        # `def` has already been defined during Base image gen so we just need to find and
+        # document any subexpressions marked with `@__doc__`.
+        docs = []
+        found = finddoc(def) do each
+            push!(docs, :(@doc($meta, $(each.args[end]), $define)))
+        end
+        # If any subexpressions have been documented then replace the entire expression with
+        # just those documented subexpressions to avoid redefining any definitions.
+        if found
+            def.head = :toplevel
+            def.args = docs
+        end
+        found
+    end
+end
+# Walk expression tree `def` and call `λ` when any `@__doc__` markers are found. Returns
+# `true` to signify that at least one `@__doc__` has been found, and `false` otherwise.
+function finddoc(λ, def::Expr)
+    if isexpr(def, :block, 2) &&
+       isexpr(def.args[1], :meta, 1) && def.args[1].args[1] === :doc
+        # Found the macroexpansion of an `@__doc__` expression.
+        λ(def)
+        true
+    else
+        found = false
+        for each in def.args
+            found |= finddoc(λ, each)
+        end
+        found
+    end
+end
+finddoc(λ, @nospecialize def) = false
+
+# Predicates and helpers for `docm` expression selection:
+
+const FUNC_HEADS = [:function, :macro, :(=)]
+const BINDING_HEADS = [:const, :global, :(=)]
+# For the special `:@mac` / `:(Base.@mac)` syntax for documenting a macro after definition.
+isquotedmacrocall(@nospecialize x) =
+    isexpr(x, :copyast, 1) &&
+    isa(x.args[1], QuoteNode) && isexpr(x.args[1].value, :macrocall, 2)
+# Simple expressions / atoms the may be documented.
+isbasicdoc(@nospecialize x) = isexpr(x, :.) || isa(x, Union{QuoteNode,Symbol})
+is_signature(@nospecialize x) =
+    isexpr(x, :call) ||
+    (isexpr(x, :(::), 2) && isexpr(x.args[1], :call)) || isexpr(x, :where)
+
+function docm(source::LineNumberNode, mod::Module, ex)
+    @nospecialize ex
+    if isexpr(ex, :->) && length(ex.args) > 1
+        return docm(source, mod, ex.args...)
+    else
+        # TODO: this is a shim to continue to allow `@doc` for looking up docstrings
+        REPL = Base.REPL_MODULE_REF[]
+        return REPL.lookup_doc(ex)
+    end
+end
+
+function docm(source::LineNumberNode, mod::Module, meta, ex, define::Bool = true)
+    @nospecialize meta ex
+    # Some documented expressions may be decorated with macro calls which obscure the actual
+    # expression. Expand the macro calls and remove extra blocks.
+    x = unblock(macroexpand(mod, ex))
+    # Don't try to redefine expressions. This is only needed for `Base` img gen since
+    # otherwise calling `loaddocs` would redefine all documented functions and types.
+    def = define ? x : nothing
+    if isa(x, GlobalRef) && (x::GlobalRef).mod == mod
+        x = (x::GlobalRef).name
+    end
+
+    # Keywords using the `@kw_str` macro in `base/docs/basedocs.jl`.
+    #
+    #   "..."
+    #   kw"if", kw"else"
+    #
+    doc = isa(x, Base.BaseDocs.Keyword) ? keyworddoc(source, mod, meta, x) :
+
+    # Method / macro definitions and "call" syntax.
+    #
+    #   function f(...) ... end
+    #   f(...) = ...
+    #   macro m(...) end
+    #   function f end
+    #   f(...)
+    #
+          isexpr(x, FUNC_HEADS) && is_signature(x.args[1]) ?
+          objectdoc(source, mod, meta, def, x, signature(x)) :
+          isexpr(x, [:function, :macro]) && !isexpr(x.args[1], :call) ?
+          objectdoc(source, mod, meta, def, x) :
+          isexpr(x, :call) ? calldoc(source, mod, meta, x) :
+
+    # Type definitions.
+    #
+    #   struct T ... end
+    #   abstract type T end
+    #   primitive type T N end
+    #
+          isexpr(x, [:struct, :abstract, :primitive]) ?
+          objectdoc(source, mod, meta, def, x) :
+
+    # "Bindings". Names that resolve to objects with different names, ie.
+    #
+    #   const T = S
+    #   T = S
+    #   global T = S
+    #
+          isexpr(x, BINDING_HEADS) && !isexpr(x.args[1], :call) ?
+          objectdoc(source, mod, meta, def, x) :
+
+    # Quoted macrocall syntax. `:@time` / `:(Base.@time)`.
+          isquotedmacrocall(x) ? objectdoc(source, mod, meta, def, x) :
+    # Modules and baremodules.
+          isexpr(x, :module) ? moduledoc(source, mod, meta, def, x) :
+    # Document several expressions with the same docstring. `a, b, c`.
+          isexpr(x, :tuple) ? multidoc(source, mod, meta, x, define) :
+    # Errors generated by calling `macroexpand` are passed back to the call site.
+          isexpr(x, :error) ? esc(x) :
+    # When documenting macro-generated code we look for embedded `@__doc__` calls.
+          __doc__!(meta, x, define) ? esc(x) :
+    # Any "basic" expression such as a bare function or module name or numeric literal.
+          isbasicdoc(x) ? objectdoc(source, mod, meta, nothing, x) :
+
+    # All other expressions are undocumentable and should be handled on a case-by-case basis
+    # with `@__doc__`. Unbound string literals are also undocumentable since they cannot be
+    # retrieved from the module's metadata `IdDict` without a reference to the string.
+          docerror(ex)
+
+    return doc
+end
+
+function docerror(@nospecialize ex)
+    txt = """
+    cannot document the following expression:
+
+    $(isa(ex, AbstractString) ? repr(ex) : ex)"""
+    if isexpr(ex, :macrocall)
+        txt *= "\n\n'$(ex.args[1])' not documentable. See 'Base.@__doc__' docs for details."
+    end
+    return :($(error)($txt, "\n"))
+end
+
+include("utils.jl")
+
+# Swap out the bootstrap macro with the real one.
+Core.atdoc!(docm)
+
+function loaddocs(docs)
+    for (mod, ex, str, file, line) in docs
+        data = Dict(:path => string(file), :linenumber => line)
+        doc = docstr(str, data)
+        docstring = docm(LineNumberNode(line, file), mod, doc, ex, false) # expand the real @doc macro now
+        Core.eval(mod, Expr(Core.unescape, docstring, Docs))
+    end
+    empty!(docs)
+    nothing
+end
+
+function formatdoc end
+function parsedoc end
+function apropos end
+function doc end
+
+end
diff --git a/test/files/PProf.jl b/test/files/PProf.jl
index 4147ecf5..9f1dbb26 100644
--- a/test/files/PProf.jl
+++ b/test/files/PProf.jl
@@ -45,7 +45,7 @@ Useful for the Strings table
 NOTE: We must use Int64 throughout this package (regardless of system word-size) b/c the
 proto file specifies 64-bit integers.
 """
-function _enter!(dict::OrderedDict{T,Int64}, key::T) where T
+function _enter!(dict::OrderedDict{T,Int64}, key::T) where {T}
     if haskey(dict, key)
         return dict[key]
     else
@@ -97,7 +97,7 @@ function pprof(
     out::AbstractString = "profile.pb.gz",
     from_c::Bool = true,
     drop_frames::Union{Nothing,AbstractString} = nothing,
-    keep_frames::Union{Nothing,AbstractString} = nothing
+    keep_frames::Union{Nothing,AbstractString} = nothing,
 )
     if data === nothing
         data = copy(Profile.fetch())
@@ -123,7 +123,7 @@ function pprof(
 
     sample_type = [
         ValueType!("events", "count"), # Mandatory
-        ValueType!("stack_depth", "count")
+        ValueType!("stack_depth", "count"),
     ]
 
     prof = PProfile(
@@ -135,7 +135,7 @@ function pprof(
         sample_type = sample_type,
         default_sample_type = 1, # events
         period = period,
-        period_type = ValueType!("cpu", "nanoseconds")
+        period_type = ValueType!("cpu", "nanoseconds"),
     )
 
     if drop_frames !== nothing
@@ -261,7 +261,7 @@ function refresh(
     ;
     webhost::AbstractString = "localhost",
     webport::Integer = 57599,
-    file::AbstractString = "profile.pb.gz"
+    file::AbstractString = "profile.pb.gz",
 )
 
     if proc[] === nothing
diff --git a/test/files/adjoint.jl b/test/files/adjoint.jl
index 6f9aa661..675b210e 100644
--- a/test/files/adjoint.jl
+++ b/test/files/adjoint.jl
@@ -23,7 +23,7 @@ function _forward end
 function gradm(ex, mut = false)
     @capture(
         shortdef(ex),
-        (name_(args__) = body_) | (name_(args__) where {Ts__} = body_) 
+        (name_(args__) = body_) | (name_(args__) where {Ts__} = body_),
     ) || error("Need a function definition")
     kw = length(args) > 1 && isexpr(args[1], :parameters) ? esc(popfirst!(args)) : nothing
     isclosure = isexpr(name, :(::)) && length(name.args) > 1
@@ -42,10 +42,10 @@ function gradm(ex, mut = false)
     fargs = kw == nothing ? [cx, :($f::$T), args...] : [kw, cx, :($f::$T), args...]
     gradtuple = isclosure ? gradtuple0 : gradtuple1
     gradtuplekw = isclosure ? gradtuple2 : gradtuple3
-    adj = @q @inline ZygoteRules.adjoint($(fargs...)) where $(Ts...) = $(esc(body))
+    adj = @q @inline ZygoteRules.adjoint($(fargs...)) where {$(Ts...)} = $(esc(body))
     quote
         $adj
-        @inline function ZygoteRules._forward($cx, $f::$T, $(args...)) where $(Ts...)
+        @inline function ZygoteRules._forward($cx, $f::$T, $(args...)) where {$(Ts...)}
             y, _back = adjoint(__context__, $f, $(argnames...))
             $(mut ? nothing : :(back(::Nothing) = nothing))
             back(Δ) = $gradtuple(_back(Δ))
@@ -56,8 +56,8 @@ function gradm(ex, mut = false)
             ::$kT,
             kw,
             $f::$T,
-            $(args...)
-        ) where $(Ts...)
+            $(args...),
+        ) where {$(Ts...)}
             y, _back = adjoint(__context__, $f, $(argnames...); kw...)
             $(mut ? nothing : :(back(::Nothing) = nothing))
             back(Δ) = $gradtuplekw(_back(Δ))
diff --git a/test/files/cppwrapper.jl b/test/files/cppwrapper.jl
index 7503248b..0474e653 100644
--- a/test/files/cppwrapper.jl
+++ b/test/files/cppwrapper.jl
@@ -1,15 +1,15 @@
 
 
 struct CPolygon
-  vertexlist::Ptr{Cint}
-  numberofvertices::Cint
+    vertexlist::Ptr{Cint}
+    numberofvertices::Cint
 end
 
 struct CFacet{T}
-  polygonlist::Ptr{CPolygon}
-  numberofpolygons::Cint
-  holelist::Ptr{T}
-  numberofholes::Cint
+    polygonlist::Ptr{CPolygon}
+    numberofpolygons::Cint
+    holelist::Ptr{T}
+    numberofholes::Cint
 end
 
 # function Base.cconvert(::Type{Vector{CFacet{T}}}, triangles::Vector{NGonFace{N, Cint}}) where {N, T}
@@ -22,25 +22,29 @@ end
 #     return facets
 # end
 
-struct JLFacet{T, A <: AbstractVector{Cint}}
+struct JLFacet{T,A<:AbstractVector{Cint}}
     polygons::Vector{A}
     holelist::Vector{T}
 end
 JLFacet(x::AbstractVector{Cint}) = JLFacet([x])
-JLFacet(x::Vector{<: AbstractVector{Cint}}) = JLFacet{Float64, eltype(typeof(x))}(x, Float64[])
+JLFacet(x::Vector{<:AbstractVector{Cint}}) =
+    JLFacet{Float64,eltype(typeof(x))}(x, Float64[])
 
 
 function JLPolygon(x::CPolygon)
     JLPolygon(unsafe_wrap(Array, x.vertexlist, x.numberofvertices, own = true))
 end
 
-function JLFacet(f::CFacet{T}) where T
+function JLFacet(f::CFacet{T}) where {T}
     c_polys = unsafe_wrap(Array, f.polygonlist, f.numberofpolygons, own = true)
     holes = unsafe_wrap(Array, f.holelist, f.numberofholes, own = true)
     JLFacet{T}(JLPolygon.(c_polys), holes)
 end
 
-function convert_poly(x::Vector{T}) where T <: Union{NgonFace{N, Cint}, NTuple{N, Cint}} where N
+function convert_poly(x::Vector{T}) where {T<:Union{
+    NgonFace{N,Cint},
+    NTuple{N,Cint},
+}} where {N}
     return map(1:length(x)) do i
         CPolygon(pointer(x, i), N)
     end
@@ -54,16 +58,16 @@ end
 
 
 struct Region{T}
-    pos::Point{3, T}
+    pos::Point{3,T}
     attribute::T
     maxvol::T
 end
 
-struct FacetConstraint{IT, T}
+struct FacetConstraint{IT,T}
     facet_marker::IT
     max_area_bound::T
 end
-struct SegmentationConstraint{IT, T}
+struct SegmentationConstraint{IT,T}
     index::IT # index into pointlist
     max_length_bound::T
 end
@@ -121,13 +125,13 @@ end
 inttype(::Type{Float64}) = Int64
 inttype(::Type{Float32}) = Int32
 
-struct TetgenIO{T, NSimplex, NAttributes, NMTr, IT, FT}
-    points::Vector{Point{3, T}}
-    pointattributes::Vector{SVector{NAttributes, T}}
-    pointmtrs::Vector{SVector{NMTr, T}}
+struct TetgenIO{T,NSimplex,NAttributes,NMTr,IT,FT}
+    points::Vector{Point{3,T}}
+    pointattributes::Vector{SVector{NAttributes,T}}
+    pointmtrs::Vector{SVector{NMTr,T}}
     pointmarkers::Vector{Cint}
 
-    tetrahedra::Vector{SimplexFace{NSimplex, Cint}}
+    tetrahedra::Vector{SimplexFace{NSimplex,Cint}}
     tetrahedronattributes::Vector{T}
     tetrahedronvolumes::Vector{T}
     neighbors::Vector{Cint}
@@ -135,13 +139,13 @@ struct TetgenIO{T, NSimplex, NAttributes, NMTr, IT, FT}
     facets::FT
     facetmarkers::Vector{Cint}
 
-    holes::Vector{Point{3, T}}
+    holes::Vector{Point{3,T}}
 
     regions::Vector{Region{T}}
 
-    facetconstraints::Vector{FacetConstraint{IT, T}}
+    facetconstraints::Vector{FacetConstraint{IT,T}}
 
-    segmentconstraints::Vector{SegmentationConstraint{IT, T}}
+    segmentconstraints::Vector{SegmentationConstraint{IT,T}}
 
     trifaces::Vector{TriangleFace{Cint}}
     trifacemarkers::Vector{Cint}
@@ -150,28 +154,28 @@ struct TetgenIO{T, NSimplex, NAttributes, NMTr, IT, FT}
     edgemarkers::Vector{Cint}
 
     function TetgenIO(
-            points::Vector{Point{3, T}},
-            pointattributes::Vector{SVector{NAttributes, T}},
-            pointmtrs::Vector{SVector{NMTr, T}},
-            pointmarkers::Vector{Cint},
-            tetrahedrons::Vector{SimplexFace{NSimplex, Cint}},
-            tetrahedronattributes::Vector{T},
-            tetrahedronvolumes::Vector{T},
-            neighbors::Vector{Cint},
-            facets::FT,
-            facetmarkers::Vector{Cint},
-            holes::Vector{Point{3, T}},
-            regions::Vector{Region{T}},
-            facetconstraints::Vector{FacetConstraint{IT, T}},
-            segmentconstraints::Vector{SegmentationConstraint{IT, T}},
-            trifaces::Vector{TriangleFace{Cint}},
-            trifacemarkers::Vector{Cint},
-            edges::Vector{LineFace{Cint}},
-            edgemarkers::Vector{Cint},
-        ) where {T, NSimplex, NAttributes, NMTr, IT, FT}
-
-
-        new{T, NSimplex, NAttributes, NMTr, IT, FT}(
+        points::Vector{Point{3,T}},
+        pointattributes::Vector{SVector{NAttributes,T}},
+        pointmtrs::Vector{SVector{NMTr,T}},
+        pointmarkers::Vector{Cint},
+        tetrahedrons::Vector{SimplexFace{NSimplex,Cint}},
+        tetrahedronattributes::Vector{T},
+        tetrahedronvolumes::Vector{T},
+        neighbors::Vector{Cint},
+        facets::FT,
+        facetmarkers::Vector{Cint},
+        holes::Vector{Point{3,T}},
+        regions::Vector{Region{T}},
+        facetconstraints::Vector{FacetConstraint{IT,T}},
+        segmentconstraints::Vector{SegmentationConstraint{IT,T}},
+        trifaces::Vector{TriangleFace{Cint}},
+        trifacemarkers::Vector{Cint},
+        edges::Vector{LineFace{Cint}},
+        edgemarkers::Vector{Cint},
+    ) where {T,NSimplex,NAttributes,NMTr,IT,FT}
+
+
+        new{T,NSimplex,NAttributes,NMTr,IT,FT}(
             points,
             pointattributes,
             pointmtrs,
@@ -196,26 +200,26 @@ struct TetgenIO{T, NSimplex, NAttributes, NMTr, IT, FT}
 end
 
 function TetgenIO(
-        points::Vector{Point{3, T}};
-        pointattributes = SVector{0, T}[],
-        pointmtrs = SVector{0, T}[],
-        pointmarkers = Cint[],
-        tetrahedrons = SimplexFace{4, Cint}[],
-        tetrahedronattributes = T[],
-        tetrahedronvolumes = T[],
-        neighbors = Cint[],
-        facets = JLFacet{T, Vector{Cint}}[],
-        facetmarkers = Cint[],
-        holes = Point{3, T}[],
-        regions = Region{T}[],
-        facetconstraints = FacetConstraint{inttype(T), T}[],
-        segmentconstraints = SegmentationConstraint{inttype(T), T}[],
-        trifaces = TriangleFace{Cint}[],
-        trifacemarkers = Cint[],
-        edges = LineFace{Cint}[],
-        edgemarkers = Cint[]
-
-    ) where T
+    points::Vector{Point{3,T}};
+    pointattributes = SVector{0,T}[],
+    pointmtrs = SVector{0,T}[],
+    pointmarkers = Cint[],
+    tetrahedrons = SimplexFace{4,Cint}[],
+    tetrahedronattributes = T[],
+    tetrahedronvolumes = T[],
+    neighbors = Cint[],
+    facets = JLFacet{T,Vector{Cint}}[],
+    facetmarkers = Cint[],
+    holes = Point{3,T}[],
+    regions = Region{T}[],
+    facetconstraints = FacetConstraint{inttype(T),T}[],
+    segmentconstraints = SegmentationConstraint{inttype(T),T}[],
+    trifaces = TriangleFace{Cint}[],
+    trifacemarkers = Cint[],
+    edges = LineFace{Cint}[],
+    edgemarkers = Cint[],
+
+) where {T}
     TetgenIO(
         points,
         pointattributes,
@@ -279,24 +283,25 @@ function get_array(io, field::Symbol, Typ)
 end
 
 function Base.convert(
-        IOT::Type{TetgenIO{T, NSimplex, NAttributes, NMTr, IT, A}}, io::CPPTetgenIO{T}
-    ) where {T, NSimplex, NAttributes, NMTr, IT, A}
+    IOT::Type{TetgenIO{T,NSimplex,NAttributes,NMTr,IT,A}},
+    io::CPPTetgenIO{T},
+) where {T,NSimplex,NAttributes,NMTr,IT,A}
     TetgenIO(ntuple(fieldcount(IOT)) do i
         fname, ftype = fieldname(IOT, i), fieldtype(IOT, i)
         get_array(io, fname, eltype(ftype))
     end...)
 end
 
-function Base.convert(::Type{TetgenIO}, io::CPPTetgenIO{T}) where T
+function Base.convert(::Type{TetgenIO}, io::CPPTetgenIO{T}) where {T}
     NSimplex = Int(io.numberofcorners)
     NAttributes = Int(io.numberofpointattributes)
     NMTr = Int(io.numberofpointmtrs)
     IT = inttype(T) # TODO these are indices,
-    convert(TetgenIO{T, NSimplex, NAttributes, NMTr, IT, Vector{CFacet{T}}}, io)
+    convert(TetgenIO{T,NSimplex,NAttributes,NMTr,IT,Vector{CFacet{T}}}, io)
 end
 
 
-function Base.cconvert(::Type{Ptr{CFacet{T}}}, facets::Vector{JLFacet{T, A}}) where {T, A}
+function Base.cconvert(::Type{Ptr{CFacet{T}}}, facets::Vector{JLFacet{T,A}}) where {T,A}
     c_polys = map(facets) do facet
         convert_poly(facet.polygons)
     end
@@ -307,7 +312,7 @@ function Base.cconvert(::Type{Ptr{CFacet{T}}}, facets::Vector{JLFacet{T, A}}) wh
     return (facets, c_polys, c_facets)
 end
 
-function Base.cconvert(::Type{Ptr{CFacet{T}}}, facets::Vector{NgonFace{N, Cint}}) where {N, T}
+function Base.cconvert(::Type{Ptr{CFacet{T}}}, facets::Vector{NgonFace{N,Cint}}) where {N,T}
     nfacets = length(facets)
     c_polys = map(1:nfacets) do i
         CPolygon(Ptr{Cint}(pointer(facets, i)), N)
@@ -321,30 +326,33 @@ end
 
 
 function unsafe_array_convert(
-        ::Type{Ptr{CFacet{T}}},
-        facets::Tuple{Vector{F}, Vector{Vector{CPolygon}}, Vector{CFacet{T}}}
-    ) where {F, T}
+    ::Type{Ptr{CFacet{T}}},
+    facets::Tuple{Vector{F},Vector{Vector{CPolygon}},Vector{CFacet{T}}},
+) where {F,T}
     return Base.unsafe_convert(Ptr{CFacet{T}}, facets[3])
 end
 
 function unsafe_array_convert(
-        ::Type{Ptr{CFacet{T}}},
-        facets::Tuple{Vector{F}, Vector{CPolygon}, Vector{CFacet{T}}}
-    ) where {F, T}
+    ::Type{Ptr{CFacet{T}}},
+    facets::Tuple{Vector{F},Vector{CPolygon},Vector{CFacet{T}}},
+) where {F,T}
     return Base.unsafe_convert(Ptr{CFacet{T}}, facets[3])
 end
 
-function unsafe_array_convert(P::Type{Ptr{T}}, x::Vector{T}) where T
+function unsafe_array_convert(P::Type{Ptr{T}}, x::Vector{T}) where {T}
     Base.unsafe_convert(P, x)
 end
 
-function unsafe_array_convert(P::Type{Ptr{T1}}, x::Vector{T2}) where {T1, T2}
+function unsafe_array_convert(P::Type{Ptr{T1}}, x::Vector{T2}) where {T1,T2}
     Ptr{T1}(Base.unsafe_convert(Ptr{T2}, x))
 end
 
-function Base.cconvert(CIO::Type{CPPTetgenIO{T}}, obj::TetgenIO{T, NSimplex, NAttributes, NMTr, IT, A}) where {T, NSimplex, NAttributes, NMTr, IT, A}
+function Base.cconvert(
+    CIO::Type{CPPTetgenIO{T}},
+    obj::TetgenIO{T,NSimplex,NAttributes,NMTr,IT,A},
+) where {T,NSimplex,NAttributes,NMTr,IT,A}
     cfnames = fieldnames(CIO)
-    dict = Dict{Symbol, Any}(
+    dict = Dict{Symbol,Any}(
         :numberofpointattributes => NAttributes,
         :numberofpointmtrs => NMTr,
         :numberofcorners => NSimplex,
@@ -382,17 +390,32 @@ function Base.cconvert(CIO::Type{CPPTetgenIO{T}}, obj::TetgenIO{T, NSimplex, NAt
     return (CPPTetgenIO{T}(fields_tuple...), gc_tack_cconvert)
 end
 
-function Base.unsafe_convert(::Type{CPPTetgenIO{T}}, x::Tuple{CPPTetgenIO{T}, Vector{Any}}) where T
+function Base.unsafe_convert(
+    ::Type{CPPTetgenIO{T}},
+    x::Tuple{CPPTetgenIO{T},Vector{Any}},
+) where {T}
     return x[1]
 end
 
 
 function tetrahedralize(io::TetgenIO{Float64}, command::String)
-    cres = ccall((:tetrahedralizef64, libtet), CPPTetgenIO{Float64}, (CPPTetgenIO{Float64}, Cstring), io, command)
+    cres = ccall(
+        (:tetrahedralizef64, libtet),
+        CPPTetgenIO{Float64},
+        (CPPTetgenIO{Float64}, Cstring),
+        io,
+        command,
+    )
     return convert(TetgenIO, cres)
 end
 
 function tetrahedralize(io::TetgenIO{Float32}, command::String)
-    cres = ccall((:tetrahedralizef32, libtet), CPPTetgenIO{Float32}, (CPPTetgenIO{Float32}, Cstring), io, command)
+    cres = ccall(
+        (:tetrahedralizef32, libtet),
+        CPPTetgenIO{Float32},
+        (CPPTetgenIO{Float32}, Cstring),
+        io,
+        command,
+    )
     return convert(TetgenIO, cres)
 end
diff --git a/test/files/issue_38.jl b/test/files/issue_38.jl
index 2ecf3be8..db8ac865 100644
--- a/test/files/issue_38.jl
+++ b/test/files/issue_38.jl
@@ -9,23 +9,23 @@ free_space(path) = shutil.disk_usage(path)[3] / 1000_000_000
 settings = ArgParseSettings()
 
 @add_arg_table settings begin
-        "--file"
-        help = """
-        path to the file.
-        ...
-        """
+    "--file"
+    help = """
+    path to the file.
+    ...
+    """
 end
 
 @add_arg_table settings begin
-        "--output", "-o"
-        help = "output..."
-        default = "."
-        """
-        --file
-        """
-        help = """
-        path to the file.
-        ...
-        """
-        default = ""
+    "--output", "-o"
+    help = "output..."
+    default = "."
+    """
+    --file
+    """
+    help = """
+    path to the file.
+    ...
+    """
+    default = ""
 end
diff --git a/test/files/prettify_demo.jl b/test/files/prettify_demo.jl
index 27184961..0b832936 100644
--- a/test/files/prettify_demo.jl
+++ b/test/files/prettify_demo.jl
@@ -16,8 +16,8 @@ function TetgenIO(
     trifaces = TriangleFace{Cint}[],
     trifacemarkers = Cint[],
     edges = LineFace{Cint}[],
-    edgemarkers = Cint[]
-) where T
+    edgemarkers = Cint[],
+) where {T}
     TetgenIO(
         points,
         pointattributes,
diff --git a/test/files/reverse.jl b/test/files/reverse.jl
index bae841db..ea1e34fa 100644
--- a/test/files/reverse.jl
+++ b/test/files/reverse.jl
@@ -27,7 +27,7 @@ using Base: @get!
 
 @inline tuple_va(N, xs) = xs
 @inline tuple_va(N, x, xs...) = (x, tuple_va(N, xs...)...)
-@inline tuple_va(::Val{N}, ::Nothing) where N = ntuple(_ -> nothing, Val(N))
+@inline tuple_va(::Val{N}, ::Nothing) where {N} = ntuple(_ -> nothing, Val(N))
 
 iscall(x, m::Module, n::Symbol) = isexpr(x, :call) && x.args[1] == GlobalRef(m, n)
 
@@ -78,7 +78,7 @@ function instrument_iterate!(ir, v, ex)
     :literal_indexed_iterate,
     ex.args[2],
     Val(unwrapquote(ex.args[3])),
-    ex.args[4:end]...
+    ex.args[4:end]...,
   )) :
   ex
 end
@@ -148,7 +148,7 @@ ignored_f(f) =
     GlobalRef(Core, :throw),
     GlobalRef(Base, :kwerr),
     GlobalRef(Core, :kwfunc),
-    GlobalRef(Core, :isdefined)
+    GlobalRef(Core, :isdefined),
   )
 ignored_f(ir, f) = ignored_f(f)
 ignored_f(ir, f::Variable) = ignored_f(get(ir, f, nothing))
@@ -180,7 +180,7 @@ function primal(ir::IR)
         yJ = insert!(
           pr,
           v,
-          stmt(xcall(Zygote, :_forward, cx, ex.args...), line = ir[v].line)
+          stmt(xcall(Zygote, :_forward, cx, ex.args...), line = ir[v].line),
         )
         pr[v] = xgetindex(yJ, 1)
         J = insertafter!(
@@ -189,8 +189,8 @@ function primal(ir::IR)
           stmt(
             xgetindex(yJ, 2),
             type = T == Any ? Any : T.parameters[2],
-            line = ir[v].line
-          )
+            line = ir[v].line,
+          ),
         )
         pbs[v] = substitute(pr, J)
       else
@@ -297,8 +297,8 @@ function adjoint(pr::Primal)
           rb,
           stmt(
             xcall(Base, :error, "Can't differentiate $(ex.head) expression"),
-            line = b[v].line
-          )
+            line = b[v].line,
+          ),
         )
       else # A literal value
         continue
@@ -317,11 +317,11 @@ function adjoint(pr::Primal)
         end
       end
     else # Backprop function arguments
-      gs = [grad(arg) for arg = arguments(pr.ir)]
+      gs = [grad(arg) for arg in arguments(pr.ir)]
       Δ = push!(
         rb,
         pr.varargs == nothing ? xcall(Zygote, :tuple, gs...) :
-        xcall(Zygote, :tuple_va, Val(pr.varargs), gs...)
+        xcall(Zygote, :tuple_va, Val(pr.varargs), gs...),
       )
       branches(rb)[1].args[1] = Δ
     end
diff --git a/test/files/setup_databases.jl b/test/files/setup_databases.jl
index 8e35dfa3..e8537519 100644
--- a/test/files/setup_databases.jl
+++ b/test/files/setup_databases.jl
@@ -21,43 +21,43 @@ function check_space(free, min_space, database_name)
 end
 
 function parse_commandline()
-    settings = ArgParseSettings(description="""
-    This script set up the needed databases for PhyloSofS in the `--output`
-    path. If the `--pdb`, `--uniclust` or `--pdb70` argument is used, the
-    script is going to create a symbolic link to the indicated folder
-    instead of downloading the database.
+    settings = ArgParseSettings(description = """
+      This script set up the needed databases for PhyloSofS in the `--output`
+      path. If the `--pdb`, `--uniclust` or `--pdb70` argument is used, the
+      script is going to create a symbolic link to the indicated folder
+      instead of downloading the database.
 
-    It creates a `databases` folder in `--output` containing three folders:
-    `pdb`, `uniclust` and `pdb70`
-    """,)
+      It creates a `databases` folder in `--output` containing three folders:
+      `pdb`, `uniclust` and `pdb70`
+      """,)
 
     @add_arg_table settings begin
         "--output", "-o"
-            help = "path where the database folder is going to be created."
-            default = "."
+        help = "path where the database folder is going to be created."
+        default = "."
         "--pdb"
-            help = """
-            path to an already existing local folder containing the entire pdb
-            in mmCIF format.
-            """
-            default = ""
+        help = """
+        path to an already existing local folder containing the entire pdb
+        in mmCIF format.
+        """
+        default = ""
         "--uniclust"
-            help = """
-            path to an already existing local folder containing the uniclust
-            database from the HH-suite databases.
-            """
-            default = ""
+        help = """
+        path to an already existing local folder containing the uniclust
+        database from the HH-suite databases.
+        """
+        default = ""
         "--uniclust_version"
-            help = """
-            Uniclust30 version to be downloaded: YYYY_MM
-            """
-            default = "2018_08"
+        help = """
+        Uniclust30 version to be downloaded: YYYY_MM
+        """
+        default = "2018_08"
         "--pdb70"
-            help = """
-            path to an already existing local folder containing the
-            pdb70_from_mmcif database from the HH-suite databases.
-            """
-            default = ""
+        help = """
+        path to an already existing local folder containing the
+        pdb70_from_mmcif database from the HH-suite databases.
+        """
+        default = ""
     end
 
     return parse_args(settings)
@@ -87,10 +87,14 @@ function main()
     if uniclust == ""
         check_space(free, 14, "HH-suite Uniclust30")
         cd(output_path)
-        download(string(
-            "http://wwwuser.gwdg.de/~compbiol/uniclust/2018_08/uniclust30_",
-            uniclust_version, ".tar.gz"
-            ), "uniclust.tar.gz")
+        download(
+            string(
+                "http://wwwuser.gwdg.de/~compbiol/uniclust/2018_08/uniclust30_",
+                uniclust_version,
+                ".tar.gz",
+            ),
+            "uniclust.tar.gz",
+        )
         unpack("uniclust.tar.gz")
         mv("uniclust30_$uniclust_version", "uniclust")
         cd(execution_folder)
@@ -121,8 +125,10 @@ function main()
         check_space(free, 42, "HH-suite pdb70_from_mmcif")
         mkpath(pdb70_path)
         cd(pdb70_path)
-        download("http://wwwuser.gwdg.de/~compbiol/data/hhsuite/databases/hhsuite_dbs/pdb70_from_mmcif_latest.tar.gz",
-            "pdb70.tar.gz")
+        download(
+            "http://wwwuser.gwdg.de/~compbiol/data/hhsuite/databases/hhsuite_dbs/pdb70_from_mmcif_latest.tar.gz",
+            "pdb70.tar.gz",
+        )
         unpack("pdb70.tar.gz")
         cd(execution_folder)
     else
@@ -140,7 +146,7 @@ function main()
     if pdb == ""
         check_space(free, 157, "PDB")
         mkpath(pdb_path)
-        downloadentirepdb(pdb_dir=pdb_path, file_format=MMCIF)
+        downloadentirepdb(pdb_dir = pdb_path, file_format = MMCIF)
     else
         pdb = abspath(pdb)
         symlink(pdb, pdb_path)
diff --git a/test/runtests.jl b/test/runtests.jl
index efc8cbc0..64142765 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -35,6 +35,20 @@ end
         @test fmt("") == ""
         @test fmt("a") == "a"
         @test fmt("a  #foo") == "a  #foo"
+        @test fmt("#foo") == "#foo"
+
+        str = """
+        #=
+        Hello, world!
+        =#"""
+        @test fmt(str) == str
+
+        str = """
+        #=
+        Hello, world!
+        =#
+        a"""
+        @test fmt(str) == str
     end
 
     @testset "nofmt" begin
@@ -50,8 +64,21 @@ end
         a = 10;
 
 
-        c = 50;"""
-        @test fmt(str) == str
+        c = 50;
+
+        #comment"""
+        str_ = """
+
+        hello = "string";
+
+        a = 10
+        ;
+
+
+        c = 50;
+
+        #comment"""
+        @test fmt(str_) == str
         t = run_pretty(str, 80)
         @test length(t) == 17
     end
@@ -133,7 +160,7 @@ end
         @test fmt("(a ,b)") == "(a, b)"
         @test fmt("( a, b)") == "(a, b)"
         @test fmt("(a, b )") == "(a, b)"
-        @test fmt("(a, b ,)") == "(a, b,)"
+        @test fmt("(a, b ,)") == "(a, b)"
         @test fmt("""(a,    b ,
                             c)""") == "(a, b, c)"
     end
@@ -165,9 +192,16 @@ end
 
     @testset "where op" begin
         str = "Atomic{T}(value) where {T<:AtomicTypes} = new(value)"
+        str_ = "Atomic{T}(value) where T <: AtomicTypes = new(value)"
         @test fmt(str) == str
+        @test fmt(str_) == str
 
-        str = "Atomic{T}(value) where T <: AtomicTypes = new(value)"
+        str = "Vector{Vector{T} where T}"
+        @test fmt(str) == str
+
+        str_ = "Vector{Vector{T}} where T"
+        str = "Vector{Vector{T}} where {T}"
+        @test fmt(str_) == str
         @test fmt(str) == str
     end
 
@@ -196,7 +230,8 @@ end
         @test fmt("a:b:c ") == "a:b:c"
         @test fmt("a::b:: c") == "a::b::c"
         @test fmt("a :: b::c") == "a::b::c"
-        @test fmt("2n") == "2n"
+        @test fmt("2a") == "2a"
+        @test fmt("2(a+1)") == "2 * (a + 1)"
 
         str = "!(typ <: ArithmeticTypes)"
         @test fmt(str) == str
@@ -361,8 +396,8 @@ end
     @testset "func call" begin
         @test fmt("func(a, b, c)") == "func(a, b, c)"
         @test fmt("func(a,b,c)") == "func(a, b, c)"
-        @test fmt("func(a,b,c,)") == "func(a, b, c,)"
-        @test fmt("func(a,b,c, )") == "func(a, b, c,)"
+        @test fmt("func(a,b,c,)") == "func(a, b, c)"
+        @test fmt("func(a,b,c, )") == "func(a, b, c)"
         @test fmt("func( a,b,c    )") == "func(a, b, c)"
         @test fmt("func(a, b, c) ") == "func(a, b, c)"
         @test fmt("func(a, b; c)") == "func(a, b; c)"
@@ -423,7 +458,7 @@ end
             s = foo(
                 aaaa,
                 bbbb,
-                cccc
+                cccc,
             )
         end"""
         @test fmt(str, 4, 28) == str
@@ -479,7 +514,7 @@ end
             s = foo(
                 aaaa,
                 bbbb,
-                cccc
+                cccc,
             )
         end"""
         @test fmt(str, 4, 28) == str
@@ -516,6 +551,47 @@ end
         @test length(t) == 27
         @test fmt(str, 4, 26) == str
 
+        # issue #58
+
+        str_ = """
+        model = SDDP.LinearPolicyGraph(stages = 2, lower_bound = 1, direct_mode = false) do (subproblem1, subproblem2, subproblem3, subproblem4, subproblem5, subproblem6, subproblem7, subproblem8)
+            body
+        end"""
+        str = """
+        model =
+            SDDP.LinearPolicyGraph(
+                stages = 2,
+                lower_bound = 1,
+                direct_mode = false,
+            ) do (
+                subproblem1,
+                subproblem2,
+                subproblem3,
+                subproblem4,
+                subproblem5,
+                subproblem6,
+                subproblem7,
+                subproblem8,
+            )
+                body
+            end"""
+        @test fmt(str_) == str
+
+        str_ = """
+        model = SDDP.LinearPolicyGraph(stages = 2, lower_bound = 1, direct_mode = false) do subproblem1, subproblem2
+            body
+        end"""
+        str = """
+        model =
+            SDDP.LinearPolicyGraph(
+                stages = 2,
+                lower_bound = 1,
+                direct_mode = false,
+            ) do subproblem1, subproblem2
+                body
+            end"""
+        @test fmt(str_) == str
+
     end
 
     @testset "for" begin
@@ -1046,7 +1122,7 @@ end
         @test fmt(str) == str
     end
 
-    @testset "notcode" begin
+    @testset "comments" begin
         str = """
         module Foo
         # comment 0
@@ -1174,7 +1250,7 @@ end
             b, # comment 1
             c, # comment 2
             # in between comment
-            d # comment 3
+            d, # comment 3
         )        # pancakes"""
         @test fmt(str_) == str
 
@@ -1189,7 +1265,7 @@ end
             a,
             b, # comment 1
             c, # comment 2
-            d # comment 3
+            d, # comment 3
         )        # pancakes"""
         @test fmt(str_) == str
 
@@ -1243,28 +1319,152 @@ end
         """
         @test fmt(str) == str
 
-        str = """
+        str_ = """
         foo() = 10 where {
+            # comment
             A,
                 # comment
             B
+            # comment
         }"""
-        @test fmt(str) == str
-
         str = """
+        foo() = 10 where {
+            # comment
+            A,
+                # comment
+            B,
+            # comment
+        }"""
+        @test fmt(str_) == str
+
+        str_ = """
         foo() = 10 where Foo{
+            # comment
             A,
                 # comment
             B
+            # comment
         }"""
-        @test fmt(str) == str
-
         str = """
+        foo() = 10 where Foo{
+            # comment
+            A,
+                # comment
+            B,
+            # comment
+        }"""
+        @test fmt(str_) == str
+
+        str_ = """
         foo() = Foo(
+            # comment
             A,
                 # comment
             B
+            # comment
+        )"""
+        str = """
+        foo() = Foo(
+            # comment
+            A,
+                # comment
+            B,
+            # comment
+        )"""
+        @test fmt(str_) == str
+
+        str = """
+        foo(
+            # comment
+            ;
+            # comment
+            a = b, # comment
+            c = d,
+            # comment
+        )"""
+        @test fmt(str) == str
+
+        str = """
+        foo(
+            ;
+            a = b, # comment
+            c = d,
+            # comment
+        )"""
+        @test fmt(str) == str
+
+        str_ = """
+        foo(
+            ;
+            ;a = b, # comment
+            c = d,
+            # comment
         )"""
+        @test fmt(str_) == str
+
+        str_ = """
+        foo( ;
+            ;a = b, # comment
+            c = d,
+            # comment
+        )"""
+        @test fmt(str_) == str
+
+        # str = """
+        # [
+        #  a b Expr();
+        #  d e Expr()
+        # ]"""
+        # str_ = """
+        # [
+        # ;
+        # ;
+        # ;
+        #  a b Expr();
+        #  ;
+        #  d e Expr();
+        #  ;
+        # ]"""
+        # @test fmt(str_) == str
+        #
+
+        # Issue #51
+        # NOTE: `str_` has extra whitespace after
+        # keywords on purpose
+        str_ = "begin \n # comment\n end"
+        str = """
+        begin
+         # comment
+        end"""
+        @test fmt(str_) == str
+
+        str_ = "try \n # comment\n catch \n # comment\n finally \n # comment\n end"
+        str = """
+        try
+         # comment
+        catch
+         # comment
+        finally
+         # comment
+        end"""
+        @test fmt(str_) == str
+
+        str = """a = "hello ##" # # # α"""
+        @test fmt(str) == str
+
+        # issue #65
+        str = "1 # α"
+        @test fmt(str) == str
+
+        str = "# α"
+        @test fmt(str) == str
+
+        str = """
+        #=
+        α
+        =#
+        x = 1
+        """
         @test fmt(str) == str
 
     end
@@ -1541,55 +1741,44 @@ end
         function f(
             arg1::A,
             key1 = val1;
-            key2 = val2
+            key2 = val2,
         ) where {
             A,
             F{
               B,
-              C
-            }
+              C,
+            },
         }
             10
             20
         end"""
-        @test fmt(
-            "function f(arg1::A,key1=val1;key2=val2) where {A,F{B,C}} 10; 20 end",
-            4,
-            1
-        ) == str
+        str_ = "function f(arg1::A,key1=val1;key2=val2) where {A,F{B,C}} 10; 20 end"
+        @test fmt(str_, 4, 1) == str
 
         str = """
         function f(
             arg1::A,
             key1 = val1;
-            key2 = val2
+            key2 = val2,
         ) where {
             A,
-            F{B,C}
+            F{B,C},
         }
             10
             20
         end"""
-        @test fmt(
-            "function f(arg1::A,key1=val1;key2=val2) where {A,F{B,C}} 10; 20 end",
-            4,
-            17
-        ) == str
+        @test fmt(str_, 4, 17) == str
 
         str = """
         function f(
             arg1::A,
             key1 = val1;
-            key2 = val2
+            key2 = val2,
         ) where {A,F{B,C}}
             10
             20
         end"""
-        @test fmt(
-            "function f(arg1::A,key1=val1;key2=val2) where {A,F{B,C}} 10; 20 end",
-            4,
-            18
-        ) == str
+        @test fmt(str_, 4, 18) == str
 
         str = """
         a |
@@ -1614,7 +1803,7 @@ end
          a,
          b,
          c,
-         d
+         d,
         )"""
         @test fmt("(a, b, c, d)", 4, 11) == str
 
@@ -1626,7 +1815,7 @@ end
          a,
          b,
          c,
-         d
+         d,
         }"""
         @test fmt("{a, b, c, d}", 4, 11) == str
 
@@ -1638,7 +1827,7 @@ end
          a,
          b,
          c,
-         d
+         d,
         ]"""
         @test fmt("[a, b, c, d]", 4, 11) == str
 
@@ -1692,8 +1881,8 @@ end
              one,
              x -> (
                  true,
-                 false
-             )
+                 false,
+             ),
             )"""
         @test fmt("foo() = (one, x -> (true, false))", 4, 20) == str
 
@@ -1830,7 +2019,7 @@ end
 
         str = """this_is_a_long_variable_name = Dict{
              Symbol,
-             Any
+             Any,
         }(
              :numberofpointattributes => NAttributes,
              :numberofpointmtrs => NMTr,
@@ -1890,8 +2079,8 @@ end
             \""",
             foo(
                 b,
-                c
-            )
+                c,
+            ),
         )"""
         @test fmt(str, 4, 1) == str_
 
@@ -1904,7 +2093,7 @@ end
             string.
             Longest line
             \""",
-            foo(b, c)
+            foo(b, c),
         )"""
         @test fmt(str, 4, 31) == str
 
@@ -1978,6 +2167,24 @@ end
         @test fmt(str_) == str_
         @test fmt(str_, 4, 1) == str
 
+        # https://github.com/domluna/JuliaFormatter.jl/issues/60
+        str_ = """
+        function write_subproblem_to_file(
+                node::Node, filename::String;
+                format::Symbol=:both, throw_error::Bool = false)
+            body
+        end"""
+        str = """
+        function write_subproblem_to_file(
+            node::Node,
+            filename::String;
+            format::Symbol = :both,
+            throw_error::Bool = false,
+        )
+            body
+        end"""
+        @test fmt(str_) == str
+
     end
 
     @testset "nesting line offset" begin
@@ -2033,13 +2240,14 @@ end
         s = run_nest(str, 1)
         @test s.line_offset == 1
 
-        str = "f(a, b, c) where A"
+        str = "f(a, b, c) where {A}"
         s = run_nest(str, 100)
+        # adds surrounding {...} after `where`
         @test s.line_offset == length(str)
         s = run_nest(str, 1)
-        @test s.line_offset == 9
+        @test s.line_offset == 11
 
-        str = "f(a, b, c) where A <: S"
+        str = "f(a, b, c) where {A<:S}"
         s = run_nest(str, 100)
         @test s.line_offset == length(str)
         s = run_nest(str, 1)
@@ -2125,7 +2333,7 @@ end
                 body2
                 @Expr :break loop_exit2
                 body3
-            end
+            end,
         )"""
         @test fmt(str_, 4, 20) == str
 
@@ -2151,73 +2359,98 @@ end
     end
 
     @testset "additional length" begin
+        str_ = "f(a, @g(b, c), d)"
         str = """
         f(
           a,
           @g(b, c),
-          d
+          d,
         )"""
-        @test fmt("f(a, @g(b, c), d)", 4, 11) == str
+        @test fmt(str_, 4, 11) == str
+        @test fmt(str, 4, length(str)) == str_
 
+        str_ = "f(a, @g(b, c), d)"
         str = """
         f(
           a,
           @g(
              b,
-             c
+             c,
           ),
-          d
+          d,
         )"""
-        @test fmt("f(a, @g(b, c), d)", 4, 10) == str
+        @test fmt(str_, 4, 10) == str
+        @test fmt(str, 4, length(str)) == str_
 
+        str_ = "(a, (b, c), d)"
         str = """
         (
          a,
          (
           b,
-          c
+          c,
          ),
-         d
+         d,
         )"""
-        @test fmt("(a, (b, c), d)", 4, 7) == str
+        @test fmt(str_, 4, 7) == str
+        @test fmt(str, 4, length(str)) == str_
 
+        str_ = "(a, {b, c}, d)"
         str = """
         (
          a,
          {
           b,
-          c
+          c,
          },
-         d
+         d,
         )"""
-        @test fmt("(a, {b, c}, d)", 4, 6) == str
+        @test fmt(str_, 4, 6) == str
+        @test fmt(str, 4, length(str)) == str_
 
+        str_ = "(a, [b, c], d)"
+        str = """
+        (
+         a,
+         [
+          b,
+          c,
+         ],
+         d,
+        )"""
+        @test fmt(str_, 4, 6) == str
+        @test fmt(str, 4, length(str)) == str_
+
+        str_ = "a, (b, c), d"
         str = """
         a,
         (
          b,
-         c
+         c,
         ),
         d"""
-        @test fmt("a, (b, c), d", 4, 6) == str
+        @test fmt(str_, 4, 6) == str
+        @test fmt(str, 4, length(str)) == str_
 
+        str_ = "a, (b, c), d"
         str = """
         a,
         (b, c),
         d"""
-        @test fmt("a, (b, c), d", 4, 7) == str
+        @test fmt(str_, 4, 7) == str
+        @test fmt(str, 4, length(str)) == str_
 
         str = """
         (
          var1,
-         var2
+         var2,
         ) && var3"""
         @test fmt("(var1,var2) && var3", 4, 10) == str
 
         str = """
         (
          var1,
-         var2
+         var2,
         ) && var3"""
         @test fmt("(var1,var2) && var3", 4, 19) == str
 
@@ -2230,11 +2463,11 @@ end
         str = """
         (
          var1,
-         var2
+         var2,
         ) ?
         (
          var3,
-         var4
+         var4,
         ) :
         var5"""
         @test fmt("(var1,var2) ? (var3,var4) : var5", 4, 13) == str
@@ -2252,22 +2485,24 @@ end
         str = """
         f(
           var1::A,
-          var2::B
+          var2::B,
         ) where {A,B}"""
         @test fmt("f(var1::A, var2::B) where {A,B}", 4, 30) == str
 
         str = """
         f(
           var1::A,
-          var2::B
+          var2::B,
         ) where {
             A,
-            B
+            B,
         }"""
         @test fmt("f(var1::A, var2::B) where {A,B}", 4, 12) == str
 
         str = "foo(a, b, c)::Rtype where {A,B} = 10"
+        str_ = "foo(a, b, c)::Rtype where {A,B,} = 10"
         @test fmt(str, 4, length(str)) == str
+        @test fmt(str_, 4, length(str_)) == str
 
         str_ = """
         foo(a, b, c)::Rtype where {A,B} =
@@ -2278,7 +2513,7 @@ end
         str_ = """
         foo(a, b, c)::Rtype where {
             A,
-            B
+            B,
         } = 10"""
         @test fmt(str, 4, 32) == str_
         @test fmt(str, 4, 19) == str_
@@ -2287,10 +2522,10 @@ end
         foo(
             a,
             b,
-            c
+            c,
         )::Rtype where {
             A,
-            B
+            B,
         } = 10"""
         @test fmt(str, 4, 18) == str_
 
@@ -2301,7 +2536,7 @@ end
         str = """
         transcode(::Type{THISISONESUPERLONGTYPE1234567}) where {T<:Union{
           Int32,
-          UInt32
+          UInt32,
         }} = transcode(T, String(Vector(src)))"""
         @test fmt(str_, 2, 80) == str
         @test fmt(str_, 2, 38) == str
@@ -2309,7 +2544,7 @@ end
         str = """
         transcode(::Type{THISISONESUPERLONGTYPE1234567}) where {T<:Union{
           Int32,
-          UInt32
+          UInt32,
         }} =
           transcode(T, String(Vector(src)))"""
         @test fmt(str_, 2, 37) == str
@@ -2318,7 +2553,7 @@ end
         str = """
         transcode(
           ::Type{T},
-          src::AbstractVector{UInt8}
+          src::AbstractVector{UInt8},
         ) where {T<:Union{Int32,UInt32}} = transcode(T, String(Vector(src)))"""
         @test fmt(str_, 2, 80) == str
         @test fmt(str_, 2, 68) == str
@@ -2326,16 +2561,48 @@ end
         str = """
         transcode(
           ::Type{T},
-          src::AbstractVector{UInt8}
+          src::AbstractVector{UInt8},
         ) where {T<:Union{Int32,UInt32}} =
           transcode(T, String(Vector(src)))"""
         @test fmt(str_, 2, 67) == str
+
+        # issue 56
+        str_ = "a_long_function_name(Array{Float64,2}[[1.0], [0.5 0.5], [0.5 0.5; 0.5 0.5], [0.5 0.5; 0.5 0.5]])"
+        str = """
+        a_long_function_name(Array{Float64,2}[
+            [1.0],
+            [0.5 0.5],
+            [0.5 0.5; 0.5 0.5],
+            [0.5 0.5; 0.5 0.5],
+        ])"""
+        @test fmt(str, 4, length(str)) == str_
+        @test fmt(str_) == str
+
+        # unary op
+        str_ = "[1, 1]'"
+        str = """
+        [
+         1,
+         1,
+        ]'"""
+        @test fmt(str, 4, length(str)) == str_
+        @test fmt(str_, 4, length(str_) - 1) == str
+    end
+
+    @testset "Trailing zeros" begin
+        @test fmt("1.") == "1.0"
+        @test fmt("a * 1. + b") == "a * 1.0 + b"
+        @test fmt("1. + 2. * im") == "1.0 + 2.0 * im"
+        @test fmt("[1., 2.]") == "[1.0, 2.0]"
+        @test fmt("""
+        1. +
+            2.
+        """) == "1.0 + 2.0\n"
     end
 
 # @testset "meta-format" begin
 #     str = String(read("./runtests.jl"))
 #     str = fmt(str)
-#
 # end
 
 end