Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop trailing semicolons in block contexts #47

Merged
merged 1 commit into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Runic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode}
@return_something no_leading_and_single_trailing_newline(ctx, node)
@return_something max_three_consecutive_newlines(ctx, node)
@return_something insert_delete_mark_newlines(ctx, node)
@return_something remove_trailing_semicolon(ctx, node)
@return_something trim_trailing_whitespace(ctx, node)
@return_something format_hex_literals(ctx, node)
@return_something format_float_literals(ctx, node)
Expand Down
129 changes: 128 additions & 1 deletion src/runestone.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1809,7 +1809,6 @@ function indent_let(ctx::Context, node::Node)
return any_kid_changed ? make_node(node, kids) : nothing
end

# TODO: Reuse indent_block?
function indent_begin(ctx::Context, node::Node, block_kind = K"begin")
@assert kind(node) === K"block"
pos = position(ctx.fmt_io)
Expand Down Expand Up @@ -3200,3 +3199,131 @@ function indent_multiline_strings(ctx::Context, node::Node)
seek(ctx.fmt_io, pos)
return any_changes ? make_node(node, kids′) : nothing
end

# Pattern matching for "bad" semicolons:
# - `\s*;\n` -> `\n`
# - `\s*;\s*#\n` -> `\s* \s*#\n`
function remove_trailing_semicolon_block(ctx::Context, node::Node)
kind(node) === K"block" || return nothing
@assert !is_leaf(node)
pos = position(ctx.fmt_io)
kids = verified_kids(node)
kids′ = kids
dealias() = kids′ === kids ? copy(kids) : kids′
function kmatch(i, kinds)
if i < 1 || i + length(kinds) - 1 > length(kids′)
return false
end
for (j, k) in pairs(kinds)
if kind(kids′[i + j - 1]) !== k
return false
end
end
return true
end
semi_idx = findfirst(x -> kind(x) === K";", kids′)
while semi_idx !== nothing
search_index = semi_idx + 1
if kmatch(semi_idx, KSet"; NewlineWs")
# `\s*;\n` -> `\n`
kids′ = dealias()
space_before = kmatch(semi_idx - 1, KSet"Whitespace ;")
if space_before
span_overwrite = span(kids′[semi_idx - 1]) + span(kids′[semi_idx])
nodes_to_skip_over = semi_idx - 2
deleteat!(kids′, semi_idx)
deleteat!(kids′, semi_idx - 1)
search_index = semi_idx - 1
else
span_overwrite = span(kids′[semi_idx])
nodes_to_skip_over = semi_idx - 1
deleteat!(kids′, semi_idx)
search_index = semi_idx
end
let p = position(ctx.fmt_io)
for i in 1:nodes_to_skip_over
accept_node!(ctx, kids′[i])
end
replace_bytes!(ctx, "", span_overwrite)
seek(ctx.fmt_io, p)
end
elseif kmatch(semi_idx, KSet"; Comment NewlineWs") ||
kmatch(semi_idx, KSet"; Whitespace Comment NewlineWs")
# `\s*;\s*#\n` -> `\s* \s*#\n`
# The `;` is replaced by ` ` here in case comments are aligned
kids′ = dealias()
ws_span = span(kids′[semi_idx])
@assert ws_span == 1
space_before = kmatch(semi_idx - 1, KSet"Whitespace ;")
if space_before
ws_span += span(kids′[semi_idx - 1])
end
space_after = kmatch(semi_idx, KSet"; Whitespace")
if space_after
ws_span += span(kids′[semi_idx + 1])
end
let p = position(ctx.fmt_io)
for i in 1:(semi_idx - 1)
accept_node!(ctx, kids′[i])
end
replace_bytes!(ctx, " ", span(kids′[semi_idx]))
seek(ctx.fmt_io, p)
end
# Insert new node
@assert kind(kids′[semi_idx]) === K";"
ws = Node(JuliaSyntax.SyntaxHead(K"Whitespace", JuliaSyntax.TRIVIA_FLAG), ws_span)
kids′[semi_idx] = ws
# Delete the consumed whitespace nodes
space_after && deleteat!(kids′, semi_idx + 1)
space_before && deleteat!(kids′, semi_idx - 1)
end
# Compute next index
semi_idx = findnext(x -> kind(x) === K";", kids′, search_index)
end
# Reset the stream and return
seek(ctx.fmt_io, pos)
return kids′ === kids ? nothing : make_node(node, kids′)
end

