From 82596772b5699772565ceed1dc6e03bd7173136c Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Mon, 4 May 2026 14:57:08 +0200 Subject: [PATCH 01/27] switch to juliasyntax v1 --- Project.toml | 2 +- src/JuliaFormatter.jl | 4 +- src/fst.jl | 43 ++++- src/passes.jl | 3 + src/styles/default/pretty.jl | 303 ++++++++++++++++++++++++++++++++--- src/styles/yas/pretty.jl | 9 +- 6 files changed, 332 insertions(+), 32 deletions(-) diff --git a/Project.toml b/Project.toml index b38498062..abaf0b406 100644 --- a/Project.toml +++ b/Project.toml @@ -13,7 +13,7 @@ TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" [compat] CommonMark = "0.5, 0.6, 0.7, 0.8, 0.9, 0.10" Glob = "1.3" -JuliaSyntax = "^0.4.10" +JuliaSyntax = "1" PrecompileTools = "1" TOML = "1" Test = "1" diff --git a/src/JuliaFormatter.jl b/src/JuliaFormatter.jl index c531a3034..13218fc80 100644 --- a/src/JuliaFormatter.jl +++ b/src/JuliaFormatter.jl @@ -7,7 +7,7 @@ end using PrecompileTools: @setup_workload, @compile_workload using JuliaSyntax -using JuliaSyntax: haschildren, children, span, @K_str, kind, @KSet_str +using JuliaSyntax: children, span, @K_str, kind, @KSet_str using TOML: parsefile using Glob import CommonMark: block_modifier @@ -34,6 +34,8 @@ export format, SciMLStyle, MinimalStyle +haschildren(node::JuliaSyntax.GreenNode) = !JuliaSyntax.is_leaf(node) + struct Configuration args::Dict{String,Any} file::Dict{String,Any} diff --git a/src/fst.jl b/src/fst.jl index 7bfd6120e..2b8cb434a 100644 --- a/src/fst.jl +++ b/src/fst.jl @@ -278,7 +278,7 @@ function is_macrostr(t::JuliaSyntax.GreenNode)::Bool end function contains_macrostr(t::JuliaSyntax.GreenNode)::Bool - if kind(t) in KSet"StringMacroName CmdMacroName core_@cmd" + if kind(t) in KSet"StringMacroName CmdMacroName" return true elseif kind(t) === "quote" && haschildren(t) return contains_macrostr(t[1]) @@ -547,6 +547,21 @@ function _callinfo(x::JuliaSyntax.GreenNode) return 0, 0 end k = kind(x) + if k === K"call" && JuliaSyntax.is_infix_op_call(x) + args = count(n -> !JuliaSyntax.is_whitespace(n), children(x)) + return div(args - 1, 2), div(args + 1, 2) + elseif k === K"dotcall" && JuliaSyntax.is_infix_op_call(x) + args = count(n -> !JuliaSyntax.is_whitespace(n), children(x)) + nops = div(args - 1, 3) + return nops, nops + 1 + elseif k === K"op=" + return 1, 2 + elseif JuliaSyntax.is_operator(x) && haschildren(x) + args = count(n -> !JuliaSyntax.is_whitespace(n), children(x)) + if args >= 3 + return div(args - 1, 2), div(args + 1, 2) + end + end n_operators = 0 n_args = 0 @@ -565,7 +580,7 @@ function _callinfo(x::JuliaSyntax.GreenNode) end function is_unary(x::JuliaSyntax.GreenNode) - if JuliaSyntax.is_unary_op(x) + if JuliaSyntax.is_unary_op(x) && !haschildren(x) return true end if kind(x) in KSet"call dotcall" || (JuliaSyntax.is_operator(x) && haschildren(x)) @@ -646,6 +661,13 @@ function is_function_or_macro_def(cst::JuliaSyntax.GreenNode) return false end +function is_short_function_def(cst::JuliaSyntax.GreenNode) + kind(cst) === K"function" && haschildren(cst) || return false + idx = findfirst(n -> !JuliaSyntax.is_whitespace(n), children(cst)) + isnothing(idx) && return false + return kind(cst[idx]) !== K"function" +end + function is_function_like_lhs(node::JuliaSyntax.GreenNode) k = kind(node) if k in KSet"call dotcall" @@ -813,6 +835,23 @@ function eq_to_in_normalization!(fst::FST, always_for_in::Bool, for_in_replaceme op.val = "=" op.len = length(op.val) end + if !isnothing(fst.metadata) + metadata = fst.metadata::Metadata + opkind = try + JuliaSyntax.Kind(op.val) + catch + metadata.op_kind + end + fst.metadata = Metadata( + opkind, + metadata.op_dotted, + metadata.is_standalone_shortcircuit, + metadata.is_short_form_function, + op.val == "=", + metadata.is_long_form_function, + metadata.has_multiline_argument, + ) + end elseif fst.typ === Block || fst.typ === Brackets || fst.typ === Filter past_if = false for n in fst.nodes::Vector diff --git a/src/passes.jl b/src/passes.jl index f467cfee5..48a68a8e2 100644 --- a/src/passes.jl +++ b/src/passes.jl @@ -145,6 +145,9 @@ function pipe_to_function_call(fst::FST) arg2[end][end].typ === IDENTIFIER n = FST(PUNCTUATION, -1, arg2.endline, arg2.endline, ".") push!(nodes, n) + elseif dot && arg2.typ === Accessor && arg2[end].typ === IDENTIFIER + n = FST(PUNCTUATION, -1, arg2.endline, arg2.endline, ".") + push!(nodes, n) elseif dot && arg2.typ === Brackets idx = findfirst(n -> n.typ === Binary && op_kind(n) === K"->", arg2.nodes) if idx !== nothing diff --git a/src/styles/default/pretty.jl b/src/styles/default/pretty.jl index 2b0107a7e..4987333ac 100644 --- a/src/styles/default/pretty.jl +++ b/src/styles/default/pretty.jl @@ -21,6 +21,116 @@ function newctx(s::PrettyContext; kwargs...) PrettyContext(values...) end +function source_kind(s::State, cst::JuliaSyntax.GreenNode, offset::Int) + span(cst) == 0 && return nothing + val = getsrcval(s.doc, offset:(offset+span(cst)-1)) + try + return JuliaSyntax.Kind(val) + catch + return nothing + end +end + +function source_operator_kind(s::State, cst::JuliaSyntax.GreenNode, offset::Int) + if JuliaSyntax.is_operator(cst) && !haschildren(cst) + return kind(cst) + elseif kind(cst) === K"Identifier" && !haschildren(cst) + k = source_kind(s, cst, offset) + if !isnothing(k) && JuliaSyntax.is_operator(k) + return k + end + end + return nothing +end + +function is_source_operator(s::State, cst::JuliaSyntax.GreenNode, offset::Int) + !isnothing(source_operator_kind(s, cst, offset)) +end + +function source_prefix_operator_index(cst::JuliaSyntax.GreenNode, s::State) + kind(cst) in KSet"call dotcall" && haschildren(cst) || return nothing + childs = children(cst) + args = findall(n -> !JuliaSyntax.is_whitespace(n), childs) + isempty(args) && return nothing + + op_arg = if kind(cst) === K"dotcall" && + length(args) >= 2 && + kind(childs[args[1]]) === K"." && + !haschildren(childs[args[1]]) + args[2] + else + args[1] + end + offset = s.offset + sum(span, childs[1:(op_arg-1)]; init = 0) + return is_source_operator(s, childs[op_arg], offset) ? op_arg : nothing +end + +function is_source_prefix_op_call(cst::JuliaSyntax.GreenNode, s::State) + !isnothing(source_prefix_operator_index(cst, s)) +end + +function source_op_kind( + s::State, + cst::JuliaSyntax.GreenNode, + op_indices::Vector{Int}, +) + opkind = op_kind(cst) + opkind !== K"None" && opkind !== K"Identifier" && return opkind + + offset = s.offset + childs = children(cst) + for (i, c) in enumerate(childs) + if i in op_indices + k = source_operator_kind(s, c, offset) + if !isnothing(k) && !(kind(cst) === K"dotcall" && k === K".") + return k + end + end + offset += span(c) + end + return opkind +end + +function update_operator_indices(childs::Vector{JuliaSyntax.GreenNode{T}}) where {T} + args = findall(n -> !JuliaSyntax.is_whitespace(n), childs) + length(args) < 4 && return Int[] + return args[2:(end-1)] +end + +function do_block_index(childs::Vector{JuliaSyntax.GreenNode{T}}) where {T} + findfirst(n -> kind(n) === K"do" && haschildren(n), childs) +end + +function has_do_block_call(cst::JuliaSyntax.GreenNode) + kind(cst) in KSet"call dotcall" && haschildren(cst) || + return false + !isnothing(do_block_index(children(cst))) +end + +function call_args(childs::Vector{JuliaSyntax.GreenNode{T}}) where {T} + idx = findfirst(n -> kind(n) in KSet"( { [", childs) + start = isnothing(idx) ? 1 : idx + 1 + get_args(childs[start:end]) +end + +function last_code_child(cst::JuliaSyntax.GreenNode) + haschildren(cst) || return nothing + idx = findlast(n -> !JuliaSyntax.is_whitespace(n), children(cst)) + isnothing(idx) ? nothing : children(cst)[idx] +end + +function iteration_rhs(cst::JuliaSyntax.GreenNode) + rhs = last_code_child(cst) + if kind(cst) === K"iteration" && !isnothing(rhs) && haschildren(rhs) + rhs = last_code_child(rhs) + end + rhs +end + +function iteration_has_comma(cst::JuliaSyntax.GreenNode) + haschildren(cst) && any(n -> kind(n) === K",", children(cst)) +end + function pretty( ds::AbstractStyle, node::JuliaSyntax.GreenNode, @@ -34,6 +144,9 @@ function pretty( ret = if k == K"Identifier" && !haschildren(node) p_identifier(style, node, s, ctx, lineage) + elseif k === K"Placeholder" + s.offset += span(node) + FST(NONE, 0, 0, 0, "") elseif JuliaSyntax.is_operator(node) && !haschildren(node) p_operator(style, node, s, ctx, lineage) elseif k == K"Comment" @@ -58,9 +171,11 @@ function pretty( p_begin(style, node, s, ctx, lineage) elseif k === K"block" p_block(style, node, s, ctx, lineage) + elseif is_short_function_def(node) + p_binaryopcall(style, node, s, ctx, lineage) elseif k === K"function" p_functiondef(style, node, s, ctx, lineage) - elseif k in KSet"MacroName StringMacroName CmdMacroName core_@cmd" + elseif k in KSet"MacroName StringMacroName CmdMacroName" p_macroname(style, node, s, ctx, lineage) elseif k === K"macro" p_macro(style, node, s, ctx, lineage) @@ -102,7 +217,7 @@ function pretty( p_bracescat(style, node, s, ctx, lineage) elseif k === K"tuple" p_tuple(style, node, s, ctx, lineage) - elseif k === K"cartesian_iterator" + elseif k === K"iteration" p_cartesian_iterator(style, node, s, ctx, lineage) elseif k === K"parens" p_invisbrackets(style, node, s, ctx, lineage) @@ -118,6 +233,10 @@ function pretty( p_whereopcall(style, node, s, ctx, lineage) elseif k === K"?" && haschildren(node) p_conditionalopcall(style, node, s, ctx, lineage) + elseif has_do_block_call(node) + p_do_call(style, node, s, ctx, lineage) + elseif is_source_prefix_op_call(node, s) + p_unaryopcall(style, node, s, ctx, lineage) elseif is_binary(node) p_binaryopcall(style, node, s, ctx, lineage) elseif is_chain(node) @@ -1426,10 +1545,18 @@ function p_for( add_node!(t, pretty(style, c, s, ctx, lineage), s) else add_node!(t, Whitespace(1), s) - n = if kind(c) === K"cartesian_iterator" - s.indent += s.opts.indent + n = if kind(c) === K"iteration" + rhs_is_iterable = !iteration_has_comma(c) && is_iterable(iteration_rhs(c)) + if !rhs_is_iterable + s.indent += s.opts.indent + end n = pretty(style, c, s, newctx(ctx; from_for = true), lineage) - s.indent -= s.opts.indent + if !rhs_is_iterable + s.indent -= s.opts.indent + end + if rhs_is_iterable + ends_in_iterable = true + end n else n = pretty(style, c, s, newctx(ctx; from_for = true), lineage) @@ -1462,8 +1589,14 @@ function p_cartesian_iterator( end childs = children(cst) + has_comma = iteration_has_comma(cst) for (i, c) in enumerate(childs) n = pretty(style, c, s, ctx, lineage) + if has_comma && kind(c) in KSet"in ∈" && is_iterable(iteration_rhs(c)) + if n.typ === Binary && n[end].typ === Vect + n[end].nest_behavior = AlwaysNest + end + end if kind(c) === K"," add_node!(t, n, s; join_lines = true) if needs_placeholder(childs, i + 1, K")") @@ -1494,9 +1627,8 @@ function p_while( t end -# Do -# node [nodes] do [nodes] node node end -function p_do( +function append_do_nodes!( + t::FST, ds::AbstractStyle, cst::JuliaSyntax.GreenNode, s::State, @@ -1504,7 +1636,6 @@ function p_do( lineage::Vector{Tuple{JuliaSyntax.Kind,Bool,Bool}}, ) style = getstyle(ds) - t = FST(Do, nspaces(s)) if !haschildren(cst) return t end @@ -1534,6 +1665,43 @@ function p_do( t end +# Do +# node [nodes] do [nodes] node node end +function p_do( + ds::AbstractStyle, + cst::JuliaSyntax.GreenNode, + s::State, + ctx::PrettyContext, + lineage::Vector{Tuple{JuliaSyntax.Kind,Bool,Bool}}, +) + t = FST(Do, nspaces(s)) + append_do_nodes!(t, ds, cst, s, ctx, lineage) +end + +function p_do_call( + ds::AbstractStyle, + cst::JuliaSyntax.GreenNode, + s::State, + ctx::PrettyContext, + lineage::Vector{Tuple{JuliaSyntax.Kind,Bool,Bool}}, +) + t = FST(Do, nspaces(s)) + childs = children(cst) + idx = do_block_index(childs) + if isnothing(idx) + return p_call(ds, cst, s, ctx, lineage) + end + + add_node!(t, p_call(ds, cst, s, ctx, lineage; child_limit = idx - 1), s; join_lines = true) + + do_node = childs[idx] + push!(lineage, (kind(do_node), is_iterable(do_node), is_assignment(do_node))) + append_do_nodes!(t, ds, do_node, s, ctx, lineage) + pop!(lineage) + + t +end + # Try function p_try( ds::AbstractStyle, @@ -1583,7 +1751,7 @@ function p_try( ) elseif !JuliaSyntax.is_whitespace(c) # "catch" vs "catch ..." - if !(kind(cst) === K"catch" && any(n -> kind(n) === K"false", childs)) + if !(kind(cst) === K"catch" && any(n -> kind(n) === K"Placeholder", childs)) add_node!(t, Whitespace(1), s) end add_node!(t, pretty(style, c, s, ctx, lineage), s; join_lines = true) @@ -1688,9 +1856,14 @@ function p_kw( add_node!(t, pretty(style, c, s, ctx, lineage), s; join_lines = true) add_node!(t, Whitespace(1), s) else + source_prefix = is_source_prefix_op_call(c, s) n = pretty(style, c, s, ctx, lineage) if !s.opts.whitespace_in_kwargs && - ((n.typ === IDENTIFIER && endswith(n.val, "!")) || (is_prefix_op_call(c))) + ( + (n.typ === IDENTIFIER && endswith(n.val, "!")) || + is_prefix_op_call(c) || + source_prefix + ) add_node!( t, FST(PUNCTUATION, -1, n.startline, n.startline, "("), @@ -1728,7 +1901,10 @@ function p_binaryopcall( return t end - opkind = op_kind(cst) + childs = children(cst) + op_indices = kind(cst) === K"op=" ? update_operator_indices(childs) : + extract_operator_indices(childs) + opkind = source_op_kind(s, cst, op_indices) nonest = ctx.nonest || opkind === K":" @@ -1737,6 +1913,12 @@ function p_binaryopcall( (t.nest_behavior = AlwaysNest) end nest = (is_binaryop_nestable(style, cst) && !nonest) || nrhs + if opkind === K"=>" && haschildren(cst) + rhs_idx = findlast(n -> !JuliaSyntax.is_whitespace(n), childs) + if !isnothing(rhs_idx) && is_str_or_cmd(childs[rhs_idx]) + nest = false + end + end is_short_form_function = defines_function(cst) && !ctx.from_let op_dotted = kind(cst) === K"dotcall" @@ -1762,13 +1944,12 @@ function p_binaryopcall( op_dotted, lazy_op && standalone_binary_circuit, is_short_form_function, - is_assignment(cst), + is_assignment(cst) || defines_function(cst), false, false, ) has_ws = false - childs = children(cst) for (i, c) in enumerate(childs) if i > 1 && kind(c) in KSet"Whitespace NewlineWs" @@ -1778,6 +1959,7 @@ function p_binaryopcall( end from_colon = ctx.from_colon + from_typedef = ctx.from_typedef || any(x -> x[1] === K"where", lineage) nospace = ctx.nospace if opkind === K":" @@ -1785,9 +1967,15 @@ function p_binaryopcall( from_colon = true elseif opkind in KSet"::" nospace = true + elseif is_short_form_function && opkind === K"=" + nospace = false + has_ws = true elseif opkind in KSet"in ∈ isa ." nospace = false - elseif ctx.from_typedef && opkind in KSet"<: >:" + elseif opkind in KSet"=> ->" || (opkind === K"|>" && !(ctx.from_ref || from_colon)) + nospace = false + has_ws = true + elseif from_typedef && opkind in KSet"<: >:" if s.opts.whitespace_typedefs nospace = false has_ws = true @@ -1817,9 +2005,32 @@ function p_binaryopcall( nlws_count = 0 after_op = false - op_indices = extract_operator_indices(childs) - + skip_until = 0 for (i, c) in enumerate(childs) + i <= skip_until && continue + if kind(cst) === K"op=" && !isempty(op_indices) && i == first(op_indices) + loc = cursor_loc(s) + op_span = sum(span, childs[first(op_indices):last(op_indices)]; init = 0) + val = getsrcval(s.doc, s.offset:(s.offset+op_span-1)) + s.offset += op_span + n = FST(OPERATOR, loc[2], loc[1], loc[1], val) + n.metadata = Metadata(K"op=", startswith(val, ".")) + if nws > 0 && i > 1 + add_node!(t, Whitespace(nws), s) + end + add_node!(t, n, s; join_lines = true) + if nws > 0 + if nest + add_node!(t, Placeholder(nws), s) + else + add_node!(t, Whitespace(nws), s) + end + end + after_op = true + skip_until = last(op_indices) + continue + end + offset = s.offset n = pretty( style, c, @@ -1836,6 +2047,11 @@ function p_binaryopcall( ) is_dot = kind(c) === K"." + is_op = i in op_indices && is_source_operator(s, c, offset) + if is_op && n.typ === IDENTIFIER + n.typ = OPERATOR + n.metadata = Metadata(source_operator_kind(s, c, offset)::JuliaSyntax.Kind, is_dot) + end if is_dot && haschildren(c) && length(children(c)) == 2 # [.] # . @@ -1857,7 +2073,7 @@ function p_binaryopcall( end after_op = true # elseif (kind(c) === opkind || kind(c) === K".") && !haschildren(c) - elseif JuliaSyntax.is_operator(c) && !haschildren(c) && i in op_indices + elseif is_op && !haschildren(c) # there are some weird cases where we can assign an operator a value so that # the arguments are operators as well. # @@ -1935,7 +2151,11 @@ function p_binaryopcall( end end - if nest && (length(op_indices) == 1 || (length(op_indices) == 2 && has_dot)) + if nest && ( + kind(cst) === K"op=" || + length(op_indices) == 1 || + (length(op_indices) == 2 && has_dot) + ) # for indent, will be converted to `indent` if needed insert!(t.nodes::Vector{FST}, length(t.nodes::Vector{FST}), Placeholder(0)) end @@ -2060,16 +2280,31 @@ function p_unaryopcall( return t end - opkind = op_kind(cst) + childs = children(cst) + first_idx = findfirst(n -> !JuliaSyntax.is_whitespace(n), childs) + op_idx = source_prefix_operator_index(cst, s) + if isnothing(op_idx) + op_idx = first_idx + end + opkind = isnothing(op_idx) ? op_kind(cst) : source_op_kind(s, cst, Int[op_idx]) op_dotted = kind(cst) === K"dotcall" t.metadata = Metadata(opkind, op_dotted) - for (i, c) in enumerate(children(cst)) + for (i, c) in enumerate(childs) + offset = s.offset if i > 1 && kind(c) in KSet"Whitespace" add_node!(t, Whitespace(1), s) end - add_node!(t, pretty(style, c, s, ctx, lineage), s; join_lines = true) + n = pretty(style, c, s, ctx, lineage) + if i == op_idx && n.typ === IDENTIFIER + k = source_operator_kind(s, c, offset) + if !isnothing(k) + n.typ = OPERATOR + n.metadata = Metadata(k, false) + end + end + add_node!(t, n, s; join_lines = true) end t end @@ -2129,6 +2364,8 @@ function p_call( s::State, ctx::PrettyContext, lineage::Vector{Tuple{JuliaSyntax.Kind,Bool,Bool}}, + ; + child_limit::Union{Int,Nothing} = nothing, ) style = getstyle(ds) t = FST(Call, nspaces(s)) @@ -2136,14 +2373,23 @@ function p_call( return t end - args = get_args(cst) + childs = children(cst) + if isnothing(child_limit) + idx = do_block_index(childs) + if !isnothing(idx) + childs = childs[1:(idx-1)] + end + else + childs = childs[1:child_limit] + end + + args = call_args(childs) nest = length(args) > 0 && !( length(args) == 1 && (unnestable_node(args[1]) || s.opts.disallow_single_arg_nesting) ) - childs = children(cst) for (i, a) in enumerate(childs) k = kind(a) n = if k == K"=" && haschildren(a) @@ -2991,9 +3237,11 @@ function p_generator( childs = children(cst) has_for_kw = findfirst(n -> kind(n) === K"for", childs) !== nothing + from_for = has_for_kw || ctx.from_for + past_if = false for (i, a) in enumerate(childs) - n = pretty(style, a, s, newctx(ctx; from_for = has_for_kw), lineage) + n = pretty(style, a, s, newctx(ctx; from_for = from_for), lineage) if JuliaSyntax.is_keyword(a) && !haschildren(a) # for keyword can only be on the following line # if this expression is within an iterable expression @@ -3014,11 +3262,14 @@ function p_generator( add_node!(t, n, s; join_lines = true) end - if has_for_kw + if from_for && !past_if eq_to_in_normalization!(n, s.opts.always_for_in, s.opts.for_in_replacement) else false end + if kind(a) === K"if" && JuliaSyntax.is_keyword(a) && !haschildren(a) + past_if = true + end end t end diff --git a/src/styles/yas/pretty.jl b/src/styles/yas/pretty.jl index 622dff4ce..4fd66e108 100644 --- a/src/styles/yas/pretty.jl +++ b/src/styles/yas/pretty.jl @@ -872,9 +872,11 @@ function p_generator( childs = children(cst) has_for_kw = findfirst(n -> kind(n) === K"for", childs) !== nothing + from_for = has_for_kw || ctx.from_for + past_if = false for (i, a) in enumerate(childs) - n = pretty(style, a, s, newctx(ctx; from_for = has_for_kw), lineage) + n = pretty(style, a, s, newctx(ctx; from_for = from_for), lineage) if JuliaSyntax.is_keyword(a) && !haschildren(a) idx = findprev(n -> !JuliaSyntax.is_whitespace(n), childs, i - 1) if !isnothing(idx) && is_block(childs[idx]) @@ -895,11 +897,14 @@ function p_generator( add_node!(t, n, s; join_lines = true) end - if has_for_kw + if from_for && !past_if eq_to_in_normalization!(n, s.opts.always_for_in, s.opts.for_in_replacement) else false end + if kind(a) === K"if" && JuliaSyntax.is_keyword(a) && !haschildren(a) + past_if = true + end end t From 5cf92e5e68d75acab85b8cfd57be390ebb946618 Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 5 May 2026 10:17:27 +0200 Subject: [PATCH 02/27] fix edge cases --- src/styles/default/pretty.jl | 45 ++++++++++++++++++++++++++++++++---- src/styles/yas/pretty.jl | 2 -- test/default_style.jl | 3 +++ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/styles/default/pretty.jl b/src/styles/default/pretty.jl index 4987333ac..bc37d1b47 100644 --- a/src/styles/default/pretty.jl +++ b/src/styles/default/pretty.jl @@ -47,6 +47,10 @@ function is_source_operator(s::State, cst::JuliaSyntax.GreenNode, offset::Int) !isnothing(source_operator_kind(s, cst, offset)) end +function is_source_unary_operator(k::JuliaSyntax.Kind) + JuliaSyntax.is_unary_op(JuliaSyntax.SyntaxHead(k, UInt16(0))) +end + function source_prefix_operator_index(cst::JuliaSyntax.GreenNode, s::State) kind(cst) in KSet"call dotcall" && haschildren(cst) || return nothing childs = children(cst) @@ -66,7 +70,40 @@ function source_prefix_operator_index(cst::JuliaSyntax.GreenNode, s::State) end function is_source_prefix_op_call(cst::JuliaSyntax.GreenNode, s::State) - !isnothing(source_prefix_operator_index(cst, s)) + op_idx = source_prefix_operator_index(cst, s) + isnothing(op_idx) && return false + + childs = children(cst) + op_offset = s.offset + sum(span, childs[1:(op_idx-1)]; init = 0) + opkind = source_operator_kind(s, childs[op_idx], op_offset) + (isnothing(opkind) || !is_source_unary_operator(opkind)) && return false + + call_idx = findfirst(n -> kind(n) in KSet"( { [", childs) + if !isnothing(call_idx) + args = 0 + for c in childs[(call_idx+1):end] + kind(c) === K"parameters" && return false + kind(c) in KSet", ..." && return false + if !( + is_punc(c) || + kind(c) == K";" || + JuliaSyntax.is_whitespace(c) || + kind(c) in KSet"` ``` \" \"\"\"" + ) + args += 1 + end + end + return args == 1 + end + + args = findall(n -> !JuliaSyntax.is_whitespace(n), childs) + operands = 0 + for idx in args + idx == op_idx && continue + kind(childs[idx]) === K"." && !haschildren(childs[idx]) && continue + operands += 1 + end + return operands == 1 end function source_op_kind( @@ -237,6 +274,8 @@ function pretty( p_do_call(style, node, s, ctx, lineage) elseif is_source_prefix_op_call(node, s) p_unaryopcall(style, node, s, ctx, lineage) + elseif !isnothing(source_prefix_operator_index(node, s)) + p_call(style, node, s, ctx, lineage) elseif is_binary(node) p_binaryopcall(style, node, s, ctx, lineage) elseif is_chain(node) @@ -1856,7 +1895,7 @@ function p_kw( add_node!(t, pretty(style, c, s, ctx, lineage), s; join_lines = true) add_node!(t, Whitespace(1), s) else - source_prefix = is_source_prefix_op_call(c, s) + source_prefix = !isnothing(source_prefix_operator_index(c, s)) n = pretty(style, c, s, ctx, lineage) if !s.opts.whitespace_in_kwargs && ( @@ -3264,8 +3303,6 @@ function p_generator( if from_for && !past_if eq_to_in_normalization!(n, s.opts.always_for_in, s.opts.for_in_replacement) - else - false end if kind(a) === K"if" && JuliaSyntax.is_keyword(a) && !haschildren(a) past_if = true diff --git a/src/styles/yas/pretty.jl b/src/styles/yas/pretty.jl index 4fd66e108..53a6bb84f 100644 --- a/src/styles/yas/pretty.jl +++ b/src/styles/yas/pretty.jl @@ -899,8 +899,6 @@ function p_generator( if from_for && !past_if eq_to_in_normalization!(n, s.opts.always_for_in, s.opts.for_in_replacement) - else - false end if kind(a) === K"if" && JuliaSyntax.is_keyword(a) && !haschildren(a) past_if = true diff --git a/test/default_style.jl b/test/default_style.jl index 825338465..cf252497e 100644 --- a/test/default_style.jl +++ b/test/default_style.jl @@ -4559,6 +4559,9 @@ some_function( str_ = "a * %" str = "a * %" @test fmt(str_, 4, 100) == str + + @test run_pretty("+(y)", 80)[1].typ === JuliaFormatter.Unary + @test run_pretty(">=(y)", 80)[1].typ === JuliaFormatter.Call end @testset "binary shortcircuit" begin From 60a148ad0b865672367bdf386b2fa52a7700cb7b Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 5 May 2026 13:56:46 +0200 Subject: [PATCH 03/27] fix alignment --- src/styles/sciml/nest.jl | 11 ++++++++++- test/sciml_style.jl | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/styles/sciml/nest.jl b/src/styles/sciml/nest.jl index 974832e66..f00c480eb 100644 --- a/src/styles/sciml/nest.jl +++ b/src/styles/sciml/nest.jl @@ -268,9 +268,18 @@ function n_ref!( if is_lhs_of_assignment # Don't break the LHS of an assignment # Format children but keep them on the same line + nodes = fst.nodes::Vector{FST} + for (i, n) in enumerate(nodes) + if n.typ === NEWLINE && + (i == 1 || !is_comment(nodes[i-1])) && + (i == length(nodes) || !is_comment(nodes[i+1])) + nodes[i] = Whitespace(n.len) + end + end + lo = s.line_offset nested = false - for (i, n) in enumerate(fst.nodes) + for n in nodes nested |= nest!(ss, n, s, lineage) if n.typ !== NEWLINE # Prevent any newlines s.line_offset += length(n) diff --git a/test/sciml_style.jl b/test/sciml_style.jl index f8a44126f..783bfd25e 100644 --- a/test/sciml_style.jl +++ b/test/sciml_style.jl @@ -36,6 +36,21 @@ formatted = format_text(str, SciMLStyle()) @test !contains(formatted, "du[\n") @test startswith(strip(formatted), "du[i, j, 1]") + + str = raw""" + function update_entry!() + result_array[row, + column] = previous_array[row, column] + step_size * increment_array[row, column] + end + """ + formatted_str = raw""" + function update_entry!() + result_array[row, column] = previous_array[row, column] + + step_size * increment_array[row, column] + end + """ + @test format_text(str, SciMLStyle(); margin = 80) == formatted_str + str = raw""" @noinline require_complete(m::Matching) = m.inv_match === nothing && throw(ArgumentError("Backwards matching not defined. `complete` the matching first.")) """ From 7d8ca1c78ee56c27296953066b9d8c1993c0bbee Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 5 May 2026 14:23:46 +0200 Subject: [PATCH 04/27] fix --- src/styles/default/nest.jl | 14 +++++++++++++- test/sciml_style.jl | 27 +++++++++++++++++++++------ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/styles/default/nest.jl b/src/styles/default/nest.jl index a1fd78f3e..914f6efc1 100644 --- a/src/styles/default/nest.jl +++ b/src/styles/default/nest.jl @@ -951,6 +951,14 @@ function n_binaryopcall!( # Undo nest if possible if can_nest(fst) && !no_unnest(rhs) && !src_diff_line line_margin = s.line_offset + _, rhs_has_newline = length_to(rhs, (NEWLINE,)) + if op_kind(fst) === K"=>" && + rhs_has_newline && + fst[1].indent > 0 && + fst[1].line_offset <= fst[1].indent + 1 && + fst[1].indent < line_offset + line_margin -= line_offset - fst[1].indent + end # replace IN with all of precedence level 6 if (rhs.typ === Binary && !(op_kind(rhs) in KSet"in ::")) || @@ -978,7 +986,11 @@ function n_binaryopcall!( fst[i1] = Whitespace(1) if indent_nest || style isa YASStyle fst[i2] = Whitespace(0) - walk(unnest!(style; dedent = true), rhs, s) + unnest_style = + op_kind(fst) === K"=>" && + s.opts.yas_style_nesting && + !(style isa YASStyle) ? YASStyle(style) : style + walk(unnest!(unnest_style; dedent = true), rhs, s) end end end diff --git a/test/sciml_style.jl b/test/sciml_style.jl index 783bfd25e..0fd880026 100644 --- a/test/sciml_style.jl +++ b/test/sciml_style.jl @@ -38,15 +38,15 @@ @test startswith(strip(formatted), "du[i, j, 1]") str = raw""" - function update_entry!() - result_array[row, - column] = previous_array[row, column] + step_size * increment_array[row, column] + function update_cache!() + cache_table[key, + slot] = source_table[key, slot] + step * delta_table[key, slot] end """ formatted_str = raw""" - function update_entry!() - result_array[row, column] = previous_array[row, column] + - step_size * increment_array[row, column] + function update_cache!() + cache_table[key, slot] = source_table[key, slot] + + step * delta_table[key, slot] end """ @test format_text(str, SciMLStyle(); margin = 80) == formatted_str @@ -1136,6 +1136,21 @@ # Test 37: Pairs syntax str = "a => b" @test format_text(str, SciMLStyle()) == str + + str = raw""" + file_index = [ + "Data Structures" => [ + "Hash table implementation notes" => joinpath("src", + "hash_table.md"), + ], + ] + """ + formatted = format_text(str, SciMLStyle(); yas_style_nesting = true) + @test contains( + formatted, + "\"Hash table implementation notes\" => joinpath(\"src\",", + ) + @test !contains(formatted, "\"Hash table implementation notes\" =>\n") # Test 38: Quote expressions str = ":(x + y)" From 4688cb14dde979e8fe4c987825d5a2be7b38c6f5 Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 5 May 2026 14:40:02 +0200 Subject: [PATCH 05/27] fix more regressions from edge cases --- src/styles/sciml/nest.jl | 33 +++++++++++++++++ test/sciml_style.jl | 80 +++++++++++++++++++++++++--------------- 2 files changed, 84 insertions(+), 29 deletions(-) diff --git a/src/styles/sciml/nest.jl b/src/styles/sciml/nest.jl index f00c480eb..87627a892 100644 --- a/src/styles/sciml/nest.jl +++ b/src/styles/sciml/nest.jl @@ -44,6 +44,39 @@ end # nest!(style, fst[end], s, lineage) # end +function _is_for_tuple_binding( + fst::FST, + s::State, + lineage::Vector{Tuple{FNode,Union{Nothing,Metadata}}}, +) + length(lineage) >= 2 && + lineage[end-1][1] === CartesianIterator && + op_kind(fst) in KSet"in ∈" && + fst[1].typ === TupleN && + s.line_offset + length(fst[1]) <= s.opts.margin +end + +function n_binaryopcall!( + ss::SciMLStyle, + fst::FST, + s::State, + lineage::Vector{Tuple{FNode,Union{Nothing,Metadata}}}; + indent::Int = -1, +) + if _is_for_tuple_binding(fst, s, lineage) + lhs = fst[1] + nest_behavior = lhs.nest_behavior + lhs.nest_behavior = NeverNest + try + return n_binaryopcall!(DefaultStyle(getstyle(ss)), fst, s, lineage; indent) + finally + lhs.nest_behavior = nest_behavior + end + end + + n_binaryopcall!(DefaultStyle(getstyle(ss)), fst, s, lineage; indent) +end + function n_functiondef!( ss::SciMLStyle, fst::FST, diff --git a/test/sciml_style.jl b/test/sciml_style.jl index 0fd880026..8e4346010 100644 --- a/test/sciml_style.jl +++ b/test/sciml_style.jl @@ -37,20 +37,6 @@ @test !contains(formatted, "du[\n") @test startswith(strip(formatted), "du[i, j, 1]") - str = raw""" - function update_cache!() - cache_table[key, - slot] = source_table[key, slot] + step * delta_table[key, slot] - end - """ - formatted_str = raw""" - function update_cache!() - cache_table[key, slot] = source_table[key, slot] + - step * delta_table[key, slot] - end - """ - @test format_text(str, SciMLStyle(); margin = 80) == formatted_str - str = raw""" @noinline require_complete(m::Matching) = m.inv_match === nothing && throw(ArgumentError("Backwards matching not defined. `complete` the matching first.")) """ @@ -636,6 +622,57 @@ """ @test format_text(str, SciMLStyle(); margin = 13) == fstr end + + @testset "regression cases" begin + str = """ + function update_cache!() + cache_table[key, + slot] = source_table[key, slot] + step * delta_table[key, slot] + end + """ + + fstr = """ + function update_cache!() + cache_table[key, slot] = source_table[key, slot] + + step * delta_table[key, slot] + end + """ + @test format_text(str, SciMLStyle(); margin = 80) == fstr + + str = """ + path_lookup = [ + "Hash table implementation notes" => joinpath("src", + "hash_table.md"), + ] + """ + + fstr = """ + path_lookup = [ + "Hash table implementation notes" => joinpath("src", + "hash_table.md"), + ] + """ + @test format_text(str, SciMLStyle(); yas_style_nesting = true) == fstr + + str = """ + function copy_values!(destination::ThreadedArray, source::AbstractArray) + @threaded destination.scheduler for (dest_id, src_id) in zip(eachindex(destination), + eachindex(source)) + @inbounds destination[dest_id] = source[src_id] + end + end + """ + + fstr = """ + function copy_values!(destination::ThreadedArray, source::AbstractArray) + @threaded destination.scheduler for (dest_id, src_id) in zip(eachindex(destination), + eachindex(source)) + @inbounds destination[dest_id] = source[src_id] + end + end + """ + @test format_text(str, SciMLStyle(); yas_style_nesting = true) == fstr + end end str = raw""" @@ -1136,21 +1173,6 @@ # Test 37: Pairs syntax str = "a => b" @test format_text(str, SciMLStyle()) == str - - str = raw""" - file_index = [ - "Data Structures" => [ - "Hash table implementation notes" => joinpath("src", - "hash_table.md"), - ], - ] - """ - formatted = format_text(str, SciMLStyle(); yas_style_nesting = true) - @test contains( - formatted, - "\"Hash table implementation notes\" => joinpath(\"src\",", - ) - @test !contains(formatted, "\"Hash table implementation notes\" =>\n") # Test 38: Quote expressions str = ":(x + y)" From 3a04a9b510c3e375f1a88a5c11be5afbe5b53ce6 Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 5 May 2026 14:49:47 +0200 Subject: [PATCH 06/27] more regressions fixed --- src/styles/sciml/nest.jl | 13 ++++++++++++- test/sciml_style.jl | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/styles/sciml/nest.jl b/src/styles/sciml/nest.jl index 87627a892..894c3f08f 100644 --- a/src/styles/sciml/nest.jl +++ b/src/styles/sciml/nest.jl @@ -129,6 +129,13 @@ function n_macro!( n_functiondef!(ss, fst, s, lineage) end +function _is_multiline_typed_ref(fst::FST) + fst.typ === RefN && + length(fst.nodes::Vector) > 1 && + fst[1].typ === Curly && + any(n -> n.typ === NEWLINE, fst.nodes::Vector) +end + function _n_tuple!( ss::SciMLStyle, fst::FST, @@ -298,7 +305,7 @@ function n_ref!( end end - if is_lhs_of_assignment + if is_lhs_of_assignment && fst.extra_margin > 0 # Don't break the LHS of an assignment # Format children but keep them on the same line nodes = fst.nodes::Vector{FST} @@ -322,6 +329,10 @@ function n_ref!( return nested end + if _is_multiline_typed_ref(fst) + return n_ref!(YASStyle(getstyle(ss)), fst, s, lineage) + end + # Otherwise use the default behavior if s.opts.yas_style_nesting return n_ref!(YASStyle(getstyle(ss)), fst, s, lineage) diff --git a/test/sciml_style.jl b/test/sciml_style.jl index 8e4346010..c90d1a258 100644 --- a/test/sciml_style.jl +++ b/test/sciml_style.jl @@ -654,6 +654,22 @@ """ @test format_text(str, SciMLStyle(); yas_style_nesting = true) == fstr + str = """ + metadata = Pair{String, Any}["Start vertex" => first(graph.vertices), + "Final vertex" => last(graph.vertices), + "search algorithm" => traversal.algorithm |> typeof |> nameof, + "directed" => graph.is_directed] + """ + + fstr = """ + metadata = Pair{String, Any}["Start vertex" => first(graph.vertices), + "Final vertex" => last(graph.vertices), + "search algorithm" => traversal.algorithm |> typeof |> nameof, + "directed" => graph.is_directed] + """ + @test format_text(str, SciMLStyle()) == fstr + @test format_text(str, SciMLStyle(); yas_style_nesting = true) == fstr + str = """ function copy_values!(destination::ThreadedArray, source::AbstractArray) @threaded destination.scheduler for (dest_id, src_id) in zip(eachindex(destination), From ef2889baecc7544262b4a9604e5c3de9eafb2145 Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 5 May 2026 14:56:39 +0200 Subject: [PATCH 07/27] more regression with yas style nesting --- src/styles/sciml/nest.jl | 30 ++++++++++++++++++++++++++++++ test/sciml_style.jl | 30 ++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/styles/sciml/nest.jl b/src/styles/sciml/nest.jl index 894c3f08f..d17eb1211 100644 --- a/src/styles/sciml/nest.jl +++ b/src/styles/sciml/nest.jl @@ -136,6 +136,36 @@ function _is_multiline_typed_ref(fst::FST) any(n -> n.typ === NEWLINE, fst.nodes::Vector) end +function _has_multiline_do_args(fst::FST) + length(fst.nodes::Vector) >= 5 && + fst[4].typ === WHITESPACE && + !is_leaf(fst[5]) && + any(n -> n.typ === NEWLINE, fst[5].nodes::Vector) +end + +function n_do!( + ss::SciMLStyle, + fst::FST, + s::State, + lineage::Vector{Tuple{FNode,Union{Nothing,Metadata}}}, +) + style = getstyle(ss) + extra_margin = sum(length.(fst[2:3])) + if fst[4].typ === WHITESPACE + extra_margin += length(fst[4]) + if !_has_multiline_do_args(fst) + extra_margin += length(fst[5]) + end + end + fst[1].extra_margin = fst.extra_margin + extra_margin + + nested = false + nested |= nest!(style, fst[1], s, lineage) + nested |= + nest!(style, fst[2:end], s, fst.indent, lineage; extra_margin = fst.extra_margin) + return nested +end + function _n_tuple!( ss::SciMLStyle, fst::FST, diff --git a/test/sciml_style.jl b/test/sciml_style.jl index c90d1a258..d4b6320c2 100644 --- a/test/sciml_style.jl +++ b/test/sciml_style.jl @@ -688,6 +688,36 @@ end """ @test format_text(str, SciMLStyle(); yas_style_nesting = true) == fstr + + str = """ + function collect_close_items(points, min_distance) + foreach_neighbor(points, points, index; + backend=SerialBackend()) do item, neighbor, + _, _ + if item != neighbor + push!(result, item) + end + end + end + """ + + fstr = """ + function collect_close_items(points, min_distance) + foreach_neighbor(points, points, index; + backend=SerialBackend()) do item, neighbor, + _, _ + if item != neighbor + push!(result, item) + end + end + end + """ + @test format_text( + str, + SciMLStyle(); + yas_style_nesting = true, + whitespace_in_kwargs = false, + ) == fstr end end From d17ba720103033cf76d23c16ef3bc1dc468481e6 Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 5 May 2026 15:24:03 +0200 Subject: [PATCH 08/27] fix alignment of comments and arrows --- src/styles/sciml/nest.jl | 96 +++++++++++- test/sciml_style.jl | 320 ++++++++++++++++++++++----------------- 2 files changed, 277 insertions(+), 139 deletions(-) diff --git a/src/styles/sciml/nest.jl b/src/styles/sciml/nest.jl index d17eb1211..8dbadc1f0 100644 --- a/src/styles/sciml/nest.jl +++ b/src/styles/sciml/nest.jl @@ -56,6 +56,67 @@ function _is_for_tuple_binding( s.line_offset + length(fst[1]) <= s.opts.margin end +function _align_tuple_comments!(fst::FST) + for n in fst.nodes::Vector + n.typ === NOTCODE && (n.indent = fst.indent) + end +end + +function _is_dict_call(fst::FST) + fst.typ === Call && + length(fst.nodes::Vector) > 0 && + fst[1].typ === IDENTIFIER && + fst[1].val == "Dict" +end + +function _is_nonempty_tuple_pair(fst::FST) + fst.typ === Binary && + op_kind(fst) === K"=>" && + fst[end].typ === TupleN && + n_args(fst[end]) > 0 +end + +function _align_dict_tuple_pair_arrows!(fst::FST) + pair_inds = findall(_is_nonempty_tuple_pair, fst.nodes::Vector) + length(pair_inds) > 1 || return + + align_len = maximum(node_align_length(fst[i][1]) for i in pair_inds) + for i in pair_inds + pair = fst[i] + ws = align_len - node_align_length(pair[1]) + 1 + align_binaryopcall!(pair, ws) + end +end + +function _align_pair_tuple_rhs!(fst::FST, s::State) + rhs = fst[end] + rhs.typ === TupleN || return + + desired_indent = if length(rhs.nodes::Vector) > 1 && rhs[2].typ === NEWLINE + rhs.indent - 1 + else + fst.indent + node_align_length(fst[1]) + sum(length.(fst[2:4])) + 1 + end + add_indent!(rhs, s, desired_indent - rhs.indent) + _align_tuple_comments!(rhs) +end + +function _unnest_short_binary_lines!(fst::FST, s::State) + f = (n::FST, _::State) -> begin + if n.typ === Binary && !contains_comment(n) && n.line_offset >= 0 + nl_inds = findall(nn -> nn.typ === NEWLINE, n.nodes::Vector) + if length(nl_inds) > 0 && + n.line_offset + n.extra_margin + node_align_length(n) <= s.opts.margin + nl_to_ws!(n, nl_inds) + end + end + return nothing + end + line_offset = s.line_offset + walk(f, fst, s) + s.line_offset = line_offset +end + function n_binaryopcall!( ss::SciMLStyle, fst::FST, @@ -63,6 +124,37 @@ function n_binaryopcall!( lineage::Vector{Tuple{FNode,Union{Nothing,Metadata}}}; indent::Int = -1, ) + if op_kind(fst) === K"=>" && + fst[end].typ === TupleN && + fst[1].endline == fst[end].startline + style = getstyle(ss) + rhs_style = YASStyle(style) + nodes = fst.nodes::Vector + oplen = sum(length.(fst[2:end])) + nested = false + for (i, n) in enumerate(nodes) + if n.typ === NEWLINE + s.line_offset = fst.indent + elseif i == 1 + n.extra_margin = oplen + fst.extra_margin + nested |= nest!(style, n, s, lineage) + elseif i == length(nodes) + n.extra_margin = fst.extra_margin + n.indent = s.line_offset + nested |= nest!(rhs_style, n, s, lineage) + _align_pair_tuple_rhs!(fst, s) + lo = s.line_offset + walk(unnest!(rhs_style; dedent = false), n, s) + s.line_offset = lo + _unnest_short_binary_lines!(n, s) + _align_tuple_comments!(n) + else + nested |= nest!(style, n, s, lineage) + end + end + return nested + end + if _is_for_tuple_binding(fst, s, lineage) lhs = fst[1] nest_behavior = lhs.nest_behavior @@ -173,8 +265,10 @@ function _n_tuple!( lineage::Vector{Tuple{FNode,Union{Nothing,Metadata}}}, ) style = getstyle(ss) - line_margin = s.line_offset + length(fst) + fst.extra_margin nodes = fst.nodes::Vector + _is_dict_call(fst) && _align_dict_tuple_pair_arrows!(fst) + + line_margin = s.line_offset + length(fst) + fst.extra_margin has_closer = is_closer(fst[end]) start_line_offset = s.line_offset diff --git a/test/sciml_style.jl b/test/sciml_style.jl index d4b6320c2..ad91ab17f 100644 --- a/test/sciml_style.jl +++ b/test/sciml_style.jl @@ -7,14 +7,14 @@ if length(str) <= 92 @test format_text(str, SciMLStyle()) == str end - + # Test for function calls not breaking unnecessarily str = raw""" res = NLS.solve(nlls_prob, NLS.LevenbergMarquardt(); maxiters = 1000, show_trace = Val(true)) """ # Should remain on one line if it fits @test format_text(str, SciMLStyle()) == str - + # Test for anonymous function parameters not breaking str = raw""" f! = @closure (cfx, cx, user_ctx) -> begin @@ -27,7 +27,7 @@ end """ @test format_text(str, SciMLStyle()) == formatted_str - + # Test for array indexing not breaking unnecessarily str = raw""" du[i, j, 1] = alpha * (u[im1, j, 1] + u[ip1, j, 1] + u[i, jp1, 1] + u[i, jm1, 1] - 4u[i, j, 1]) @@ -718,6 +718,50 @@ yas_style_nesting = true, whitespace_in_kwargs = false, ) == fstr + + str = """ + function build_search_cases(item_spacing) + algorithm_cases = Dict( + "default search" => (), + "with static scheduler" => (backend=StaticBackend(),), + "with damping source term" => (source_terms=DampingTerm(coefficient=1e-4),), + "with lookup density" => (density_calculator=LookupDensity(), + clip_negative_pressure=true), + "with weighted viscosity" => ( + # from 0.02*10.0*1.2*0.05/8 + viscosity_model=WeightedViscosity(nu=0.0015),), + "with spline kernel" => (smoothing_length=1.1 * + item_spacing, + smoothing_kernel=SplineKernel{2}()) + ) + end + """ + + fstr = """ + function build_search_cases(item_spacing) + algorithm_cases = Dict( + "default search" => (), + "with static scheduler" => (backend=StaticBackend(),), + "with damping source term" => (source_terms=DampingTerm(coefficient=1e-4),), + "with lookup density" => (density_calculator=LookupDensity(), + clip_negative_pressure=true), + "with weighted viscosity" => ( + # from 0.02*10.0*1.2*0.05/8 + viscosity_model=WeightedViscosity(nu=0.0015),), + "with spline kernel" => (smoothing_length=1.1 * item_spacing, + smoothing_kernel=SplineKernel{2}()) + ) + end + """ + formatted = format_text(str, SciMLStyle(); whitespace_in_kwargs = false) + @test formatted == fstr + + formatted_lines = split(formatted, '\n') + comment_line = only(filter(line -> contains(line, "# from"), formatted_lines)) + option_line = + only(filter(line -> contains(line, "viscosity_model"), formatted_lines)) + @test length(comment_line) - length(lstrip(comment_line)) == + length(option_line) - length(lstrip(option_line)) end end @@ -733,7 +777,7 @@ # Test for comprehensive line break fixes # https://github.com/SciML/DiffEqGPU.jl/pull/356/files - + # Test 1: Array indexing should not be broken across lines str = """ du[II[i, j, 1]] = α * (u[II[im1, j, 1]] + u[II[ip1, j, 1]] + u[II[i, jp1, 1]] + u[II[i, jm1, 1]]) @@ -742,25 +786,25 @@ # The key fix: array indices should not be broken between parameters @test !contains(formatted, "II[i,\n") # Should not break between i and j @test !contains(formatted, "II[i, j,\n") # Should not break between j and 1 - + # Test 2: @unpack macro calls should not be broken unnecessarily str = "@unpack γ, a31, a32, a41, a42, a43, btilde1, btilde2, btilde3, btilde4, c3 = integ.tab" formatted = format_text(str, SciMLStyle()) # Should not break the unpack statement awkwardly lines = split(formatted, "\n") @test length(lines) <= 2 # Should fit on 1-2 lines max - + # Test 3: Function calls should not be broken awkwardly str = "beta1, beta2, qmax, qmin, gamma = build_adaptive_controller_cache(integ.alg, T)" formatted = format_text(str, SciMLStyle()) lines = split(formatted, "\n") @test length(lines) <= 2 # Should not create excessive line breaks - - # Test 4: Type parameters should not be broken unnecessarily + + # Test 4: Type parameters should not be broken unnecessarily str = "Vector{Float64, Int32, String}" formatted = format_text(str, SciMLStyle()) @test !contains(formatted, "{\n") # Should not break type parameters - + # Test 5: Short vector literals should not be broken str = "[a, b, c, d]" formatted = format_text(str, SciMLStyle()) @@ -769,7 +813,7 @@ @testset "SciML Repository Regression Tests" begin # These tests are based on real formatting issues found in SciML repositories # that were broken by JuliaFormatter v2 changes - + @testset "DiffEqGPU.jl regressions" begin # Test case from: https://github.com/SciML/DiffEqGPU.jl/pull/356/files # Array indexing in mathematical expressions should not be broken @@ -785,7 +829,7 @@ @test !contains(formatted, "II[i, j,\n") @test !contains(formatted, "II[im1,\n") @test !contains(formatted, "II[ip1,\n") - + # Second array indexing case from the same PR str = """ du[II[i, j, 2]] = α * (u[II[im1, j, 2]] + u[II[ip1, j, 2]] + u[II[i, jp1, 2]] + @@ -803,7 +847,7 @@ @test length(lines) <= 2 # Should not break across many lines # Accept the current behavior - the key improvement is limiting to 2 lines max # The original problem was much worse breaking across many lines - + # Function assignment should not break awkwardly str = "beta1, beta2, qmax, qmin, gamma, qoldinit, _ = build_adaptive_controller_cache(integ.alg, T)" formatted = format_text(str, SciMLStyle()) @@ -815,7 +859,7 @@ @testset "General SciML formatting patterns" begin # Common patterns that should not be broken excessively - + # Mathematical expressions with multiple array accesses str = "result = A[i, j] + B[i, k] * C[k, j] - D[i, j]" formatted = format_text(str, SciMLStyle()) @@ -823,26 +867,26 @@ @test !contains(formatted, "[i, j,\n") @test !contains(formatted, "[i, k,\n") @test !contains(formatted, "[k, j,\n") - + # Function calls with multiple parameters should be conservative str = "solve(prob, alg, abstol=1e-6, reltol=1e-3, callback=cb)" formatted = format_text(str, SciMLStyle()) # Should not break every single parameter lines = split(formatted, "\n") @test length(lines) <= 3 # Allow some breaking but not excessive - + # Type annotations should not break unnecessarily str = "function foo(x::AbstractVector{T}, y::AbstractMatrix{T}) where T<:Real end" formatted = format_text(str, SciMLStyle()) @test !contains(formatted, "AbstractVector{\nT") @test !contains(formatted, "AbstractMatrix{\nT") - + # Macro calls with assignments should be protected str = "@inbounds @simd for i in 1:n; end" formatted = format_text(str, SciMLStyle()) # Allow reasonable formatting - the key is not excessive breaking @test length(split(formatted, "\n")) <= 4 # Allow reasonable flexibility - + # Complex expressions should prioritize readability str = "u_new[idx] = u_old[idx] + dt * (k1[idx] + 2*k2[idx] + 2*k3[idx] + k4[idx]) / 6" formatted = format_text(str, SciMLStyle()) @@ -855,27 +899,27 @@ @testset "Edge cases from issue #930" begin # These are patterns mentioned in the GitHub issue that should be handled well - + # Short parameter lists should not be broken str = "f(a, b, c, d)" formatted = format_text(str, SciMLStyle()) @test formatted == str - - # Short array literals should not be broken + + # Short array literals should not be broken str = "[1, 2, 3, 4, 5]" formatted = format_text(str, SciMLStyle()) @test formatted == str - + # Short tuple should not be broken str = "(x, y, z, w)" formatted = format_text(str, SciMLStyle()) @test formatted == str - + # Short type parameters should not be broken str = "Vector{Float64}" formatted = format_text(str, SciMLStyle()) @test formatted == str - + str = "Dict{String, Int}" formatted = format_text(str, SciMLStyle()) @test formatted == str @@ -883,60 +927,60 @@ @testset "Additional SciML repository patterns" begin # Patterns found in various SciML repositories that were problematic - + # Long function calls should not break every argument (common pattern) str = "OrdinaryDiffEqCore._ode_addsteps!(k, t, uprev, u, dt, f, p, cache, always_calc_begin, true, true)" formatted = format_text(str, SciMLStyle()) # Should not put every single argument on its own line lines = split(formatted, "\n") @test length(lines) <= 4 # Allow reasonable breaking but not excessive - + # Complex constructor calls should be handled reasonably str = "CellGenotype([Float64(1)], :aSymbiontGenotype)" formatted = format_text(str, SciMLStyle()) # Should not break the array literal unnecessarily @test !contains(formatted, "[Float64(\n") @test !contains(formatted, "Float64(1\n") - + # Nested function calls should prioritize readability str = "construct(Plant, (deepcopy(organ1), deepcopy(organ2)), Float64[], PlantSettings(1))" formatted = format_text(str, SciMLStyle()) # Should not break simple arguments unnecessarily @test !contains(formatted, "deepcopy(\n") @test !contains(formatted, "Float64[\n") - + # Mathematical expressions with array indexing should stay readable str = "du[i] = α * u[i-1] + β * u[i] + γ * u[i+1]" formatted = format_text(str, SciMLStyle()) @test !contains(formatted, "u[i-\n") @test !contains(formatted, "u[i+\n") @test !contains(formatted, "du[\n") - + # Complex type annotations should not break type parameters str = "function solve(prob::BVProblem{T,U,V}, alg::Algorithm) where {T,U,V} end" formatted = format_text(str, SciMLStyle()) @test !contains(formatted, "BVProblem{\nT") @test !contains(formatted, "{T,\nU") - + # Closure definitions should be handled gracefully str = "@closure (t, u, du) -> du .= vec(prob.f(reshape(u, u0_size), prob.p, t))" formatted = format_text(str, SciMLStyle()) # Should not break the simple parameter list @test !contains(formatted, "(t,\n") @test !contains(formatted, "u,\n") - + # Array slicing operations should not be broken str = "left_bc = reshape(@view(r[1:no_left_bc]), left_bc_size)" formatted = format_text(str, SciMLStyle()) @test !contains(formatted, "r[1:\n") @test !contains(formatted, "@view(\n") - + # Multiple assignment patterns should be conservative str = "ya, yb, ya_size, yb_size = extract_parameters(prob)" formatted = format_text(str, SciMLStyle()) lines = split(formatted, "\n") @test length(lines) <= 2 # Should not break excessively - + # Nested array access should stay together str = "cache.u[cache.stage_indices[i]][j]" formatted = format_text(str, SciMLStyle()) @@ -945,19 +989,19 @@ @test !contains(formatted, "][j\n") end end - + @testset "semantic safety" begin # Test 1: Whitespace-sensitive array literals # Space means horizontal concatenation, ; or newline means vertical str = "[1 2 3]" # 1×3 matrix @test format_text(str, SciMLStyle()) == str - - str = "[1; 2; 3]" # 3×1 matrix + + str = "[1; 2; 3]" # 3×1 matrix @test format_text(str, SciMLStyle()) == str - + str = "[1 2; 3 4]" # 2×2 matrix @test format_text(str, SciMLStyle()) == str - + # Test that we don't accidentally change array dimensions str = """ A = [1 2 3 @@ -966,7 +1010,7 @@ formatted = format_text(str, SciMLStyle()) @test contains(formatted, "1 2 3") # Ensure spaces preserved @test !contains(formatted, "1; 2; 3") # Ensure no semicolons added - + # Test 2: Operator precedence preservation precedence_tests = [ ("x = a + b * c", "x = a + b * c"), @@ -975,171 +1019,171 @@ ("x = a / b * c", "x = a / b * c"), # Left associative ("x = -a^2", "x = -a^2"), # Unary minus precedence ] - + for (input, expected) in precedence_tests @test format_text(input, SciMLStyle()) == expected end - + # Test 3: Coefficient syntax preservation # In Julia, 2x means 2*x, but 2 x is an error str = "y = 2x + 3y - 4z" @test format_text(str, SciMLStyle()) == str - + str = "y = 2(x + y)" # Coefficient with parentheses @test format_text(str, SciMLStyle()) == str - + # Test 4: Macro scope preservation str = "@. x = y + z" formatted = format_text(str, SciMLStyle()) @test formatted == str @test contains(formatted, "@.") - + str = "@views x[1:n] = y[1:n]" formatted = format_text(str, SciMLStyle()) @test contains(formatted, "@views") @test !contains(formatted, "@views\n") # Macro not separated from expression - + # Test 5: String and character literals (must not break) str = "msg = \"This is a long string that should not be broken\"" @test format_text(str, SciMLStyle()) == str - + str = "c = 'a'" @test format_text(str, SciMLStyle()) == str - + # Test 6: Ternary operator associativity str = "x = a ? b : c ? d : e" # Right associative formatted = format_text(str, SciMLStyle()) # Ensure it doesn't become (a ? b : c) ? d : e @test Meta.parse(str) == Meta.parse(formatted) - + # Test 7: Short-circuit evaluation order str = "a && b || c && d" formatted = format_text(str, SciMLStyle()) @test Meta.parse(str) == Meta.parse(formatted) - + # Test 8: Anonymous function syntax str = "f = x -> x^2" @test format_text(str, SciMLStyle()) == str - + str = "g = (x, y) -> x + y" @test format_text(str, SciMLStyle()) == str - + # Test 9: Array comprehension preservation str = "[i * j for i in 1:3, j in 1:3]" formatted = format_text(str, SciMLStyle()) @test contains(formatted, "for i in 1:3, j in 1:3") - + # Test 10: Broadcasting syntax str = "x .= y .+ z .* w" @test format_text(str, SciMLStyle()) == str - + str = "sin.(x) .+ cos.(y)" @test format_text(str, SciMLStyle()) == str - + # Test 11: Type parameter syntax str = "Vector{Float64}" @test format_text(str, SciMLStyle()) == str - + str = "Dict{String,Int}" formatted = format_text(str, SciMLStyle()) @test formatted == str || formatted == "Dict{String, Int}" # Space after comma is ok - + # Test 12: Splatting and slurping str = "f(x...)" @test format_text(str, SciMLStyle()) == str - + str = "g(a, b..., c)" @test format_text(str, SciMLStyle()) == str - + # Test 13: Range syntax preservation str = "1:10" @test format_text(str, SciMLStyle()) == str - + str = "1:2:10" @test format_text(str, SciMLStyle()) == str - + # Test 14: Rational number syntax str = "1//2" @test format_text(str, SciMLStyle()) == str - + # Test 15: Complex number syntax str = "1 + 2im" @test format_text(str, SciMLStyle()) == str - + # Test 16: Symbol syntax str = ":symbol" @test format_text(str, SciMLStyle()) == str - + str = ":(a + b)" @test format_text(str, SciMLStyle()) == str - + # Test 17: Interpolation in strings str = "\"x = \$x, y = \$(y + 1)\"" @test format_text(str, SciMLStyle()) == str - + # Test 18: Command literals str = "`echo hello`" @test format_text(str, SciMLStyle()) == str - + # Test 19: Version number literals str = "v\"1.2.3\"" @test format_text(str, SciMLStyle()) == str - + # Test 20: Regex literals str = "r\"[a-z]+\"" @test format_text(str, SciMLStyle()) == str - + # Test 21: Critical array literal semantic preservation # These MUST not change dimensions - + # Horizontal concatenation (space) str = "[1 2 3]" formatted = format_text(str, SciMLStyle()) @test formatted == "[1 2 3]" # Must preserve spaces - + # Vertical concatenation (semicolon) str = "[1; 2; 3]" formatted = format_text(str, SciMLStyle()) @test formatted == "[1; 2; 3]" # Must preserve semicolons - + # Mixed (creates matrix) str = "[1 2; 3 4]" - formatted = format_text(str, SciMLStyle()) + formatted = format_text(str, SciMLStyle()) @test formatted == "[1 2; 3 4]" # Must preserve exact structure - + # Test 21b: More array literal edge cases # Mixed spaces and semicolons str = "[1 2; 3 4; 5 6]" # 3×2 matrix @test format_text(str, SciMLStyle()) == str - + # Nested arrays str = "[[1, 2], [3, 4]]" @test format_text(str, SciMLStyle()) == str - + # Array with trailing comma (1-element array vs scalar) str = "[1,]" # 1-element array formatted = format_text(str, SciMLStyle()) @test formatted == "[1]" || formatted == "[1,]" # Both are valid - + # Empty arrays with type str = "Float64[]" @test format_text(str, SciMLStyle()) == str - + str = "Vector{Int}()" @test format_text(str, SciMLStyle()) == str - + # Test 22: Tuple vs array distinction str = "(1, 2, 3)" # Tuple @test format_text(str, SciMLStyle()) == str - + str = "(1,)" # 1-element tuple (comma required) @test format_text(str, SciMLStyle()) == str - + # Test 23: Generator expressions str = "(x^2 for x in 1:10)" formatted = format_text(str, SciMLStyle()) @test contains(formatted, "for x in 1:10") - + # Test 24: Multi-line array construction preservation str = """ A = [ @@ -1153,40 +1197,40 @@ @test contains(formatted, "1 2 3") @test contains(formatted, "4 5 6") @test contains(formatted, "7 8 9") - + # Test 25: Coefficient with array indexing str = "y = 2A[i, j]" # 2*A[i,j], not 2 A[i,j] @test format_text(str, SciMLStyle()) == str - + # Test 26: Multiple dispatch syntax str = "f(::Type{T}) where T = T" formatted = format_text(str, SciMLStyle()) @test contains(formatted, "where") && contains(formatted, "T") && contains(formatted, "Type{T}") - + # Test 27: Subtype syntax str = "T <: Number" @test format_text(str, SciMLStyle()) == str - + # Test 28: Union types str = "Union{Int, Float64}" @test format_text(str, SciMLStyle()) == str - + # Test 29: Parametric types with constraints str = "struct Foo{T<:Real} end" formatted = format_text(str, SciMLStyle()) @test contains(formatted, "T<:Real") || contains(formatted, "T <: Real") - + # Test 30: Named tuples str = "(a=1, b=2)" formatted = format_text(str, SciMLStyle()) # Named tuples might get spaces around = @test formatted == str || formatted == "(a = 1, b = 2)" - + # Test 31: Keyword arguments str = "f(x; y=1, z=2)" formatted = format_text(str, SciMLStyle()) @test contains(formatted, "; y") # Semicolon preserved - + # Test 32: Do block syntax str = """ map(1:3) do x @@ -1195,7 +1239,7 @@ """ formatted = format_text(str, SciMLStyle()) @test contains(formatted, "do x") - + # Test 33: Let block with multiple bindings str = "let x = 1, y = 2; x + y end" formatted = format_text(str, SciMLStyle()) @@ -1203,31 +1247,31 @@ str_ast = replace(string(Meta.parse(str)), r"#= \S+:\d+ =#" => "") form_ast = replace(string(Meta.parse(formatted)), r"#= \S+:\d+ =#" => "") @test str_ast == form_ast - + # Test 34: Destructuring str = "a, b, c = 1, 2, 3" @test format_text(str, SciMLStyle()) == str - + # Test 35: Unicode operators str = "x ∈ A ∩ B" @test format_text(str, SciMLStyle()) == str - + # Test 36: Pipe operator str = "x |> f |> g" @test format_text(str, SciMLStyle()) == str - + # Test 37: Pairs syntax str = "a => b" @test format_text(str, SciMLStyle()) == str - + # Test 38: Quote expressions str = ":(x + y)" @test format_text(str, SciMLStyle()) == str - + # Test 39: Escaped identifiers str = "var\"strange name\"" @test format_text(str, SciMLStyle()) == str - + # Test 40: Line number nodes (should be preserved in macros) str = """ macro foo() @@ -1240,21 +1284,21 @@ formatted = format_text(str, SciMLStyle()) @test contains(formatted, "quote") end - + @testset "line break quality" begin # Helper function to analyze line utilization function analyze_line_quality(formatted_code) lines = split(formatted_code, '\n') non_empty_lines = filter(l -> !isempty(strip(l)), lines) - + if isempty(non_empty_lines) return (max_length=0, avg_length=0.0, num_lines=0, efficiency=0.0) end - + lengths = [length(rstrip(line)) for line in non_empty_lines] max_length = maximum(lengths) avg_length = sum(lengths) / length(lengths) - + # Efficiency: how well lines use available space (0-1) # Penalize both very short lines and lines over margin efficiency_scores = map(lengths) do len @@ -1267,11 +1311,11 @@ end end efficiency = sum(efficiency_scores) / length(efficiency_scores) - - return (max_length=max_length, avg_length=avg_length, + + return (max_length=max_length, avg_length=avg_length, num_lines=length(non_empty_lines), efficiency=efficiency) end - + # Test 1: Simple expressions should stay on one line when they fit @testset "single line preservation" begin # These should remain on one line @@ -1282,14 +1326,14 @@ "y = sin(x) + cos(z) * exp(-t)", "data = [1, 2, 3, 4, 5]", ] - + for code in single_line_cases formatted = format_text(code, SciMLStyle()) @test !contains(formatted, '\n') @test formatted == code end end - + # Test 2: Long lines should break efficiently @testset "efficient line breaking" begin # Function call with many parameters @@ -1298,23 +1342,23 @@ """ formatted = format_text(str, SciMLStyle()) quality = analyze_line_quality(formatted) - + @test quality.max_length <= 92 # Should respect margin @test quality.avg_length > 50 # Should use space efficiently @test quality.efficiency > 0.6 # Good space utilization - + # Mathematical expression str = """ residual = alpha * (u[i-1, j] + u[i+1, j] - 2u[i, j]) / dx^2 + beta * (u[i, j-1] + u[i, j+1] - 2u[i, j]) / dy^2 + gamma * u[i, j] """ formatted = format_text(str, SciMLStyle()) quality = analyze_line_quality(formatted) - + # Should break but maintain good line density @test quality.num_lines >= 2 @test quality.avg_length > 40 # Not too many short lines end - + # Test 3: Type parameters should wrap nicely @testset "type parameter wrapping" begin str = """ @@ -1324,11 +1368,11 @@ """ formatted = format_text(str, SciMLStyle()) quality = analyze_line_quality(formatted) - + # Should fit on 3 lines max @test quality.num_lines <= 3 @test quality.max_length <= 92 - + # Long type parameter list str = """ struct MyType{T1,T2,T3,T4,T5,T6,T7,T8,T9,T10} <: AbstractType{T1,T2,T3,T4,T5,T6,T7,T8,T9,T10} @@ -1336,7 +1380,7 @@ """ formatted = format_text(str, SciMLStyle()) lines = split(formatted, '\n') - + # Should break but not excessively @test length(lines) <= 4 # No single parameter on a line @@ -1346,18 +1390,18 @@ end end end - + # Test 4: Array operations should maintain structure @testset "array operation formatting" begin # Array comprehension str = "[f(i, j) * g(k) for i in 1:n, j in 1:m, k in 1:p if condition(i, j, k)]" formatted = format_text(str, SciMLStyle()) - + # Should keep comprehension readable @test contains(formatted, "for i in 1:n, j in 1:m") quality = analyze_line_quality(formatted) @test quality.efficiency > 0.5 - + # Matrix literal str = """ A = [ @@ -1371,7 +1415,7 @@ @test contains(formatted, "a11 a12 a13 a14") @test contains(formatted, "a21 a22 a23 a24") end - + # Test 5: Function definitions with multiple arguments @testset "function definition formatting" begin # Moderate length - should stay on 2-3 lines @@ -1382,10 +1426,10 @@ """ formatted = format_text(str, SciMLStyle()) quality = analyze_line_quality(formatted) - + @test quality.num_lines <= 4 # Reasonable number of lines @test quality.avg_length > 30 # Reasonable line utilization - + # Very long parameter list str = """ function complex_solver(problem::NonlinearProblem{uType,tType,isinplace}, algorithm::NewtonRaphson{CS,AD,FDT,L,P,ST,CJ}, x0::AbstractVector, p::NamedTuple; abstol::Float64=1e-8, reltol::Float64=1e-8, maxiters::Int=1000, callback=nothing, show_trace::Bool=false) where {uType,tType,isinplace,CS,AD,FDT,L,P,ST,CJ} @@ -1394,32 +1438,32 @@ """ formatted = format_text(str, SciMLStyle()) lines = split(formatted, '\n') - + # Should break intelligently @test all(length(rstrip(line)) <= 92 for line in lines) # But not too many lines @test length(lines) <= 12 # Complex function may need more lines end - + # Test 6: Nested expressions should maintain hierarchy @testset "nested expression formatting" begin str = """ result = transform(integrate(differentiate(interpolate(data, method=:cubic), order=2), bounds=(a, b)), scaling=:log) """ formatted = format_text(str, SciMLStyle()) - + # Should break at outer function calls first @test contains(formatted, "transform(") quality = analyze_line_quality(formatted) @test quality.efficiency > 0.5 - + # Nested array access str = "value = data[indices[i]][subindices[j]][k]" formatted = format_text(str, SciMLStyle()) # Should keep array access together when reasonable @test !contains(formatted, "[\n") end - + # Test 7: Mathematical expressions should break at operators @testset "mathematical expression breaking" begin # Long equation @@ -1428,29 +1472,29 @@ """ formatted = format_text(str, SciMLStyle()) lines = split(formatted, '\n') - + # Should break at + operators for line in lines[2:end] stripped = strip(line) if !isempty(stripped) # Continuation lines should start with operator or be aligned - @test startswith(stripped, "+") || startswith(stripped, "-") || + @test startswith(stripped, "+") || startswith(stripped, "-") || startswith(stripped, "*") || startswith(stripped, "/") || length(lstrip(line)) < length(line) # Or be indented end end - + quality = analyze_line_quality(formatted) @test quality.avg_length > 40 # Efficient use of space end - + # Test 8: Avoid "orphan" parameters @testset "avoid orphan parameters" begin # Should not leave single parameters on lines str = "f(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z)" formatted = format_text(str, SciMLStyle()) lines = split(formatted, '\n') - + for line in lines stripped = strip(line) # If it's just a single letter and comma/paren, it's an orphan @@ -1459,41 +1503,41 @@ end end end - + # Test 9: Keyword arguments should group nicely @testset "keyword argument grouping" begin str = """ solve(prob; abstol=1e-8, reltol=1e-8, dtmin=1e-10, dtmax=0.1, maxiters=10000, adaptive=true, save_everystep=false, save_start=true, save_end=true, verbose=true) """ formatted = format_text(str, SciMLStyle()) - + # Should group related kwargs when possible lines = split(formatted, '\n') # First line should have multiple kwargs if they fit if length(lines) > 1 @test contains(lines[1], ",") || contains(lines[1], ";") end - + quality = analyze_line_quality(formatted) @test quality.efficiency > 0.6 end - + # Test 10: Preserve logical grouping @testset "logical grouping preservation" begin # Coordinates should stay together str = "plot(x1, y1, x2, y2, x3, y3; xlabel=\"Time\", ylabel=\"Value\", title=\"Results\")" formatted = format_text(str, SciMLStyle()) - + # x,y pairs should stay together when possible @test contains(formatted, "x1, y1") || contains(formatted, "x1,y1") - + # Matrix multiplication chain str = "result = A * B * C * D * E * F" formatted = format_text(str, SciMLStyle()) # Should keep some products together @test length(split(formatted, '\n')) <= 3 end - + # Test 11: Real-world examples from SciML @testset "SciML real-world examples" begin # From OrdinaryDiffEq @@ -1510,11 +1554,11 @@ """ formatted = format_text(str, SciMLStyle()) quality = analyze_line_quality(formatted) - + # Should maintain readability @test quality.max_length <= 92 @test quality.avg_length > 30 - + # From ModelingToolkit str = """ @parameters t σ ρ β @@ -1523,7 +1567,7 @@ eqs = [D(x) ~ σ * (y - x), D(y) ~ x * (ρ - z) - y, D(z) ~ x * y - β * z] """ formatted = format_text(str, SciMLStyle()) - + # Should keep equations readable @test contains(formatted, "D(x) ~ σ * (y - x)") lines = split(formatted, '\n') From 1f0d70bb44ed422eccbbc052095b67ea6f51ebc9 Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 5 May 2026 15:33:02 +0200 Subject: [PATCH 09/27] fix alignment of arrows --- src/styles/sciml/nest.jl | 7 +++---- test/sciml_style.jl | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/styles/sciml/nest.jl b/src/styles/sciml/nest.jl index 8dbadc1f0..f3738090e 100644 --- a/src/styles/sciml/nest.jl +++ b/src/styles/sciml/nest.jl @@ -69,15 +69,14 @@ function _is_dict_call(fst::FST) fst[1].val == "Dict" end -function _is_nonempty_tuple_pair(fst::FST) +function _is_tuple_pair(fst::FST) fst.typ === Binary && op_kind(fst) === K"=>" && - fst[end].typ === TupleN && - n_args(fst[end]) > 0 + fst[end].typ === TupleN end function _align_dict_tuple_pair_arrows!(fst::FST) - pair_inds = findall(_is_nonempty_tuple_pair, fst.nodes::Vector) + pair_inds = findall(_is_tuple_pair, fst.nodes::Vector) length(pair_inds) > 1 || return align_len = maximum(node_align_length(fst[i][1]) for i in pair_inds) diff --git a/test/sciml_style.jl b/test/sciml_style.jl index ad91ab17f..9087bc148 100644 --- a/test/sciml_style.jl +++ b/test/sciml_style.jl @@ -740,7 +740,7 @@ fstr = """ function build_search_cases(item_spacing) algorithm_cases = Dict( - "default search" => (), + "default search" => (), "with static scheduler" => (backend=StaticBackend(),), "with damping source term" => (source_terms=DampingTerm(coefficient=1e-4),), "with lookup density" => (density_calculator=LookupDensity(), From d68cb963dca4be1bed8d07084104ff0b3721bdae Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 5 May 2026 15:47:49 +0200 Subject: [PATCH 10/27] simplify nesting rules --- src/styles/sciml/nest.jl | 62 ++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 41 deletions(-) diff --git a/src/styles/sciml/nest.jl b/src/styles/sciml/nest.jl index f3738090e..c0d6d1c44 100644 --- a/src/styles/sciml/nest.jl +++ b/src/styles/sciml/nest.jl @@ -56,6 +56,14 @@ function _is_for_tuple_binding( s.line_offset + length(fst[1]) <= s.opts.margin end +function _nearest_binary_is_assignment(lineage::Vector{Tuple{FNode,Union{Nothing,Metadata}}}) + i = findlast(x -> x[1] === Binary, lineage) + i === nothing && return false + + metadata = lineage[i][2] + return !isnothing(metadata) && metadata.is_assignment +end + function _align_tuple_comments!(fst::FST) for n in fst.nodes::Vector n.typ === NOTCODE && (n.indent = fst.indent) @@ -94,26 +102,26 @@ function _align_pair_tuple_rhs!(fst::FST, s::State) desired_indent = if length(rhs.nodes::Vector) > 1 && rhs[2].typ === NEWLINE rhs.indent - 1 else - fst.indent + node_align_length(fst[1]) + sum(length.(fst[2:4])) + 1 + fst.indent + node_align_length(fst[1:(end-1)]) + 1 end add_indent!(rhs, s, desired_indent - rhs.indent) _align_tuple_comments!(rhs) end function _unnest_short_binary_lines!(fst::FST, s::State) - f = (n::FST, _::State) -> begin - if n.typ === Binary && !contains_comment(n) && n.line_offset >= 0 - nl_inds = findall(nn -> nn.typ === NEWLINE, n.nodes::Vector) - if length(nl_inds) > 0 && - n.line_offset + n.extra_margin + node_align_length(n) <= s.opts.margin - nl_to_ws!(n, nl_inds) - end + is_leaf(fst) && return + + if fst.typ === Binary && !contains_comment(fst) && fst.line_offset >= 0 + nl_inds = findall(n -> n.typ === NEWLINE, fst.nodes::Vector) + if length(nl_inds) > 0 && + fst.line_offset + fst.extra_margin + node_align_length(fst) <= s.opts.margin + nl_to_ws!(fst, nl_inds) end - return nothing end - line_offset = s.line_offset - walk(f, fst, s) - s.line_offset = line_offset + + for n in fst.nodes::Vector + _unnest_short_binary_lines!(n, s) + end end function n_binaryopcall!( @@ -400,35 +408,7 @@ function n_ref!( s::State, lineage::Vector{Tuple{FNode,Union{Nothing,Metadata}}}, ) - # Check if this RefN is the LHS of an assignment - # Look through the lineage to see if we have a Binary assignment parent - # and this RefN comes before any other Binary operators - is_lhs_of_assignment = false - - if length(lineage) >= 2 - # Check if we have a Binary assignment in the lineage - for i in length(lineage):-1:1 - if lineage[i][1] === Binary && - !isnothing(lineage[i][2]) && - lineage[i][2].is_assignment - # Check if there are any other Binary nodes between us and the assignment - has_intermediate_binary = false - for j in (i+1):length(lineage) - if lineage[j][1] === Binary - has_intermediate_binary = true - break - end - end - - if !has_intermediate_binary - is_lhs_of_assignment = true - end - break - end - end - end - - if is_lhs_of_assignment && fst.extra_margin > 0 + if _nearest_binary_is_assignment(lineage) && fst.extra_margin > 0 # Don't break the LHS of an assignment # Format children but keep them on the same line nodes = fst.nodes::Vector{FST} From ba7fa189f820ee9821bbc2eda94352582275b923 Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 12 May 2026 14:32:18 +0200 Subject: [PATCH 11/27] fix tests --- src/print.jl | 8 ++-- src/styles/default/pretty.jl | 88 +++++++++++++++++++----------------- src/styles/yas/pretty.jl | 2 +- test/default_style.jl | 8 ++++ 4 files changed, 60 insertions(+), 46 deletions(-) diff --git a/src/print.jl b/src/print.jl index 0e0775ab0..6ffa5126e 100644 --- a/src/print.jl +++ b/src/print.jl @@ -4,7 +4,7 @@ function format_check(io::IOBuffer, fst::FST, s::State) return end - line_range = fst.startline:fst.endline + line_range = (fst.startline):(fst.endline) skip = s.doc.format_skips[1] nlines = numlines(s.doc) @@ -19,7 +19,7 @@ function format_check(io::IOBuffer, fst::FST, s::State) write(io, JuliaSyntax.sourcetext(s.doc.srcfile)[first(r1):last(r2)]) end - output = JuliaSyntax.sourcetext(s.doc.srcfile)[skip.startoffset:skip.endoffset] + output = JuliaSyntax.sourcetext(s.doc.srcfile)[(skip.startoffset):(skip.endoffset)] l1 = skip.endline + 1 l2 = fst.endline @@ -52,7 +52,7 @@ function format_check(io::IOBuffer, fst::FST, s::State) end s.on = false elseif !s.on && skip.endline in line_range - output = JuliaSyntax.sourcetext(s.doc.srcfile)[skip.startoffset:skip.endoffset] + output = JuliaSyntax.sourcetext(s.doc.srcfile)[(skip.startoffset):(skip.endoffset)] l1 = skip.endline + 1 l2 = fst.endline @@ -180,7 +180,7 @@ function print_notcode(io::IOBuffer, fst::FST, s::State) if !(s.on) return end - for l in fst.startline:fst.endline + for l in (fst.startline):(fst.endline) _, v = get(s.doc.comments, l, (0, "\n")) ws = fst.indent diff --git a/src/styles/default/pretty.jl b/src/styles/default/pretty.jl index bc37d1b47..efc77f87e 100644 --- a/src/styles/default/pretty.jl +++ b/src/styles/default/pretty.jl @@ -21,9 +21,11 @@ function newctx(s::PrettyContext; kwargs...) PrettyContext(values...) end -function source_kind(s::State, cst::JuliaSyntax.GreenNode, offset::Int) +function source_kind(s::State, cst::JuliaSyntax.GreenNode, offset::Integer) span(cst) == 0 && return nothing - val = getsrcval(s.doc, offset:(offset+span(cst)-1)) + offset = Int(offset) + n = Int(span(cst)) + val = getsrcval(s.doc, offset:(offset+n-1)) try return JuliaSyntax.Kind(val) catch @@ -31,7 +33,7 @@ function source_kind(s::State, cst::JuliaSyntax.GreenNode, offset::Int) end end -function source_operator_kind(s::State, cst::JuliaSyntax.GreenNode, offset::Int) +function source_operator_kind(s::State, cst::JuliaSyntax.GreenNode, offset::Integer) if JuliaSyntax.is_operator(cst) && !haschildren(cst) return kind(cst) elseif kind(cst) === K"Identifier" && !haschildren(cst) @@ -43,7 +45,7 @@ function source_operator_kind(s::State, cst::JuliaSyntax.GreenNode, offset::Int) return nothing end -function is_source_operator(s::State, cst::JuliaSyntax.GreenNode, offset::Int) +function is_source_operator(s::State, cst::JuliaSyntax.GreenNode, offset::Integer) !isnothing(source_operator_kind(s, cst, offset)) end @@ -57,14 +59,15 @@ function source_prefix_operator_index(cst::JuliaSyntax.GreenNode, s::State) args = findall(n -> !JuliaSyntax.is_whitespace(n), childs) isempty(args) && return nothing - op_arg = if kind(cst) === K"dotcall" && - length(args) >= 2 && - kind(childs[args[1]]) === K"." && - !haschildren(childs[args[1]]) - args[2] - else - args[1] - end + op_arg = + if kind(cst) === K"dotcall" && + length(args) >= 2 && + kind(childs[args[1]]) === K"." && + !haschildren(childs[args[1]]) + args[2] + else + args[1] + end offset = s.offset + sum(span, childs[1:(op_arg-1)]; init = 0) return is_source_operator(s, childs[op_arg], offset) ? op_arg : nothing end @@ -106,15 +109,11 @@ function is_source_prefix_op_call(cst::JuliaSyntax.GreenNode, s::State) return operands == 1 end -function source_op_kind( - s::State, - cst::JuliaSyntax.GreenNode, - op_indices::Vector{Int}, -) +function source_op_kind(s::State, cst::JuliaSyntax.GreenNode, op_indices::Vector{Int}) opkind = op_kind(cst) opkind !== K"None" && opkind !== K"Identifier" && return opkind - offset = s.offset + offset = Int(s.offset) childs = children(cst) for (i, c) in enumerate(childs) if i in op_indices @@ -123,7 +122,7 @@ function source_op_kind( return k end end - offset += span(c) + offset += Int(span(c)) end return opkind end @@ -139,8 +138,7 @@ function do_block_index(childs::Vector{JuliaSyntax.GreenNode{T}}) where {T} end function has_do_block_call(cst::JuliaSyntax.GreenNode) - kind(cst) in KSet"call dotcall" && haschildren(cst) || - return false + kind(cst) in KSet"call dotcall" && haschildren(cst) || return false !isnothing(do_block_index(children(cst))) end @@ -379,7 +377,7 @@ function p_identifier( lineage::Vector{Tuple{JuliaSyntax.Kind,Bool,Bool}}, ) loc = cursor_loc(s) - val = getsrcval(s.doc, s.offset:(s.offset+span(cst)-1)) + val = getsrcval(s.doc, (s.offset):(s.offset+span(cst)-1)) s.offset += span(cst) FST(IDENTIFIER, loc[2], loc[1], loc[1], val) end @@ -392,7 +390,7 @@ function p_whitespace( lineage::Vector{Tuple{JuliaSyntax.Kind,Bool,Bool}}, ) loc = cursor_loc(s) - val = getsrcval(s.doc, s.offset:(s.offset+span(cst)-1)) + val = getsrcval(s.doc, (s.offset):(s.offset+span(cst)-1)) s.offset += span(cst) FST(NONE, loc[2], loc[1], loc[1], val) end @@ -406,7 +404,7 @@ function p_comment( ) loc = cursor_loc(s) same_line = on_same_line(s, s.offset, s.offset + span(cst) - 1) - val = getsrcval(s.doc, s.offset:(s.offset+span(cst)-1)) + val = getsrcval(s.doc, (s.offset):(s.offset+span(cst)-1)) if same_line && startswith(val, "#=") && endswith(val, "=#") s.offset += span(cst) return FST(HASHEQCOMMENT, loc[2], loc[1], loc[1], val) @@ -435,7 +433,7 @@ function p_macroname( ::Vector{Tuple{JuliaSyntax.Kind,Bool,Bool}}, ) loc = cursor_loc(s) - val = getsrcval(s.doc, s.offset:(s.offset+span(cst)-1)) + val = getsrcval(s.doc, (s.offset):(s.offset+span(cst)-1)) s.offset += span(cst) FST(MACRONAME, loc[2], loc[1], loc[1], val) end @@ -448,7 +446,7 @@ function p_operator( ::Vector{Tuple{JuliaSyntax.Kind,Bool,Bool}}, ) loc = cursor_loc(s) - val = getsrcval(s.doc, s.offset:(s.offset+span(cst)-1)) + val = getsrcval(s.doc, (s.offset):(s.offset+span(cst)-1)) s.offset += span(cst) t = FST(OPERATOR, loc[2], loc[1], loc[1], val) t.metadata = Metadata(kind(cst), JuliaSyntax.is_dotted(cst)) @@ -463,7 +461,7 @@ function p_keyword( ::Vector{Tuple{JuliaSyntax.Kind,Bool,Bool}}, ) loc = cursor_loc(s) - val = getsrcval(s.doc, s.offset:(s.offset+span(cst)-1)) + val = getsrcval(s.doc, (s.offset):(s.offset+span(cst)-1)) s.offset += span(cst) FST(KEYWORD, loc[2], loc[1], loc[1], val) end @@ -476,7 +474,7 @@ function p_punctuation( ::Vector{Tuple{JuliaSyntax.Kind,Bool,Bool}}, ) loc = cursor_loc(s) - val = getsrcval(s.doc, s.offset:(s.offset+span(cst)-1)) + val = getsrcval(s.doc, (s.offset):(s.offset+span(cst)-1)) s.offset += span(cst) FST(PUNCTUATION, loc[2], loc[1], loc[1], val) end @@ -605,7 +603,7 @@ function p_literal( lineage::Vector{Tuple{JuliaSyntax.Kind,Bool,Bool}}, ) loc = cursor_loc(s) - val = getsrcval(s.doc, s.offset:(s.offset+span(cst)-1)) + val = getsrcval(s.doc, (s.offset):(s.offset+span(cst)-1)) if !is_str_or_cmd(cst) if kind(cst) in KSet"Float Float32" && !startswith(val, r"[+-]?0x") @@ -669,7 +667,7 @@ function p_stringh( end loc2 = cursor_loc(s, s.offset+span(cst)-1) - val = getsrcval(s.doc, s.offset:(s.offset+span(cst)-1)) + val = getsrcval(s.doc, (s.offset):(s.offset+span(cst)-1)) startline = loc[1] endline = loc2[1] @@ -1731,7 +1729,12 @@ function p_do_call( return p_call(ds, cst, s, ctx, lineage) end - add_node!(t, p_call(ds, cst, s, ctx, lineage; child_limit = idx - 1), s; join_lines = true) + add_node!( + t, + p_call(ds, cst, s, ctx, lineage; child_limit = idx - 1), + s; + join_lines = true, + ) do_node = childs[idx] push!(lineage, (kind(do_node), is_iterable(do_node), is_assignment(do_node))) @@ -1897,12 +1900,11 @@ function p_kw( else source_prefix = !isnothing(source_prefix_operator_index(c, s)) n = pretty(style, c, s, ctx, lineage) - if !s.opts.whitespace_in_kwargs && - ( - (n.typ === IDENTIFIER && endswith(n.val, "!")) || - is_prefix_op_call(c) || - source_prefix - ) + if !s.opts.whitespace_in_kwargs && ( + (n.typ === IDENTIFIER && endswith(n.val, "!")) || + is_prefix_op_call(c) || + source_prefix + ) add_node!( t, FST(PUNCTUATION, -1, n.startline, n.startline, "("), @@ -1941,8 +1943,11 @@ function p_binaryopcall( end childs = children(cst) - op_indices = kind(cst) === K"op=" ? update_operator_indices(childs) : - extract_operator_indices(childs) + op_indices = if kind(cst) === K"op=" + update_operator_indices(childs) + else + extract_operator_indices(childs) + end opkind = source_op_kind(s, cst, op_indices) nonest = ctx.nonest || opkind === K":" @@ -2050,7 +2055,7 @@ function p_binaryopcall( if kind(cst) === K"op=" && !isempty(op_indices) && i == first(op_indices) loc = cursor_loc(s) op_span = sum(span, childs[first(op_indices):last(op_indices)]; init = 0) - val = getsrcval(s.doc, s.offset:(s.offset+op_span-1)) + val = getsrcval(s.doc, (s.offset):(s.offset+op_span-1)) s.offset += op_span n = FST(OPERATOR, loc[2], loc[1], loc[1], val) n.metadata = Metadata(K"op=", startswith(val, ".")) @@ -2089,7 +2094,8 @@ function p_binaryopcall( is_op = i in op_indices && is_source_operator(s, c, offset) if is_op && n.typ === IDENTIFIER n.typ = OPERATOR - n.metadata = Metadata(source_operator_kind(s, c, offset)::JuliaSyntax.Kind, is_dot) + n.metadata = + Metadata(source_operator_kind(s, c, offset)::JuliaSyntax.Kind, is_dot) end if is_dot && haschildren(c) && length(children(c)) == 2 # [.] diff --git a/src/styles/yas/pretty.jl b/src/styles/yas/pretty.jl index 53a6bb84f..a19842c0e 100644 --- a/src/styles/yas/pretty.jl +++ b/src/styles/yas/pretty.jl @@ -418,7 +418,7 @@ function p_call( else 0 end - val = getsrcval(s.doc, s.offset:(s.offset+offset)) + val = getsrcval(s.doc, (s.offset):(s.offset+offset)) if val in s.opts.variable_call_indent return p_call(DefaultStyle(style), cst, s, ctx, lineage) end diff --git a/test/default_style.jl b/test/default_style.jl index cf252497e..89b6f0b28 100644 --- a/test/default_style.jl +++ b/test/default_style.jl @@ -396,6 +396,14 @@ str = "!(typ <: ArithmeticTypes)" @test fmt(str) == str + text = "a + b" + d = JuliaFormatter.Document(text) + s = JuliaFormatter.State(d, Options()) + g = JuliaSyntax.parseall(JuliaSyntax.GreenNode, text) + op = JuliaSyntax.children(only(JuliaSyntax.children(g)))[3] + @test JuliaFormatter.source_operator_kind(s, op, UInt32(3)) === + JuliaSyntax.Kind("+") + @test fmt("1 // 2 + 3^4") == "1 // 2 + 3^4" @test fmt("1 // 2 + 3 ^ 4") == "1 // 2 + 3 ^ 4" From 51a5740811607240cf5804dec3ef3499e369754b Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Mon, 18 May 2026 15:26:15 +0200 Subject: [PATCH 12/27] fix formatting --- src/styles/default/pretty.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/styles/default/pretty.jl b/src/styles/default/pretty.jl index efc77f87e..b52bbb60f 100644 --- a/src/styles/default/pretty.jl +++ b/src/styles/default/pretty.jl @@ -2408,8 +2408,7 @@ function p_call( cst::JuliaSyntax.GreenNode, s::State, ctx::PrettyContext, - lineage::Vector{Tuple{JuliaSyntax.Kind,Bool,Bool}}, - ; + lineage::Vector{Tuple{JuliaSyntax.Kind,Bool,Bool}}; child_limit::Union{Int,Nothing} = nothing, ) style = getstyle(ds) From f6a79ad78337434c848132f4c58365113271ae0f Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 26 May 2026 14:41:57 +0200 Subject: [PATCH 13/27] add more tests for utils --- test/internal_utils.jl | 114 +++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 2 files changed, 115 insertions(+) create mode 100644 test/internal_utils.jl diff --git a/test/internal_utils.jl b/test/internal_utils.jl new file mode 100644 index 000000000..945b8f828 --- /dev/null +++ b/test/internal_utils.jl @@ -0,0 +1,114 @@ +@testset "Internal utilities" begin + # Build the same parser/document/state objects the formatter utilities consume. + function parsed_node(text) + doc = JuliaFormatter.Document(text) + state = JuliaFormatter.State(doc, Options()) + root = JuliaSyntax.parseall(JuliaSyntax.GreenNode, text) + nodes = filter(n -> !JuliaSyntax.is_whitespace(n), JuliaSyntax.children(root)) + return doc, state, only(nodes) + end + + @testset "source operator recovery" begin + # JuliaSyntax v1 can represent source operators as Identifier leaves; recover + # the actual operator kind from the original source text. + _, state, node = parsed_node("a + b") + op_indices = JuliaFormatter.extract_operator_indices(JuliaSyntax.children(node)) + op = JuliaSyntax.children(node)[only(op_indices)] + + @test JuliaFormatter.source_operator_kind(state, op, 3) === JuliaSyntax.Kind("+") + @test JuliaFormatter.source_op_kind(state, node, op_indices) === + JuliaSyntax.Kind("+") + + _, state, node = parsed_node("x .+ y") + op_indices = JuliaFormatter.extract_operator_indices(JuliaSyntax.children(node)) + @test JuliaFormatter.source_op_kind(state, node, op_indices) === + JuliaSyntax.Kind("+") + + # Prefix operator calls should be treated as unary only when the recovered + # source operator is valid in unary position. + _, state, node = parsed_node("+(y)") + @test JuliaFormatter.source_prefix_operator_index(node, state) == 1 + @test JuliaFormatter.is_source_prefix_op_call(node, state) + + _, state, node = parsed_node(">=(y)") + @test JuliaFormatter.source_prefix_operator_index(node, state) == 1 + @test !JuliaFormatter.is_source_prefix_op_call(node, state) + end + + @testset "short-form function utilities" begin + # Short-form definitions parse as function nodes but need binary-op handling + # so the equals sign keeps assignment-like spacing and metadata. + _, state, node = parsed_node("f(x) = x") + op_indices = JuliaFormatter.extract_operator_indices(JuliaSyntax.children(node)) + + @test JuliaFormatter.is_short_function_def(node) + @test JuliaFormatter.source_op_kind(state, node, op_indices) === + JuliaSyntax.Kind("=") + + # Compound assignments split the source operator across multiple child nodes. + _, state, node = parsed_node("x += 1") + op_indices = JuliaFormatter.update_operator_indices(JuliaSyntax.children(node)) + @test op_indices == [3, 4] + @test JuliaFormatter.source_op_kind(state, node, op_indices) === + JuliaSyntax.Kind("op=") + + # Long-form definitions must stay on the dedicated function-definition path. + _, _, node = parsed_node(""" + function f(x) + x + end + """) + @test !JuliaFormatter.is_short_function_def(node) + end + + @testset "do-block and iteration utilities" begin + # Calls with a do block are split so the call arguments and do body can be + # formatted by their dedicated paths. + _, _, node = parsed_node(""" + map(xs) do x + x + 1 + end + """) + childs = JuliaSyntax.children(node) + do_idx = JuliaFormatter.do_block_index(childs) + + @test do_idx == 5 + @test JuliaFormatter.has_do_block_call(node) + @test length(JuliaFormatter.call_args(childs[1:(do_idx-1)])) == 1 + + # Single-iterator loops can inspect the RHS iterable directly. + _, _, node = parsed_node(""" + for x in [1, 2] + end + """) + iter = JuliaSyntax.children(node)[2] + @test !JuliaFormatter.iteration_has_comma(iter) + @test JuliaSyntax.kind(JuliaFormatter.iteration_rhs(iter)) === + JuliaSyntax.Kind("vect") + + # Cartesian iterations need to detect the comma and use the final iteration + # expression as the effective RHS. + _, _, node = parsed_node(""" + for x in [1, 2], y in 3:4 + end + """) + iter = JuliaSyntax.children(node)[2] + @test JuliaFormatter.iteration_has_comma(iter) + @test JuliaSyntax.kind(JuliaFormatter.iteration_rhs(iter)) === + JuliaSyntax.Kind("call") + end + + @testset "display-width source offsets" begin + # Alignment uses display columns, not code-unit offsets; the combining mark + # in s\u0304_b should not add a visible column. + text = "s\u0304_b = 1\n" + doc = JuliaFormatter.Document(text) + eq_offset = findfirst(isequal('='), text) + + @test eq_offset > 10 + @test JuliaFormatter.source_display_line_offset(doc, 1, eq_offset) == 10 + @test JuliaFormatter.node_align_length( + JuliaFormatter.FST(JuliaFormatter.IDENTIFIER, 1, 1, 1, "s\u0304_b"), + ) == 3 + end +end diff --git a/test/runtests.jl b/test/runtests.jl index f8d9317bb..17c5d8e03 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -65,6 +65,7 @@ function run_format(text::String; style = DefaultStyle(), opts = Options()) end @testset "JuliaFormatter" begin + include("internal_utils.jl") include("default_style.jl") include("yas_style.jl") include("blue_style.jl") From f3578e44610d62ec615185a87f296aacd6785fee Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 26 May 2026 14:47:23 +0200 Subject: [PATCH 14/27] simplify is_short_function_def --- src/fst.jl | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/fst.jl b/src/fst.jl index 6f282e77d..3cc646cef 100644 --- a/src/fst.jl +++ b/src/fst.jl @@ -661,12 +661,8 @@ function is_function_or_macro_def(cst::JuliaSyntax.GreenNode) return false end -function is_short_function_def(cst::JuliaSyntax.GreenNode) - kind(cst) === K"function" && haschildren(cst) || return false - idx = findfirst(n -> !JuliaSyntax.is_whitespace(n), children(cst)) - isnothing(idx) && return false - return kind(cst[idx]) !== K"function" -end +is_short_function_def(cst::JuliaSyntax.GreenNode) = + JuliaSyntax.has_flags(cst, JuliaSyntax.SHORT_FORM_FUNCTION_FLAG) function is_function_like_lhs(node::JuliaSyntax.GreenNode) k = kind(node) From 71370f1097742e39cbf30c8e764e808780a1c135 Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 26 May 2026 14:56:53 +0200 Subject: [PATCH 15/27] cleanup --- src/fst.jl | 6 ++---- src/styles/default/pretty.jl | 2 ++ test/internal_utils.jl | 26 ++++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/fst.jl b/src/fst.jl index 3cc646cef..a80c676a7 100644 --- a/src/fst.jl +++ b/src/fst.jl @@ -629,10 +629,7 @@ function is_assignment(x::FST) end function is_assignment(t::JuliaSyntax.GreenNode) - if JuliaSyntax.is_prec_assignment(kind(t)) && haschildren(t) - return !any(c -> kind(c) in KSet"in ∈", children(t)) - end - return false + return JuliaSyntax.is_syntactic_assignment(t) && haschildren(t) end is_assignment(::Nothing) = false @@ -662,6 +659,7 @@ function is_function_or_macro_def(cst::JuliaSyntax.GreenNode) end is_short_function_def(cst::JuliaSyntax.GreenNode) = + kind(cst) === K"function" && JuliaSyntax.has_flags(cst, JuliaSyntax.SHORT_FORM_FUNCTION_FLAG) function is_function_like_lhs(node::JuliaSyntax.GreenNode) diff --git a/src/styles/default/pretty.jl b/src/styles/default/pretty.jl index b52bbb60f..1d2fb2588 100644 --- a/src/styles/default/pretty.jl +++ b/src/styles/default/pretty.jl @@ -314,6 +314,8 @@ function pretty( p_abstract(style, node, s, ctx, lineage) elseif k === K"primitive" p_primitive(style, node, s, ctx, lineage) + elseif k === K"module" && JuliaSyntax.has_flags(node, JuliaSyntax.BARE_MODULE_FLAG) + p_baremodule(style, node, s, ctx, lineage) elseif k === K"module" p_module(style, node, s, ctx, lineage) elseif k === K"baremodule" diff --git a/test/internal_utils.jl b/test/internal_utils.jl index 945b8f828..bbfd16c99 100644 --- a/test/internal_utils.jl +++ b/test/internal_utils.jl @@ -61,6 +61,19 @@ @test !JuliaFormatter.is_short_function_def(node) end + @testset "assignment predicate" begin + # Use JuliaSyntax's syntactic-assignment category instead of rebuilding + # assignment membership from precedence ranges and child checks. + _, _, node = parsed_node("x = 1") + @test JuliaFormatter.is_assignment(node) + + _, _, node = parsed_node("x += 1") + @test JuliaFormatter.is_assignment(node) + + _, _, node = parsed_node("x in xs") + @test !JuliaFormatter.is_assignment(node) + end + @testset "do-block and iteration utilities" begin # Calls with a do block are split so the call arguments and do body can be # formatted by their dedicated paths. @@ -111,4 +124,17 @@ JuliaFormatter.FST(JuliaFormatter.IDENTIFIER, 1, 1, 1, "s\u0304_b"), ) == 3 end + + @testset "module flags" begin + # JuliaSyntax v1 stores baremodule as a module node with a flag. + _, _, node = parsed_node(""" + baremodule A + end + """) + + @test JuliaSyntax.kind(node) === JuliaSyntax.Kind("module") + @test JuliaSyntax.has_flags(node, JuliaSyntax.BARE_MODULE_FLAG) + @test !JuliaFormatter.is_short_function_def(node) + @test JuliaFormatter.format_text("baremodule A\nend") == "baremodule A end" + end end From 2df12bbebe4ba2a91ada92f396fe19b35dad7c44 Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 26 May 2026 14:59:40 +0200 Subject: [PATCH 16/27] add comments --- src/styles/default/pretty.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/styles/default/pretty.jl b/src/styles/default/pretty.jl index 1d2fb2588..2b1f93d1f 100644 --- a/src/styles/default/pretty.jl +++ b/src/styles/default/pretty.jl @@ -179,6 +179,8 @@ function pretty( ret = if k == K"Identifier" && !haschildren(node) p_identifier(style, node, s, ctx, lineage) + # Example: `try f() catch g() end` has a zero-width Placeholder + # where a catch binding would appear. elseif k === K"Placeholder" s.offset += span(node) FST(NONE, 0, 0, 0, "") @@ -206,6 +208,7 @@ function pretty( p_begin(style, node, s, ctx, lineage) elseif k === K"block" p_block(style, node, s, ctx, lineage) + # Example: `f(x) = x` is a function node flagged as short-form. elseif is_short_function_def(node) p_binaryopcall(style, node, s, ctx, lineage) elseif k === K"function" @@ -252,6 +255,7 @@ function pretty( p_bracescat(style, node, s, ctx, lineage) elseif k === K"tuple" p_tuple(style, node, s, ctx, lineage) + # Example: `for x in xs, y in ys` uses an iteration node. elseif k === K"iteration" p_cartesian_iterator(style, node, s, ctx, lineage) elseif k === K"parens" @@ -268,10 +272,13 @@ function pretty( p_whereopcall(style, node, s, ctx, lineage) elseif k === K"?" && haschildren(node) p_conditionalopcall(style, node, s, ctx, lineage) + # Example: `map(xs) do x; x + 1; end` is a call node with a do child. elseif has_do_block_call(node) p_do_call(style, node, s, ctx, lineage) + # Example: `+(x)` parses as a call but the callee is a source unary operator. elseif is_source_prefix_op_call(node, s) p_unaryopcall(style, node, s, ctx, lineage) + # Example: `>=(x)` has a source operator callee but is not a unary op. elseif !isnothing(source_prefix_operator_index(node, s)) p_call(style, node, s, ctx, lineage) elseif is_binary(node) @@ -314,6 +321,7 @@ function pretty( p_abstract(style, node, s, ctx, lineage) elseif k === K"primitive" p_primitive(style, node, s, ctx, lineage) + # Example: `baremodule A end` is a module node with BARE_MODULE_FLAG. elseif k === K"module" && JuliaSyntax.has_flags(node, JuliaSyntax.BARE_MODULE_FLAG) p_baremodule(style, node, s, ctx, lineage) elseif k === K"module" From 2563425e3acb53c8bd4dba15ad3b2a8424babd77 Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 26 May 2026 15:31:31 +0200 Subject: [PATCH 17/27] added comment --- src/styles/default/pretty.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/styles/default/pretty.jl b/src/styles/default/pretty.jl index 2b1f93d1f..902f12d1f 100644 --- a/src/styles/default/pretty.jl +++ b/src/styles/default/pretty.jl @@ -33,6 +33,10 @@ function source_kind(s::State, cst::JuliaSyntax.GreenNode, offset::Integer) end end +# JuliaSyntax v1 can encode source operators as Identifier leaves in call +# forms, e.g. `a + b`, `x .+ y`, `+(x)`, and `>=(x)`. The parent call flags +# describe the shape, but the leaf itself is not `is_operator`, and the exact +# operator kind is only recoverable from the source span. function source_operator_kind(s::State, cst::JuliaSyntax.GreenNode, offset::Integer) if JuliaSyntax.is_operator(cst) && !haschildren(cst) return kind(cst) From 92311521a022adce1ea1254070d76ba1f047dd67 Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 26 May 2026 15:38:46 +0200 Subject: [PATCH 18/27] refactor based on suggestion --- src/fst.jl | 17 -------- src/styles/default/pretty.jl | 76 +++++++++++++++++++++++++----------- test/internal_utils.jl | 21 ++++------ 3 files changed, 62 insertions(+), 52 deletions(-) diff --git a/src/fst.jl b/src/fst.jl index a80c676a7..018feee4f 100644 --- a/src/fst.jl +++ b/src/fst.jl @@ -525,23 +525,6 @@ function is_gen(x::FST) x.typ in (Generator, Filter, Flatten) end -function extract_operator_indices(childs::Vector{JuliaSyntax.GreenNode{T}}) where {T} - args = findall(n -> !JuliaSyntax.is_whitespace(n), childs) - op_indices = Int[] - i = 2 - while i <= length(args) - push!(op_indices, args[i]) - if i < length(args) && - kind(childs[args[i]]) === K"." && - !haschildren(childs[args[i]]) - push!(op_indices, args[i+1]) - i += 1 - end - i += 2 - end - return op_indices -end - function _callinfo(x::JuliaSyntax.GreenNode) if !haschildren(x) return 0, 0 diff --git a/src/styles/default/pretty.jl b/src/styles/default/pretty.jl index 902f12d1f..a170caa94 100644 --- a/src/styles/default/pretty.jl +++ b/src/styles/default/pretty.jl @@ -113,28 +113,64 @@ function is_source_prefix_op_call(cst::JuliaSyntax.GreenNode, s::State) return operands == 1 end -function source_op_kind(s::State, cst::JuliaSyntax.GreenNode, op_indices::Vector{Int}) - opkind = op_kind(cst) - opkind !== K"None" && opkind !== K"Identifier" && return opkind +function source_operator_indices(cst::JuliaSyntax.GreenNode) + haschildren(cst) || return Int[] - offset = Int(s.offset) childs = children(cst) - for (i, c) in enumerate(childs) - if i in op_indices - k = source_operator_kind(s, c, offset) - if !isnothing(k) && !(kind(cst) === K"dotcall" && k === K".") - return k + args = findall(n -> !JuliaSyntax.is_whitespace(n), childs) + isempty(args) && return Int[] + + if kind(cst) === K"op=" + length(args) < 4 && return Int[] + return args[2:(end-1)] + elseif is_short_function_def(cst) + length(args) < 3 && return Int[] + return Int[args[2]] + elseif JuliaSyntax.is_prefix_op_call(cst) + if kind(cst) === K"dotcall" && + length(args) >= 2 && + kind(childs[args[1]]) === K"." && + !haschildren(childs[args[1]]) + return args[1:2] + else + return Int[args[1]] + end + elseif JuliaSyntax.is_infix_op_call(cst) + op_indices = Int[] + i = 2 + while i < length(args) + push!(op_indices, args[i]) + if kind(cst) === K"dotcall" && + i < length(args) && + kind(childs[args[i]]) === K"." && + !haschildren(childs[args[i]]) + push!(op_indices, args[i+1]) + i += 1 end + i += 2 end - offset += Int(span(c)) + return op_indices + elseif JuliaSyntax.is_postfix_op_call(cst) + return Int[args[end]] end - return opkind + + return Int[] end -function update_operator_indices(childs::Vector{JuliaSyntax.GreenNode{T}}) where {T} - args = findall(n -> !JuliaSyntax.is_whitespace(n), childs) - length(args) < 4 && return Int[] - return args[2:(end-1)] +function source_op_kind(s::State, cst::JuliaSyntax.GreenNode) + opkind = op_kind(cst) + opkind !== K"None" && opkind !== K"Identifier" && return opkind + + childs = children(cst) + for i in source_operator_indices(cst) + c = childs[i] + offset = Int(s.offset) + sum(span, childs[1:(i-1)]; init = 0) + k = source_operator_kind(s, c, offset) + if !isnothing(k) && !(kind(cst) === K"dotcall" && k === K".") + return k + end + end + return opkind end function do_block_index(childs::Vector{JuliaSyntax.GreenNode{T}}) where {T} @@ -1957,12 +1993,8 @@ function p_binaryopcall( end childs = children(cst) - op_indices = if kind(cst) === K"op=" - update_operator_indices(childs) - else - extract_operator_indices(childs) - end - opkind = source_op_kind(s, cst, op_indices) + op_indices = source_operator_indices(cst) + opkind = source_op_kind(s, cst) nonest = ctx.nonest || opkind === K":" @@ -2345,7 +2377,7 @@ function p_unaryopcall( if isnothing(op_idx) op_idx = first_idx end - opkind = isnothing(op_idx) ? op_kind(cst) : source_op_kind(s, cst, Int[op_idx]) + opkind = isnothing(op_idx) ? op_kind(cst) : source_op_kind(s, cst) op_dotted = kind(cst) === K"dotcall" t.metadata = Metadata(opkind, op_dotted) diff --git a/test/internal_utils.jl b/test/internal_utils.jl index bbfd16c99..6fe1a49f7 100644 --- a/test/internal_utils.jl +++ b/test/internal_utils.jl @@ -12,17 +12,15 @@ # JuliaSyntax v1 can represent source operators as Identifier leaves; recover # the actual operator kind from the original source text. _, state, node = parsed_node("a + b") - op_indices = JuliaFormatter.extract_operator_indices(JuliaSyntax.children(node)) + op_indices = JuliaFormatter.source_operator_indices(node) op = JuliaSyntax.children(node)[only(op_indices)] @test JuliaFormatter.source_operator_kind(state, op, 3) === JuliaSyntax.Kind("+") - @test JuliaFormatter.source_op_kind(state, node, op_indices) === - JuliaSyntax.Kind("+") + @test JuliaFormatter.source_op_kind(state, node) === JuliaSyntax.Kind("+") _, state, node = parsed_node("x .+ y") - op_indices = JuliaFormatter.extract_operator_indices(JuliaSyntax.children(node)) - @test JuliaFormatter.source_op_kind(state, node, op_indices) === - JuliaSyntax.Kind("+") + @test JuliaFormatter.source_operator_indices(node) == [3, 4] + @test JuliaFormatter.source_op_kind(state, node) === JuliaSyntax.Kind("+") # Prefix operator calls should be treated as unary only when the recovered # source operator is valid in unary position. @@ -39,18 +37,15 @@ # Short-form definitions parse as function nodes but need binary-op handling # so the equals sign keeps assignment-like spacing and metadata. _, state, node = parsed_node("f(x) = x") - op_indices = JuliaFormatter.extract_operator_indices(JuliaSyntax.children(node)) @test JuliaFormatter.is_short_function_def(node) - @test JuliaFormatter.source_op_kind(state, node, op_indices) === - JuliaSyntax.Kind("=") + @test JuliaFormatter.source_operator_indices(node) == [3] + @test JuliaFormatter.source_op_kind(state, node) === JuliaSyntax.Kind("=") # Compound assignments split the source operator across multiple child nodes. _, state, node = parsed_node("x += 1") - op_indices = JuliaFormatter.update_operator_indices(JuliaSyntax.children(node)) - @test op_indices == [3, 4] - @test JuliaFormatter.source_op_kind(state, node, op_indices) === - JuliaSyntax.Kind("op=") + @test JuliaFormatter.source_operator_indices(node) == [3, 4] + @test JuliaFormatter.source_op_kind(state, node) === JuliaSyntax.Kind("op=") # Long-form definitions must stay on the dedicated function-definition path. _, _, node = parsed_node(""" From 6b6abe988b2378b1b5accdfdb532b15b18c2d361 Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 26 May 2026 15:52:37 +0200 Subject: [PATCH 19/27] remove unneeded function --- src/styles/default/pretty.jl | 45 ++---------------------------------- test/internal_utils.jl | 4 ++-- 2 files changed, 4 insertions(+), 45 deletions(-) diff --git a/src/styles/default/pretty.jl b/src/styles/default/pretty.jl index a170caa94..de642a5f5 100644 --- a/src/styles/default/pretty.jl +++ b/src/styles/default/pretty.jl @@ -53,10 +53,6 @@ function is_source_operator(s::State, cst::JuliaSyntax.GreenNode, offset::Intege !isnothing(source_operator_kind(s, cst, offset)) end -function is_source_unary_operator(k::JuliaSyntax.Kind) - JuliaSyntax.is_unary_op(JuliaSyntax.SyntaxHead(k, UInt16(0))) -end - function source_prefix_operator_index(cst::JuliaSyntax.GreenNode, s::State) kind(cst) in KSet"call dotcall" && haschildren(cst) || return nothing childs = children(cst) @@ -76,43 +72,6 @@ function source_prefix_operator_index(cst::JuliaSyntax.GreenNode, s::State) return is_source_operator(s, childs[op_arg], offset) ? op_arg : nothing end -function is_source_prefix_op_call(cst::JuliaSyntax.GreenNode, s::State) - op_idx = source_prefix_operator_index(cst, s) - isnothing(op_idx) && return false - - childs = children(cst) - op_offset = s.offset + sum(span, childs[1:(op_idx-1)]; init = 0) - opkind = source_operator_kind(s, childs[op_idx], op_offset) - (isnothing(opkind) || !is_source_unary_operator(opkind)) && return false - - call_idx = findfirst(n -> kind(n) in KSet"( { [", childs) - if !isnothing(call_idx) - args = 0 - for c in childs[(call_idx+1):end] - kind(c) === K"parameters" && return false - kind(c) in KSet", ..." && return false - if !( - is_punc(c) || - kind(c) == K";" || - JuliaSyntax.is_whitespace(c) || - kind(c) in KSet"` ``` \" \"\"\"" - ) - args += 1 - end - end - return args == 1 - end - - args = findall(n -> !JuliaSyntax.is_whitespace(n), childs) - operands = 0 - for idx in args - idx == op_idx && continue - kind(childs[idx]) === K"." && !haschildren(childs[idx]) && continue - operands += 1 - end - return operands == 1 -end - function source_operator_indices(cst::JuliaSyntax.GreenNode) haschildren(cst) || return Int[] @@ -315,8 +274,8 @@ function pretty( # Example: `map(xs) do x; x + 1; end` is a call node with a do child. elseif has_do_block_call(node) p_do_call(style, node, s, ctx, lineage) - # Example: `+(x)` parses as a call but the callee is a source unary operator. - elseif is_source_prefix_op_call(node, s) + # Example: `+(x)` parses as a prefix-op call. + elseif JuliaSyntax.is_prefix_op_call(node) p_unaryopcall(style, node, s, ctx, lineage) # Example: `>=(x)` has a source operator callee but is not a unary op. elseif !isnothing(source_prefix_operator_index(node, s)) diff --git a/test/internal_utils.jl b/test/internal_utils.jl index 6fe1a49f7..15bf3f3c7 100644 --- a/test/internal_utils.jl +++ b/test/internal_utils.jl @@ -26,11 +26,11 @@ # source operator is valid in unary position. _, state, node = parsed_node("+(y)") @test JuliaFormatter.source_prefix_operator_index(node, state) == 1 - @test JuliaFormatter.is_source_prefix_op_call(node, state) + @test JuliaSyntax.is_prefix_op_call(node) _, state, node = parsed_node(">=(y)") @test JuliaFormatter.source_prefix_operator_index(node, state) == 1 - @test !JuliaFormatter.is_source_prefix_op_call(node, state) + @test !JuliaSyntax.is_prefix_op_call(node) end @testset "short-form function utilities" begin From d3fc853b32bed1367560772aa35d791469eb35a2 Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 26 May 2026 15:59:09 +0200 Subject: [PATCH 20/27] cleanup --- src/fst.jl | 12 +----------- src/styles/default/pretty.jl | 2 +- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/fst.jl b/src/fst.jl index 018feee4f..c5592a07d 100644 --- a/src/fst.jl +++ b/src/fst.jl @@ -259,7 +259,7 @@ is_comment(fst::FST) = fst.typ in (INLINECOMMENT, NOTCODE, HASHEQCOMMENT) is_identifier(x) = kind(x) === K"Identifier" && !haschildren(x) -is_ws(x) = kind(x) in KSet"Whitespace NewlineWs" +is_ws(x) = JuliaSyntax.is_whitespace(x) function is_multiline(fst::FST) fst.endline > fst.startline && @@ -507,16 +507,6 @@ function is_opcall(x::JuliaSyntax.GreenNode) return false end -function is_prefix_op_call(x::JuliaSyntax.GreenNode) - is_opcall(x) || return false - haschildren(x) || return false - - idx = findfirst(!JuliaSyntax.is_whitespace, children(x)) - isnothing(idx) && return false - - return JuliaSyntax.is_operator(x[idx]) -end - function is_gen(x::JuliaSyntax.GreenNode) kind(x) in KSet"generator filter" end diff --git a/src/styles/default/pretty.jl b/src/styles/default/pretty.jl index de642a5f5..7d21f2bef 100644 --- a/src/styles/default/pretty.jl +++ b/src/styles/default/pretty.jl @@ -1911,7 +1911,7 @@ function p_kw( n = pretty(style, c, s, ctx, lineage) if !s.opts.whitespace_in_kwargs && ( (n.typ === IDENTIFIER && endswith(n.val, "!")) || - is_prefix_op_call(c) || + JuliaSyntax.is_prefix_op_call(c) || source_prefix ) add_node!( From 1b8993c625c069829a6bc4a782743a325374d58f Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 26 May 2026 16:06:01 +0200 Subject: [PATCH 21/27] rename --- src/styles/default/pretty.jl | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/styles/default/pretty.jl b/src/styles/default/pretty.jl index 7d21f2bef..2d05be140 100644 --- a/src/styles/default/pretty.jl +++ b/src/styles/default/pretty.jl @@ -256,7 +256,7 @@ function pretty( p_tuple(style, node, s, ctx, lineage) # Example: `for x in xs, y in ys` uses an iteration node. elseif k === K"iteration" - p_cartesian_iterator(style, node, s, ctx, lineage) + p_iteration(style, node, s, ctx, lineage) elseif k === K"parens" p_invisbrackets(style, node, s, ctx, lineage) elseif k === K"curly" @@ -277,9 +277,6 @@ function pretty( # Example: `+(x)` parses as a prefix-op call. elseif JuliaSyntax.is_prefix_op_call(node) p_unaryopcall(style, node, s, ctx, lineage) - # Example: `>=(x)` has a source operator callee but is not a unary op. - elseif !isnothing(source_prefix_operator_index(node, s)) - p_call(style, node, s, ctx, lineage) elseif is_binary(node) p_binaryopcall(style, node, s, ctx, lineage) elseif is_chain(node) @@ -1621,7 +1618,7 @@ function p_for( t end -function p_cartesian_iterator( +function p_iteration( ds::AbstractStyle, cst::JuliaSyntax.GreenNode, s::State, From 0f9faa8a1323a8179473e978ed238019eb0e148b Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 26 May 2026 16:10:07 +0200 Subject: [PATCH 22/27] remove wrong nesting --- src/styles/default/pretty.jl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/styles/default/pretty.jl b/src/styles/default/pretty.jl index 2d05be140..393d7dc7a 100644 --- a/src/styles/default/pretty.jl +++ b/src/styles/default/pretty.jl @@ -1632,14 +1632,8 @@ function p_iteration( end childs = children(cst) - has_comma = iteration_has_comma(cst) for (i, c) in enumerate(childs) n = pretty(style, c, s, ctx, lineage) - if has_comma && kind(c) in KSet"in ∈" && is_iterable(iteration_rhs(c)) - if n.typ === Binary && n[end].typ === Vect - n[end].nest_behavior = AlwaysNest - end - end if kind(c) === K"," add_node!(t, n, s; join_lines = true) if needs_placeholder(childs, i + 1, K")") From 8ab3930ec67bc214b6da2a194d747e51f2e52d29 Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 26 May 2026 16:14:45 +0200 Subject: [PATCH 23/27] address unreachable do block --- src/styles/default/pretty.jl | 21 ++++++++++++--------- test/internal_utils.jl | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/styles/default/pretty.jl b/src/styles/default/pretty.jl index 393d7dc7a..28ba342f8 100644 --- a/src/styles/default/pretty.jl +++ b/src/styles/default/pretty.jl @@ -137,8 +137,8 @@ function do_block_index(childs::Vector{JuliaSyntax.GreenNode{T}}) where {T} end function has_do_block_call(cst::JuliaSyntax.GreenNode) - kind(cst) in KSet"call dotcall" && haschildren(cst) || return false - !isnothing(do_block_index(children(cst))) + kind(cst) in KSet"call dotcall" && haschildren(cst) || return nothing + do_block_index(children(cst)) end function call_args(childs::Vector{JuliaSyntax.GreenNode{T}}) where {T} @@ -174,6 +174,7 @@ function pretty( )::FST k = kind(node) style = getstyle(ds) + do_block_idx = has_do_block_call(node) push!(lineage, (k, is_iterable(node), is_assignment(node))) ret = if k == K"Identifier" && !haschildren(node) @@ -272,8 +273,8 @@ function pretty( elseif k === K"?" && haschildren(node) p_conditionalopcall(style, node, s, ctx, lineage) # Example: `map(xs) do x; x + 1; end` is a call node with a do child. - elseif has_do_block_call(node) - p_do_call(style, node, s, ctx, lineage) + elseif !isnothing(do_block_idx) + p_do_call(style, node, s, ctx, lineage, do_block_idx) # Example: `+(x)` parses as a prefix-op call. elseif JuliaSyntax.is_prefix_op_call(node) p_unaryopcall(style, node, s, ctx, lineage) @@ -1721,22 +1722,24 @@ function p_do_call( s::State, ctx::PrettyContext, lineage::Vector{Tuple{JuliaSyntax.Kind,Bool,Bool}}, + do_block_idx::Int, ) t = FST(Do, nspaces(s)) childs = children(cst) - idx = do_block_index(childs) - if isnothing(idx) - return p_call(ds, cst, s, ctx, lineage) + if !checkbounds(Bool, childs, do_block_idx) || + kind(childs[do_block_idx]) !== K"do" || + !haschildren(childs[do_block_idx]) + error("p_do_call called without a do block") end add_node!( t, - p_call(ds, cst, s, ctx, lineage; child_limit = idx - 1), + p_call(ds, cst, s, ctx, lineage; child_limit = do_block_idx - 1), s; join_lines = true, ) - do_node = childs[idx] + do_node = childs[do_block_idx] push!(lineage, (kind(do_node), is_iterable(do_node), is_assignment(do_node))) append_do_nodes!(t, ds, do_node, s, ctx, lineage) pop!(lineage) diff --git a/test/internal_utils.jl b/test/internal_utils.jl index 15bf3f3c7..1c7acb48b 100644 --- a/test/internal_utils.jl +++ b/test/internal_utils.jl @@ -81,7 +81,7 @@ do_idx = JuliaFormatter.do_block_index(childs) @test do_idx == 5 - @test JuliaFormatter.has_do_block_call(node) + @test JuliaFormatter.has_do_block_call(node) == do_idx @test length(JuliaFormatter.call_args(childs[1:(do_idx-1)])) == 1 # Single-iterator loops can inspect the RHS iterable directly. From ad2d5a49b5cb02ce5ac3d1ea05d9a95d5c14ecea Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 26 May 2026 16:56:10 +0200 Subject: [PATCH 24/27] implement suggestions --- src/fst.jl | 8 ++------ src/styles/default/pretty.jl | 36 ++++++++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/fst.jl b/src/fst.jl index c5592a07d..149081f72 100644 --- a/src/fst.jl +++ b/src/fst.jl @@ -804,17 +804,13 @@ function eq_to_in_normalization!(fst::FST, always_for_in::Bool, for_in_replaceme end if !isnothing(fst.metadata) metadata = fst.metadata::Metadata - opkind = try - JuliaSyntax.Kind(op.val) - catch - metadata.op_kind - end + opkind = JuliaSyntax.Kind(op.val) fst.metadata = Metadata( opkind, metadata.op_dotted, metadata.is_standalone_shortcircuit, metadata.is_short_form_function, - op.val == "=", + opkind === K"=", metadata.is_long_form_function, metadata.has_multiline_argument, ) diff --git a/src/styles/default/pretty.jl b/src/styles/default/pretty.jl index 28ba342f8..13b2211ff 100644 --- a/src/styles/default/pretty.jl +++ b/src/styles/default/pretty.jl @@ -82,9 +82,15 @@ function source_operator_indices(cst::JuliaSyntax.GreenNode) if kind(cst) === K"op=" length(args) < 4 && return Int[] return args[2:(end-1)] + elseif kind(cst) === K"comparison" + length(args) < 3 && return Int[] + return args[2:2:(end-1)] elseif is_short_function_def(cst) length(args) < 3 && return Int[] return Int[args[2]] + elseif JuliaSyntax.is_operator(cst) + length(args) < 3 && return Int[] + return args[2:(end-1)] elseif JuliaSyntax.is_prefix_op_call(cst) if kind(cst) === K"dotcall" && length(args) >= 2 && @@ -1734,7 +1740,7 @@ function p_do_call( add_node!( t, - p_call(ds, cst, s, ctx, lineage; child_limit = do_block_idx - 1), + p_call(ds, cst, s, ctx, lineage; do_block_idx = do_block_idx), s; join_lines = true, ) @@ -1901,11 +1907,13 @@ function p_kw( add_node!(t, pretty(style, c, s, ctx, lineage), s; join_lines = true) add_node!(t, Whitespace(1), s) else + # Need to explicitly check source_prefix rather than relying on + # JuliaSyntax.is_prefix_op_call, as the latter doesn't identify e.g. + # `>=(x)` as operators. source_prefix = !isnothing(source_prefix_operator_index(c, s)) n = pretty(style, c, s, ctx, lineage) if !s.opts.whitespace_in_kwargs && ( (n.typ === IDENTIFIER && endswith(n.val, "!")) || - JuliaSyntax.is_prefix_op_call(c) || source_prefix ) add_node!( @@ -2002,7 +2010,10 @@ function p_binaryopcall( end from_colon = ctx.from_colon - from_typedef = ctx.from_typedef || any(x -> x[1] === K"where", lineage) + from_typedef = ctx.from_typedef + parent_kind = length(lineage) > 1 ? lineage[end-1][1] : K"None" + preserve_assignment_spacing = + ctx.from_let || ctx.nospace || parent_kind in KSet"global local outer macrocall" nospace = ctx.nospace if opkind === K":" @@ -2013,6 +2024,11 @@ function p_binaryopcall( elseif is_short_form_function && opkind === K"=" nospace = false has_ws = true + elseif is_assignment(cst) && !preserve_assignment_spacing + nospace = false + has_ws = true + elseif kind(cst) === K"comparison" + nospace = false elseif opkind in KSet"in ∈ isa ." nospace = false elseif opkind in KSet"=> ->" || (opkind === K"|>" && !(ctx.from_ref || from_colon)) @@ -2408,7 +2424,7 @@ function p_call( s::State, ctx::PrettyContext, lineage::Vector{Tuple{JuliaSyntax.Kind,Bool,Bool}}; - child_limit::Union{Int,Nothing} = nothing, + do_block_idx::Union{Int,Nothing} = nothing, ) style = getstyle(ds) t = FST(Call, nspaces(s)) @@ -2417,13 +2433,13 @@ function p_call( end childs = children(cst) - if isnothing(child_limit) - idx = do_block_index(childs) - if !isnothing(idx) - childs = childs[1:(idx-1)] + if !isnothing(do_block_idx) + if !checkbounds(Bool, childs, do_block_idx) || + kind(childs[do_block_idx]) !== K"do" || + !haschildren(childs[do_block_idx]) + error("p_call called with an invalid do block index") end - else - childs = childs[1:child_limit] + childs = childs[1:(do_block_idx - 1)] end args = call_args(childs) From 6fe50ec67be479578471b66081f4eeb149a7d65d Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Tue, 26 May 2026 17:04:17 +0200 Subject: [PATCH 25/27] format --- src/fst.jl | 5 +++-- src/styles/default/pretty.jl | 22 ++++++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/fst.jl b/src/fst.jl index 149081f72..8d8173360 100644 --- a/src/fst.jl +++ b/src/fst.jl @@ -631,9 +631,10 @@ function is_function_or_macro_def(cst::JuliaSyntax.GreenNode) return false end -is_short_function_def(cst::JuliaSyntax.GreenNode) = +function is_short_function_def(cst::JuliaSyntax.GreenNode) kind(cst) === K"function" && - JuliaSyntax.has_flags(cst, JuliaSyntax.SHORT_FORM_FUNCTION_FLAG) + JuliaSyntax.has_flags(cst, JuliaSyntax.SHORT_FORM_FUNCTION_FLAG) +end function is_function_like_lhs(node::JuliaSyntax.GreenNode) k = kind(node) diff --git a/src/styles/default/pretty.jl b/src/styles/default/pretty.jl index 13b2211ff..32bbac4ef 100644 --- a/src/styles/default/pretty.jl +++ b/src/styles/default/pretty.jl @@ -185,8 +185,8 @@ function pretty( ret = if k == K"Identifier" && !haschildren(node) p_identifier(style, node, s, ctx, lineage) - # Example: `try f() catch g() end` has a zero-width Placeholder - # where a catch binding would appear. + # Example: `try f() catch g() end` has a zero-width Placeholder + # where a catch binding would appear. elseif k === K"Placeholder" s.offset += span(node) FST(NONE, 0, 0, 0, "") @@ -214,7 +214,7 @@ function pretty( p_begin(style, node, s, ctx, lineage) elseif k === K"block" p_block(style, node, s, ctx, lineage) - # Example: `f(x) = x` is a function node flagged as short-form. + # Example: `f(x) = x` is a function node flagged as short-form. elseif is_short_function_def(node) p_binaryopcall(style, node, s, ctx, lineage) elseif k === K"function" @@ -261,7 +261,7 @@ function pretty( p_bracescat(style, node, s, ctx, lineage) elseif k === K"tuple" p_tuple(style, node, s, ctx, lineage) - # Example: `for x in xs, y in ys` uses an iteration node. + # Example: `for x in xs, y in ys` uses an iteration node. elseif k === K"iteration" p_iteration(style, node, s, ctx, lineage) elseif k === K"parens" @@ -278,10 +278,10 @@ function pretty( p_whereopcall(style, node, s, ctx, lineage) elseif k === K"?" && haschildren(node) p_conditionalopcall(style, node, s, ctx, lineage) - # Example: `map(xs) do x; x + 1; end` is a call node with a do child. + # Example: `map(xs) do x; x + 1; end` is a call node with a do child. elseif !isnothing(do_block_idx) p_do_call(style, node, s, ctx, lineage, do_block_idx) - # Example: `+(x)` parses as a prefix-op call. + # Example: `+(x)` parses as a prefix-op call. elseif JuliaSyntax.is_prefix_op_call(node) p_unaryopcall(style, node, s, ctx, lineage) elseif is_binary(node) @@ -324,7 +324,7 @@ function pretty( p_abstract(style, node, s, ctx, lineage) elseif k === K"primitive" p_primitive(style, node, s, ctx, lineage) - # Example: `baremodule A end` is a module node with BARE_MODULE_FLAG. + # Example: `baremodule A end` is a module node with BARE_MODULE_FLAG. elseif k === K"module" && JuliaSyntax.has_flags(node, JuliaSyntax.BARE_MODULE_FLAG) p_baremodule(style, node, s, ctx, lineage) elseif k === K"module" @@ -1912,10 +1912,8 @@ function p_kw( # `>=(x)` as operators. source_prefix = !isnothing(source_prefix_operator_index(c, s)) n = pretty(style, c, s, ctx, lineage) - if !s.opts.whitespace_in_kwargs && ( - (n.typ === IDENTIFIER && endswith(n.val, "!")) || - source_prefix - ) + if !s.opts.whitespace_in_kwargs && + ((n.typ === IDENTIFIER && endswith(n.val, "!")) || source_prefix) add_node!( t, FST(PUNCTUATION, -1, n.startline, n.startline, "("), @@ -2439,7 +2437,7 @@ function p_call( !haschildren(childs[do_block_idx]) error("p_call called with an invalid do block index") end - childs = childs[1:(do_block_idx - 1)] + childs = childs[1:(do_block_idx-1)] end args = call_args(childs) From 9ad0e2fbb49cf23543068934ac953d071361a3a5 Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Wed, 27 May 2026 12:45:58 +0200 Subject: [PATCH 26/27] implement suggestions --- src/styles/default/pretty.jl | 32 ++++++++++++++++++++++---------- test/default_style.jl | 2 +- test/internal_utils.jl | 11 ++++++++++- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/styles/default/pretty.jl b/src/styles/default/pretty.jl index 32bbac4ef..093867116 100644 --- a/src/styles/default/pretty.jl +++ b/src/styles/default/pretty.jl @@ -37,7 +37,7 @@ end # forms, e.g. `a + b`, `x .+ y`, `+(x)`, and `>=(x)`. The parent call flags # describe the shape, but the leaf itself is not `is_operator`, and the exact # operator kind is only recoverable from the source span. -function source_operator_kind(s::State, cst::JuliaSyntax.GreenNode, offset::Integer) +function source_op_kind_from_offset(s::State, cst::JuliaSyntax.GreenNode, offset::Integer) if JuliaSyntax.is_operator(cst) && !haschildren(cst) return kind(cst) elseif kind(cst) === K"Identifier" && !haschildren(cst) @@ -50,7 +50,7 @@ function source_operator_kind(s::State, cst::JuliaSyntax.GreenNode, offset::Inte end function is_source_operator(s::State, cst::JuliaSyntax.GreenNode, offset::Integer) - !isnothing(source_operator_kind(s, cst, offset)) + !isnothing(source_op_kind_from_offset(s, cst, offset)) end function source_prefix_operator_index(cst::JuliaSyntax.GreenNode, s::State) @@ -77,19 +77,31 @@ function source_operator_indices(cst::JuliaSyntax.GreenNode) childs = children(cst) args = findall(n -> !JuliaSyntax.is_whitespace(n), childs) - isempty(args) && return Int[] + nonws_args = length(args) + nonws_args == 0 && + error("no non-whitespace children found for operator node; should not happen") if kind(cst) === K"op=" - length(args) < 4 && return Int[] + # 4 args for `x += y`, 5 args for `x .+= y` (the dot is preserved as a + # flag in JuliaSyntax but the kind is the same). + (nonws_args == 4 || nonws_args == 5) || + error("unexpected number of args for op= node", nonws_args, cst) return args[2:(end-1)] elseif kind(cst) === K"comparison" - length(args) < 3 && return Int[] + # `x < y < z < ... < w` has `2n - 1` args where `n >= 2`. + (iseven(nonws_args) || nonws_args < 3) && + error("unexpected number of args for comparison node", nonws_args, cst) return args[2:2:(end-1)] elseif is_short_function_def(cst) - length(args) < 3 && return Int[] + nonws_args != 3 && error( + "unexpected number of args for short function definition node", + nonws_args, + cst, + ) return Int[args[2]] elseif JuliaSyntax.is_operator(cst) - length(args) < 3 && return Int[] + nonws_args != 3 && + error("unexpected number of args for operator node", nonws_args, cst) return args[2:(end-1)] elseif JuliaSyntax.is_prefix_op_call(cst) if kind(cst) === K"dotcall" && @@ -130,7 +142,7 @@ function source_op_kind(s::State, cst::JuliaSyntax.GreenNode) for i in source_operator_indices(cst) c = childs[i] offset = Int(s.offset) + sum(span, childs[1:(i-1)]; init = 0) - k = source_operator_kind(s, c, offset) + k = source_op_kind_from_offset(s, c, offset) if !isnothing(k) && !(kind(cst) === K"dotcall" && k === K".") return k end @@ -2108,7 +2120,7 @@ function p_binaryopcall( if is_op && n.typ === IDENTIFIER n.typ = OPERATOR n.metadata = - Metadata(source_operator_kind(s, c, offset)::JuliaSyntax.Kind, is_dot) + Metadata(source_op_kind_from_offset(s, c, offset)::JuliaSyntax.Kind, is_dot) end if is_dot && haschildren(c) && length(children(c)) == 2 # [.] @@ -2356,7 +2368,7 @@ function p_unaryopcall( end n = pretty(style, c, s, ctx, lineage) if i == op_idx && n.typ === IDENTIFIER - k = source_operator_kind(s, c, offset) + k = source_op_kind_from_offset(s, c, offset) if !isnothing(k) n.typ = OPERATOR n.metadata = Metadata(k, false) diff --git a/test/default_style.jl b/test/default_style.jl index f3dd2eec4..702050408 100644 --- a/test/default_style.jl +++ b/test/default_style.jl @@ -401,7 +401,7 @@ s = JuliaFormatter.State(d, Options()) g = JuliaSyntax.parseall(JuliaSyntax.GreenNode, text) op = JuliaSyntax.children(only(JuliaSyntax.children(g)))[3] - @test JuliaFormatter.source_operator_kind(s, op, UInt32(3)) === + @test JuliaFormatter.source_op_kind_from_offset(s, op, UInt32(3)) === JuliaSyntax.Kind("+") @test fmt("1 // 2 + 3^4") == "1 // 2 + 3^4" diff --git a/test/internal_utils.jl b/test/internal_utils.jl index 1c7acb48b..9ee287d2a 100644 --- a/test/internal_utils.jl +++ b/test/internal_utils.jl @@ -15,13 +15,18 @@ op_indices = JuliaFormatter.source_operator_indices(node) op = JuliaSyntax.children(node)[only(op_indices)] - @test JuliaFormatter.source_operator_kind(state, op, 3) === JuliaSyntax.Kind("+") + @test JuliaFormatter.source_op_kind_from_offset(state, op, 3) === + JuliaSyntax.Kind("+") @test JuliaFormatter.source_op_kind(state, node) === JuliaSyntax.Kind("+") _, state, node = parsed_node("x .+ y") @test JuliaFormatter.source_operator_indices(node) == [3, 4] @test JuliaFormatter.source_op_kind(state, node) === JuliaSyntax.Kind("+") + _, state, node = parsed_node("x < y < z") + @test JuliaFormatter.source_operator_indices(node) == [3, 7] + @test JuliaFormatter.source_op_kind(state, node) === JuliaSyntax.Kind("<") + # Prefix operator calls should be treated as unary only when the recovered # source operator is valid in unary position. _, state, node = parsed_node("+(y)") @@ -47,6 +52,10 @@ @test JuliaFormatter.source_operator_indices(node) == [3, 4] @test JuliaFormatter.source_op_kind(state, node) === JuliaSyntax.Kind("op=") + _, state, node = parsed_node("x .+= 1") + @test JuliaFormatter.source_operator_indices(node) == [3, 4, 5] + @test JuliaFormatter.source_op_kind(state, node) === JuliaSyntax.Kind("op=") + # Long-form definitions must stay on the dedicated function-definition path. _, _, node = parsed_node(""" function f(x) From c99eedfae5fb7e5c081c216c07c3c0daea7704d6 Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Wed, 27 May 2026 13:43:00 +0200 Subject: [PATCH 27/27] fix tests --- src/styles/sciml/nest.jl | 78 +++++++++++++++++++++++++++++++++++----- test/sciml_style.jl | 21 +++++++++++ 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/src/styles/sciml/nest.jl b/src/styles/sciml/nest.jl index ce41018bb..6d638fdf9 100644 --- a/src/styles/sciml/nest.jl +++ b/src/styles/sciml/nest.jl @@ -95,11 +95,31 @@ function _align_dict_tuple_pair_arrows!(fst::FST) end end -function _align_pair_tuple_rhs!(fst::FST, s::State) +function _tuple_rhs_line_fits(rhs::FST, indent::Int, s::State) + nodes = rhs.nodes::Vector + idx = findfirst( + n -> + !(n.typ in (PUNCTUATION, WHITESPACE, PLACEHOLDER, NEWLINE, TRAILINGCOMMA)) && !is_closer(n), + nodes, + ) + isnothing(idx) && return true + + width, _ = length_to(rhs, (NEWLINE, PLACEHOLDER); start = idx) + return indent + width + rhs.extra_margin <= s.opts.margin +end + +function _align_pair_tuple_rhs!( + fst::FST, + s::State; + align_to_pair::Bool = true, + fallback_indent::Int = fst.indent + s.opts.indent, +) rhs = fst[end] rhs.typ === TupleN || return - desired_indent = if length(rhs.nodes::Vector) > 1 && rhs[2].typ === NEWLINE + desired_indent = if !align_to_pair + fallback_indent + elseif length(rhs.nodes::Vector) > 1 && rhs[2].typ === NEWLINE rhs.indent - 1 else fst.indent + node_align_length(fst[1:(end-1)]) + 1 @@ -108,6 +128,35 @@ function _align_pair_tuple_rhs!(fst::FST, s::State) _align_tuple_comments!(rhs) end +function _unnest_pair_tuple_body!(style::AbstractStyle, rhs::FST, s::State) + lo = s.line_offset + s.line_offset = rhs.indent + for n in rhs.nodes::Vector + n.typ === NEWLINE && (s.line_offset = rhs.indent; continue) + walk(unnest!(style; dedent = false), n, s) + end + s.line_offset = lo +end + +function _preserve_pair_tuple_newlines!(fst::FST; closer_indent::Int = fst.indent) + rhs = fst[end] + nodes = rhs.nodes::Vector + length(nodes) > 2 || return + + nodes[2].typ !== NEWLINE && (nodes[2] = Newline(; length = nodes[2].len)) + if is_closer(nodes[end]) && nodes[end-1].typ !== NEWLINE + nodes[end-1] = Newline(; length = nodes[end-1].len) + nodes[end].indent = closer_indent + end +end + +function _preserve_multiline_closer!(fst::FST) + nodes = fst.nodes::Vector + length(nodes) > 2 && is_closer(nodes[end]) || return + + nodes[end-1].typ !== NEWLINE && (nodes[end-1] = Newline(; length = nodes[end-1].len)) +end + function _unnest_short_binary_lines!(fst::FST, s::State) is_leaf(fst) && return @@ -138,6 +187,8 @@ function n_binaryopcall!( rhs_style = YASStyle(style) nodes = fst.nodes::Vector oplen = sum(length.(fst[2:end])) + line_offset = s.line_offset + fallback_indent = line_offset + s.opts.indent nested = false for (i, n) in enumerate(nodes) if n.typ === NEWLINE @@ -147,13 +198,19 @@ function n_binaryopcall!( nested |= nest!(style, n, s, lineage) elseif i == length(nodes) n.extra_margin = fst.extra_margin - n.indent = s.line_offset + align_to_pair = _tuple_rhs_line_fits(n, s.line_offset, s) + n.indent = align_to_pair ? s.line_offset : fallback_indent nested |= nest!(rhs_style, n, s, lineage) - _align_pair_tuple_rhs!(fst, s) - lo = s.line_offset - walk(unnest!(rhs_style; dedent = false), n, s) - s.line_offset = lo - _unnest_short_binary_lines!(n, s) + _align_pair_tuple_rhs!(fst, s; align_to_pair, fallback_indent) + if align_to_pair + lo = s.line_offset + walk(unnest!(rhs_style; dedent = false), n, s) + s.line_offset = lo + _unnest_short_binary_lines!(n, s) + else + _unnest_pair_tuple_body!(rhs_style, n, s) + _preserve_pair_tuple_newlines!(fst; closer_indent = line_offset) + end _align_tuple_comments!(n) else nested |= nest!(style, n, s, lineage) @@ -273,7 +330,9 @@ function _n_tuple!( ) style = getstyle(ss) nodes = fst.nodes::Vector - _is_dict_call(fst) && _align_dict_tuple_pair_arrows!(fst) + _is_dict_call(fst) && + fst.startline != fst.endline && + _align_dict_tuple_pair_arrows!(fst) line_margin = s.line_offset + length(fst) + fst.extra_margin has_closer = is_closer(fst[end]) @@ -395,6 +454,7 @@ function _n_tuple!( s.line_offset = start_line_offset walk(unnest!(style; dedent = false), fst, s) + _is_dict_call(fst) && fst.startline != fst.endline && _preserve_multiline_closer!(fst) s.line_offset = start_line_offset walk(increment_line_offset!, fst, s) diff --git a/test/sciml_style.jl b/test/sciml_style.jl index 138ca86fb..2b7790935 100644 --- a/test/sciml_style.jl +++ b/test/sciml_style.jl @@ -762,6 +762,27 @@ only(filter(line -> contains(line, "viscosity_model"), formatted_lines)) @test length(comment_line) - length(lstrip(comment_line)) == length(option_line) - length(lstrip(option_line)) + + str = "Dict(\"a\" => (x = 1,), \"longer key\" => (y = 2,))" + @test format_text(str, SciMLStyle()) == str + + str = """ + d = Dict( + "a moderately long key" => ( + x = some_long_function_name(alpha, beta), + ), + ) + """ + fstr = """ + d = Dict( + "a moderately long key" => ( + x = some_long_function_name(alpha, beta), + ), + ) + """ + formatted = format_text(str, SciMLStyle(); margin = 50) + @test formatted == fstr + @test all(length(line) <= 50 for line in split(formatted, '\n')) end end