Skip to content

Commit a8f0ced

Browse files
authored
Fix cases where trailing comma should not be added (#1086)
Closes #1017
1 parent 7897abe commit a8f0ced

6 files changed

Lines changed: 120 additions & 19 deletions

File tree

HISTORY_v2.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# v2.6.13
2+
3+
Fixed a bug where JuliaFormatter would insert trailing commas after expressions that had macros or `global` keywords, leading to syntactically invalid Julia code. (#1017, #1086)
4+
15
# v2.6.12
26

37
Fixed a bug where `;;\n` separators in rows of array literals were being converted to `;;`, leading to invalid Julia code. (#1080, #1083)

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "JuliaFormatter"
22
uuid = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
3-
version = "2.6.12"
3+
version = "2.6.13"
44
authors = ["Dominique Luna <dluna132@gmail.com> and contributors"]
55

66
[deps]

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ makedocs(;
3434
"Contributing" => "contributing.md",
3535
"Project Status" => "status.md",
3636
],
37+
checkdocs = :export,
3738
)
3839

3940
deploydocs(;

src/fst.jl

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,45 @@ function next_node_is(f::Function, nn::JuliaSyntax.GreenNode)
10061006
f(nn) || (haschildren(nn) && next_node_is(f, nn[1]))
10071007
end
10081008

1009+
function ends_with_macro_or_global(fst::FST)
1010+
# Detects constructs such as
1011+
# @macro foo
1012+
# arg = @macro foo
1013+
# arg => @macro foo
1014+
# () -> global x = true
1015+
# which cause parse errors if a comma is inserted after them.
1016+
return if fst.typ === MacroCall || fst.typ === MacroBlock || fst.typ === Global
1017+
true
1018+
elseif is_leaf(fst)
1019+
false
1020+
elseif fst.typ === Kw || fst.typ === Binary
1021+
length(fst.nodes) > 0 && ends_with_macro_or_global(fst[end])
1022+
else
1023+
false
1024+
end
1025+
end
1026+
function skip_trailing_comma(fst::FST)
1027+
prev_node = fst[end]
1028+
return if is_comma(prev_node) && fst.typ === TupleN && n_args(fst) == 1
1029+
# e.g. `(x,)` -- removing the comma changes the meaning
1030+
true
1031+
elseif (
1032+
prev_node.typ === Generator ||
1033+
prev_node.typ === Filter ||
1034+
prev_node.typ === Flatten ||
1035+
prev_node.typ === SEMICOLON ||
1036+
prev_node.typ === HASHEQCOMMENT ||
1037+
# Things that end with macros can't have a trailing comma inserted after them
1038+
# as that causes Julia to fail to parse.
1039+
# https://github.com/JuliaEditorSupport/JuliaFormatter.jl/issues/1017
1040+
ends_with_macro_or_global(prev_node)
1041+
)
1042+
true
1043+
else
1044+
false
1045+
end
1046+
end
1047+
10091048
"""
10101049
add_node!(
10111050
t::FST,
@@ -1047,28 +1086,14 @@ function add_node!(
10471086

10481087
if n.typ === TRAILINGCOMMA
10491088
en = (tnodes::Vector{FST})[end]
1050-
if en.typ === Generator ||
1051-
en.typ === Filter ||
1052-
en.typ === Flatten ||
1053-
en.typ === MacroCall ||
1054-
en.typ === MacroBlock ||
1055-
en.typ === SEMICOLON ||
1056-
en.typ === HASHEQCOMMENT ||
1057-
# arg = @macro foo
1058-
en.typ === Kw && (en[end].typ === MacroCall || en[end].typ === MacroBlock) ||
1059-
# arg => @macro foo
1060-
en.typ === Binary && (en[end].typ === MacroCall || en[end].typ === MacroBlock)
1061-
1062-
# adding a trailing comma causes a syntax error
1063-
false
1064-
elseif is_comma(en) && t.typ === TupleN && n_args(t) == 1
1065-
# e.g. `(x,)` -- removing the comma changes the meaning
1089+
if skip_trailing_comma(t)
1090+
# do not insert trailing comma
10661091
false
10671092
elseif s.opts.trailing_comma === nothing
10681093
# preserve original source code
10691094
false
10701095
elseif !s.opts.trailing_comma::Bool
1071-
# remove trailing comma
1096+
# remove preexisting comma in FST
10721097
if is_comma(en)
10731098
t[end] = Whitespace(0)
10741099
end

src/internal/utils.jl

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ function format_to_stage(
117117
throw(ArgumentError("unknown stage: $stage"))
118118
end
119119

120+
"""
121+
_repro_hint(input, style, options)
122+
123+
Generate a hint for how to reproduce a failure in `test_format`, given the input string,
124+
style, and options.
125+
"""
120126
function _repro_hint(input, style, options)
121127
opts_str = if isempty(options)
122128
""
@@ -131,6 +137,33 @@ function _repro_hint(input, style, options)
131137
return "s = $(repr(input))\nformat_text(s$(style_str)$(opts_str))"
132138
end
133139

140+
"""
141+
_normalise_ast!(ex)
142+
143+
Normalise ASTs (mostly by removing line number nodes, which are not relevant for semantics).
144+
"""
145+
_normalise_ast!(ex) = ex
146+
function _normalise_ast!(ex::Expr)
147+
if ex.head === :block || ex.head === :quote || ex.head === :toplevel
148+
filter!(ex.args) do x
149+
!(isa(x, Expr) && x.head === :line || isa(x, LineNumberNode))
150+
end
151+
end
152+
# macrocall's apparently _need_ to have a line number node or something in that
153+
# position, so we can't remove it altogether -- but we can replace all such nodes with
154+
# `nothing` so that they compare equal. See: https://stackoverflow.com/a/53285644
155+
if ex.head === :macrocall
156+
new_args = map(ex.args) do x
157+
x isa LineNumberNode ? nothing : x
158+
end
159+
ex.args = new_args
160+
end
161+
for subex in ex.args
162+
subex isa Expr && _normalise_ast!(subex)
163+
end
164+
return ex
165+
end
166+
134167
"""
135168
JuliaFormatter.Internal.test_format(
136169
input::AbstractString,
@@ -182,6 +215,8 @@ function test_format(
182215
if ast
183216
ast_in = Meta.parse(input)
184217
ast_out = Meta.parse(out)
218+
_normalise_ast!(ast_in)
219+
_normalise_ast!(ast_out)
185220
if ast_in != ast_out
186221
printstyled("AST of input and output did not match.\n\n"; color = :cyan)
187222
printstyled("Input AST:\n$ast_in\n\n"; color = :green)

test/issues.jl

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2471,7 +2471,43 @@ end
24712471
test_format(s, s)
24722472
end
24732473

2474-
@testset "1018" begin
2474+
@testset "1017 trailing commas after macros/global" begin
2475+
# macro
2476+
s_ = "f(a = u -> @. u0)"
2477+
s = "f(\n a = u ->\n @. u0\n)"
2478+
for trailing_comma in (true, false, nothing)
2479+
test_format(s_, s_; trailing_comma = trailing_comma)
2480+
test_format(s_, s; margin = 10, trailing_comma = trailing_comma)
2481+
for style in (SciMLStyle(), BlueStyle(), YASStyle(), MinimalStyle())
2482+
test_format(s_, nothing; margin=10, ast=true)
2483+
end
2484+
end
2485+
2486+
# global
2487+
s_ = raw"""
2488+
let libccalllazyfoo = LazyLibrary(lclf_path; on_load_callback=() -> global lclf_loaded = true),
2489+
libccalllazybar = LazyLibrary(lclb_path; dependencies=[libccalllazyfoo], on_load_callback=() -> global lclb_loaded = true)
2490+
eval(:(const libccalllazyfoo = $libccalllazyfoo))
2491+
eval(:(const libccalllazybar = $libccalllazybar))
2492+
end"""
2493+
s = raw"""
2494+
let libccalllazyfoo =
2495+
LazyLibrary(lclf_path; on_load_callback = () -> global lclf_loaded = true),
2496+
libccalllazybar = LazyLibrary(
2497+
lclb_path;
2498+
dependencies = [libccalllazyfoo],
2499+
on_load_callback = () -> global lclb_loaded = true
2500+
)
2501+
2502+
eval(:(const libccalllazyfoo = $libccalllazyfoo))
2503+
eval(:(const libccalllazybar = $libccalllazybar))
2504+
end"""
2505+
for trailing_comma in (true, false, nothing)
2506+
test_format(s_, s; trailing_comma = trailing_comma)
2507+
end
2508+
end
2509+
2510+
@testset "1018 macro do-block" begin
24752511
# A macro call using `do` with keyword-style args must be formatted like the
24762512
# function-call form, not left partially unformatted with extra spaces and
24772513
# unindented multiline `do` arguments.

0 commit comments

Comments
 (0)