diff --git a/src/styles/default/nest.jl b/src/styles/default/nest.jl index a1fd78f3e..71a791813 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,15 @@ 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 = + if op_kind(fst) === K"=>" && + s.opts.yas_style_nesting && + !(style isa YASStyle) + YASStyle(style) + else + style + end + walk(unnest!(unnest_style; dedent = true), rhs, s) end end end diff --git a/src/styles/sciml/nest.jl b/src/styles/sciml/nest.jl index 974832e66..6d638fdf9 100644 --- a/src/styles/sciml/nest.jl +++ b/src/styles/sciml/nest.jl @@ -44,6 +44,195 @@ 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 _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) + 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_tuple_pair(fst::FST) + fst.typ === Binary && op_kind(fst) === K"=>" && fst[end].typ === TupleN +end + +function _align_dict_tuple_pair_arrows!(fst::FST) + 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) + for i in pair_inds + pair = fst[i] + ws = align_len - node_align_length(pair[1]) + 1 + align_binaryopcall!(pair, ws) + end +end + +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 !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 + end + add_indent!(rhs, s, desired_indent - rhs.indent) + _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 + + 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 + end + + for n in fst.nodes::Vector + _unnest_short_binary_lines!(n, s) + end +end + +function n_binaryopcall!( + ss::SciMLStyle, + fst::FST, + s::State, + 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])) + line_offset = s.line_offset + fallback_indent = line_offset + s.opts.indent + 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 + 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; 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) + end + end + return nested + end + + 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, @@ -96,6 +285,43 @@ 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 _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, @@ -103,8 +329,12 @@ 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) && + 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]) start_line_offset = s.line_offset @@ -224,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) @@ -237,40 +468,21 @@ 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 + 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} + 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 - end - if is_lhs_of_assignment - # Don't break the LHS of an assignment - # Format children but keep them on the same line 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) @@ -280,6 +492,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 8ed8b5799..2b7790935 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]) @@ -36,6 +36,7 @@ formatted = format_text(str, SciMLStyle()) @test !contains(formatted, "du[\n") @test startswith(strip(formatted), "du[i, j, 1]") + str = raw""" @noinline require_complete(m::Matching) = m.inv_match === nothing && throw(ArgumentError("Backwards matching not defined. `complete` the matching first.")) """ @@ -621,6 +622,168 @@ """ @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 = """ + 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), + 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 + + 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 + + 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)) + + 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 str = raw""" @@ -635,7 +798,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]]) @@ -644,25 +807,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()) @@ -671,7 +834,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 @@ -687,7 +850,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]] + @@ -705,7 +868,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()) @@ -717,7 +880,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()) @@ -725,26 +888,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()) @@ -757,27 +920,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 @@ -785,60 +948,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()) @@ -847,19 +1010,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 @@ -868,7 +1031,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"), @@ -877,171 +1040,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 = [ @@ -1055,40 +1218,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 @@ -1097,7 +1260,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()) @@ -1105,31 +1268,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() @@ -1142,21 +1305,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 @@ -1169,11 +1332,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 @@ -1184,14 +1347,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 @@ -1200,23 +1363,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 = """ @@ -1226,11 +1389,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} @@ -1238,7 +1401,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 @@ -1248,18 +1411,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 = [ @@ -1273,7 +1436,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 @@ -1284,10 +1447,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} @@ -1296,32 +1459,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 @@ -1330,29 +1493,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 @@ -1361,41 +1524,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 @@ -1412,11 +1575,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 σ ρ β @@ -1425,7 +1588,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')