function remove_trailing_semicolon(ctx::Context, node::Node)
if is_begin_block(node)
r = remove_trailing_semicolon_block(ctx, node)
return r
end
if !(!is_leaf(node) && kind(node) in KSet"if elseif quote function for let while macro try catch finally else")
return nothing
end
if kind(node) === K"quote" && JuliaSyntax.has_flags(node, JuliaSyntax.COLON_QUOTE)
# This node is `:(...)` and not `quote...end`
return nothing
end
pos = position(ctx.fmt_io)
kids = verified_kids(node)
kids′ = kids
block_idx = findfirst(x -> kind(x) === K"block", kids′)
if kind(node) === K"let"
# The first block of let is the variables
block_idx = findnext(x -> kind(x) === K"block", kids′, block_idx + 1)
end
any_changed = false
while block_idx !== nothing
let p = position(ctx.fmt_io)
for i in 1:(block_idx - 1)
accept_node!(ctx, kids′[i])
end
block′ = remove_trailing_semicolon_block(ctx, kids′[block_idx])
if block′ !== nothing
any_changed = true
if kids′ === kids
kids′ = copy(kids)
end
kids′[block_idx] = block′
end
seek(ctx.fmt_io, p)
end
block_idx = findnext(x -> kind(x) === K"block", kids′, block_idx + 1)
end
# Reset the stream and return
seek(ctx.fmt_io, pos)
return any_changed ? make_node(node, kids′) : nothing
end
89 changes: 88 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ using Runic:
using Test:
@test, @testset, @test_broken, @inferred, @test_throws
using JuliaSyntax:
JuliaSyntax
JuliaSyntax, @K_str, @KSet_str

@testset "Node" begin
node = Runic.Node(JuliaSyntax.parseall(JuliaSyntax.GreenNode, "a = 1 + b\n"))
Expand Down Expand Up @@ -53,6 +53,12 @@ end
@test read(io, String) == "axc"
end

@testset "JuliaSyntax assumptions" begin
# Duplicates are kept in KSet
@test KSet"; ;" == (K";", K";")
@test KSet"Whitespace ; Whitespace" == (K"Whitespace", K";", K"Whitespace")
end

@testset "Trailing whitespace" begin
io = IOBuffer()
println(io, "a = 1 ") # Trailing space
Expand Down Expand Up @@ -1109,6 +1115,87 @@ end
end
end

@testset "trailing semicolon" begin
body = """
# Semicolons on their own lines
;
;;
# Trailing semicolon
a;
a;;
# Trailing semicolon with ws after
b;
b;;
# Trailing semicolon with ws before
c ;
c ;;
# Trailing semicolon with ws before and after
d ;
d ;;
# Trailing semicolon before comment
e;# comment
e;;# comment
# Trailing semicolon before ws+comment
f; # comment
f;; # comment
"""
bodyfmt = """
# Semicolons on their own lines


# Trailing semicolon
a
a
# Trailing semicolon with ws after
b
b
# Trailing semicolon with ws before
c
c
# Trailing semicolon with ws before and after
d
d
# Trailing semicolon before comment
e # comment
e # comment
# Trailing semicolon before ws+comment
f # comment
f # comment
"""
for prefix in (
"begin", "quote", "for i in I", "let", "let x = 1", "while cond",
"if cond", "macro f()", "function f()",
)
@test format_string("$(prefix)\n$(body)\nend") == "$prefix\n$(bodyfmt)\nend"
end
@test format_string(
"if cond1\n$(body)\nelseif cond2\n$(body)\nelseif cond3\n$(body)\nelse\n$(body)\nend",
) ==
"if cond1\n$(bodyfmt)\nelseif cond2\n$(bodyfmt)\nelseif cond3\n$(bodyfmt)\nelse\n$(bodyfmt)\nend"
@test format_string("try\n$(body)\ncatch\n$(body)\nend") ==
"try\n$(bodyfmt)\ncatch\n$(bodyfmt)\nend"
@test format_string("try\n$(body)\ncatch err\n$(body)\nend") ==
"try\n$(bodyfmt)\ncatch err\n$(bodyfmt)\nend"
@test format_string("try\n$(body)\nfinally\n$(body)\nend") ==
"try\n$(bodyfmt)\nfinally\n$(bodyfmt)\nend"
@test format_string("try\n$(body)\ncatch\n$(body)\nfinally\n$(body)\nend") ==
format_string("try\n$(bodyfmt)\ncatch\n$(bodyfmt)\nfinally\n$(bodyfmt)\nend")
@test format_string("try\n$(body)\ncatch err\n$(body)\nfinally\n$(body)\nend") ==
format_string("try\n$(bodyfmt)\ncatch err\n$(bodyfmt)\nfinally\n$(bodyfmt)\nend")
@test format_string("try\n$(body)\ncatch err\n$(body)\nelse\n$(body)\nend") ==
format_string("try\n$(bodyfmt)\ncatch err\n$(bodyfmt)\nelse\n$(bodyfmt)\nend")
# Top-level semicolons are kept (useful if you want to supress output in various
# contexts)
let str = """
f(x) = 1;
module A
g(x) = 2;
end;
"""
@test format_string(str) == str
end
end

@testset "# runic: (on|off)" begin
for exc in ("", "!"), word in ("runic", "format")
on = "#$(exc) $(word): on"
Expand Down
Loading