diff --git a/HISTORY_v2.md b/HISTORY_v2.md index e772f00a8..a2bb39c9d 100644 --- a/HISTORY_v2.md +++ b/HISTORY_v2.md @@ -1,3 +1,7 @@ +# v2.11.5 + +Fixed more buggy cases of YASStyle indentation (some of which were regressions in v2.11.1). (#1230, #1231) + # v2.11.4 Fixed a bug where comments from outside docstrings would be incorrectly duplicated inside the docstring if the vertical length of the docstring changed during formatting. (#1223, #1229) diff --git a/Project.toml b/Project.toml index 5a8c9d601..17020fcc0 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "JuliaFormatter" uuid = "98e50ef6-434e-11e9-1051-2b60c6c9e899" -version = "2.11.4" +version = "2.11.5" authors = ["Dominique Luna and contributors"] [workspace] diff --git a/src/fst.jl b/src/fst.jl index 467e978f2..18f5aff35 100644 --- a/src/fst.jl +++ b/src/fst.jl @@ -511,6 +511,113 @@ function is_import_expr(x::FST) return x.typ in (Import, Using, Export, Public) end +""" + is_indent_nest(fst::FST) + +Whether nesting the binary call `fst` at its operator indents the RHS by one level from the +start of the line: + +```julia +aaaaaaaaaaaa => + rhs +``` + +as opposed to bringing it back to the column the LHS starts at, which is what every other +operator does: + +```julia +aaaaaaaaaaaa in +rhs +``` + +True for assignments, `=>`, `->` and standalone short-circuits. +""" +function is_indent_nest(fst::FST) + md = fst.metadata + return (!isnothing(md) && (md::Metadata).is_short_form_function) || + is_assignment(fst) || + op_kind(fst) in KSet"=> ->" || + (!isnothing(md) && (md::Metadata).is_standalone_shortcircuit) +end + +""" + is_column_aligned(fst::FST) + +Whether YASStyle lays out the continuation lines of `fst` relative to the column at which +`fst` itself starts, rather than relative to the start of the line it appears on. + +Iterables are column-aligned, because their arguments line up with the bracket that opens +them, as is `where`, whose type parameters line up with the `{`: + +```julia +f(aaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbb) + +Foo{A} where {Aaaaaaaaaaaaaaa<:Xxxxxxxxxxxx, + Bbbbbbbbbbbbbbb<:Yyyyyyyyyyyy} +``` + +Block-like constructs are not, because their bodies are indented by one level from the +start of the line, regardless of where `function f()` starts: + +```julia +function f() + body +end +``` + +For nested constructs we might need to inspect their children. For example in + +```julia +@mymacro [aaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb] +``` + +the second line is indented according to the `[` iff the last argument of the +macro is column-aligned. + +A binary call is column-aligned unless its operator is one of those that indents the RHS +from the start of the line instead (see [`is_indent_nest`](@ref)). + +`Chain`, `Comparison` and `Conditional` are column-aligned in YASStyle too, but they are +deliberately not listed here because the only caller (`n_binaryopcall!`) accounts for their +full width before it will consider undoing a nesting, so it never has to move one of them +that has already been split over several lines. +""" +function is_column_aligned(fst::FST) + return if is_leaf(fst) + false + elseif fst.typ === Binary + nodes = fst.nodes::Vector{FST} + if any(n -> n.typ === NEWLINE, nodes) + # It has been split across its own operator, so `n_binaryopcall!` decided + # where the RHS goes. + !is_indent_nest(fst) + else + # It is still on one line, so any continuation lines belong to the RHS + # operand. In `@mac aaaa = [bbbbb, ccccc]` it is the vector literal, not the + # assignment, that the second line is aligned against. + is_column_aligned(nodes[end]) + end + elseif fst.typ === MacroBlock + # The trailing argument is the one that continuation lines follow on from. + nodes = fst.nodes::Vector{FST} + !isempty(nodes) && is_column_aligned(nodes[end]) + elseif fst.typ === Unary + # Either `!x` or `x...`; in both cases it's the operand that matters, not the + # operator. + nodes = fst.nodes::Vector{FST} + idx = findfirst(n -> n.typ !== OPERATOR, nodes) + idx !== nothing && is_column_aligned(nodes[idx]) + elseif fst.typ === Where # `where {...}` + true + elseif is_iterable(fst) + true + else + false + end +end + """ Returns whether `fst` can be an iterable argument. For example in the case of a function call, which is of type `Call`: diff --git a/src/styles/default/nest.jl b/src/styles/default/nest.jl index 4e2fbd7d2..4eba1c646 100644 --- a/src/styles/default/nest.jl +++ b/src/styles/default/nest.jl @@ -917,14 +917,7 @@ function n_binaryopcall!( fst[i1] = Newline(; length = fst[i1].len) nested = true - indent_nest = - (!isnothing(fst.metadata) && (fst.metadata::Metadata).is_short_form_function) || - is_assignment(fst) || - op_kind(fst) in KSet"=> ->" || - ( - !isnothing(fst.metadata) && - (fst.metadata::Metadata).is_standalone_shortcircuit - ) + indent_nest = is_indent_nest(fst) if indent_nest s.line_offset = fst.indent + s.opts.indent @@ -943,7 +936,12 @@ function n_binaryopcall!( end end - # rhs + # RHS. + + # Remember the column the RHS starts at in this (nested) layout. Any alignment + # that nesting the RHS sets up is relative to this column, so if the nesting is + # undone further below we need to know how far the RHS has moved sideways. + rhs_line_offset_when_op_nested = s.line_offset fst[end].extra_margin = fst.extra_margin nest!(style, fst[end], s, lineage) @@ -993,22 +991,28 @@ function n_binaryopcall!( fst[i2] = Whitespace(0) line_offset = s.line_offset walk(unnest!(style; dedent = true), rhs, s) - # Iterables in YASStyle need to be aligned with the - # open bracket - if style isa YASStyle - if is_unnamed_iterable(rhs) - extra_indent = if !isempty(rhs.nodes) && is_opener(rhs[1]) - line_offset - rhs.indent + 1 - else - line_offset - rhs.indent - end - add_indent!(rhs, s, extra_indent) - elseif is_named_iterable(rhs) - extra_indent = - line_offset - rhs.indent + length(rhs[1]) + length(rhs[2]) - add_indent!(rhs, s, extra_indent) - end + # The RHS may span multiple lines, in which case we need to decide + # how to reindent its continuation lines. + # + # If the RHS aligns its continuation lines against the column it + # starts at (see `is_column_aligned`), then we need to reuse the + # `rhs_line_offset_when_op_nested` that we calculated earlier. + # + # When we undo the nesting, the rhs gets moved to column `line_offset`, + # so every indent inside it has to be offset by the difference with + # `rhs_line_offset_when_op_nested`. + # + # `s.opts.indent` is added back on because `unnest!` above dedented the + # whole RHS by one level. + if style isa YASStyle && is_column_aligned(rhs) + add_indent!( + rhs, + s, + line_offset - rhs_line_offset_when_op_nested + s.opts.indent, + ) end + # For default style, nothing is 'column aligned' so we don't need to do + # anything. end end end diff --git a/test/yas_style.jl b/test/yas_style.jl index 842340b71..132947630 100644 --- a/test/yas_style.jl +++ b/test/yas_style.jl @@ -801,6 +801,86 @@ using JuliaFormatter: format_text test_format(s2, s2, YASStyle(); margin=margin) end end + + @testset "realignment of RHS after un-nesting a binary op" begin + s_ = """ + for x in @mac [aaaaa, bbbbb, ccccc] + foo + end""" + s = """ + for x in @mac [aaaaa, bbbbb, + ccccc] + foo + end""" + test_format(s_, s, YASStyle(); margin=30) + + s_ = """ + for x in @mac f(aaaaa, bbbbb, ccccc) + foo + end""" + s = """ + for x in @mac f(aaaaa, bbbbb, + ccccc) + foo + end""" + test_format(s_, s, YASStyle(); margin=31) + + s_ = """ + ccc => @mac function () + return aaa + end""" + s = """ + ccc => @mac function () + return aaa + end""" + test_format(s_, s, YASStyle(); margin=24) + + s_ = """ + for x in yyy::Foo(aaaaa, bbbbb, ccccc) + foo + end""" + s = """ + for x in yyy::Foo(aaaaa, bbbbb, + ccccc) + + foo + end""" + test_format(s_, s, YASStyle(); margin=33) + + s_ = """ + for x in @mac aaaa = [bbbbbb, cccccc, dddddd] + foo + end""" + s = """ + for x in @mac aaaa = [bbbbbb, cccccc, + dddddd] + foo + end""" + test_format(s_, s, YASStyle(); margin=39) + + s_ = """ + for x in !(aaaa == 1 && (bbbb(cc) || dddd)) + foo + end""" + s = """ + for x in !(aaaa == 1 && + (bbbb(cc) || dddd)) + foo + end""" + test_format(s_, s, YASStyle(); margin=24) + + s_ = """ + for x in Foo{A} where {Aa<:Xx, Bb<:Yy, Cc<:Zz} + foo + end""" + s = """ + for x in Foo{A} where {Aa<:Xx,Bb<:Yy, + Cc<:Zz} + + foo + end""" + test_format(s_, s, YASStyle(); margin=38) + end end end