A code formatter for Julia with rules set in stone.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

900 lines
30 KiB

# SPDX-License-Identifier: MIT
########################################################
# Node utilities extensions and JuliaSyntax extensions #
########################################################
# JuliaSyntax.jl overloads == for this but seems easier to just define a new function
function nodes_equal(n1::Node, n2::Node)
head(n1) == head(n2) && span(n1) == span(n2) || return false
# juliac: this is `all(((x, y),) -> nodes_equal(x, y), zip(n1.kids, n2.kids))` but
# written out as an explicit loop to help inference.
if is_leaf(n1)
return is_leaf(n2)
end
kids1 = verified_kids(n1)
kids2 = verified_kids(n2)
length(kids1) == length(kids2) || return false
for i in eachindex(kids1)
nodes_equal(n1.kids[i], n2.kids[i]) || return false
end
return true
end
# See JuliaSyntax/src/parse_stream.jl
function stringify_flags(node::Node)
io = IOBuffer()
if JuliaSyntax.has_flags(node, JuliaSyntax.TRIVIA_FLAG)
write(io, "trivia,")
end
if JuliaSyntax.is_operator(kind(node))
if JuliaSyntax.has_flags(node, JuliaSyntax.DOTOP_FLAG)
write(io, "dotted,")
end
if JuliaSyntax.has_flags(node, JuliaSyntax.SUFFIXED_FLAG)
write(io, "suffixed,")
end
end
if kind(node) in KSet"call dotcall"
if JuliaSyntax.has_flags(node, JuliaSyntax.PREFIX_CALL_FLAG)
write(io, "prefix-call,")
end
if JuliaSyntax.has_flags(node, JuliaSyntax.INFIX_FLAG)
write(io, "infix-op,")
end
if JuliaSyntax.has_flags(node, JuliaSyntax.PREFIX_OP_FLAG)
write(io, "prefix-op,")
end
if JuliaSyntax.has_flags(node, JuliaSyntax.POSTFIX_OP_FLAG)
write(io, "postfix-op,")
end
end
if kind(node) in KSet"string cmdstring" &&
JuliaSyntax.has_flags(node, JuliaSyntax.TRIPLE_STRING_FLAG)
write(io, "triple,")
end
if kind(node) in KSet"string cmdstring Identifier" &&
JuliaSyntax.has_flags(node, JuliaSyntax.RAW_STRING_FLAG)
write(io, "raw,")
end
if kind(node) in KSet"tuple block macrocall" &&
JuliaSyntax.has_flags(node, JuliaSyntax.PARENS_FLAG)
write(io, "parens,")
end
if kind(node) === K"quote" && JuliaSyntax.has_flags(node, JuliaSyntax.COLON_QUOTE)
write(io, "colon,")
end
if kind(node) === K"toplevel" && JuliaSyntax.has_flags(node, JuliaSyntax.TOPLEVEL_SEMICOLONS_FLAG)
write(io, "semicolons,")
end
if kind(node) === K"struct" && JuliaSyntax.has_flags(node, JuliaSyntax.MUTABLE_FLAG)
write(io, "mutable,")
end
if kind(node) === K"module" && JuliaSyntax.has_flags(node, JuliaSyntax.BARE_MODULE_FLAG)
write(io, "baremodule,")
end
truncate(io, max(0, position(io) - 1)) # Remove trailing comma
return String(take!(io))
end
# The parser is somewhat inconsistent(?) with where e.g. whitespace nodes end up so in order
# to simplify the formatting code we normalize some things.
function normalize_tree!(node)
is_leaf(node) && return
kids = verified_kids(node)
# Move standalone K"NewlineWs" (and other??) nodes from between the var block and the
# body block in K"let" nodes.
# Note that this happens before the whitespace into block normalization below because
# for let we want to move it to the subsequent block instead.
if kind(node) === K"let"
varsidx = findfirst(x -> kind(x) === K"block", kids)::Int
bodyidx = findnext(x -> kind(x) === K"block", kids, varsidx + 1)::Int
r = (varsidx + 1):(bodyidx - 1)
if length(r) > 0
items = kids[r]
deleteat!(kids, r)
bodyidx -= length(r)
body = kids[bodyidx]
prepend!(verified_kids(body), items)
kids[bodyidx] = make_node(body, verified_kids(body))
end
end
# Normalize K"Whitespace" nodes in blocks. For example in `if x y end` the space will be
# outside the block just before the K"end" node, but in `if x\ny\nend` the K"NewlineWs"
# will end up inside the block.
if kind(node) in KSet"function if elseif for while try do macro module baremodule let struct module"
blockidx = findfirst(x -> kind(x) === K"block", kids)
while blockidx !== nothing && blockidx < length(kids)
if kind(kids[blockidx + 1]) !== K"Whitespace"
# TODO: This repeats the computation below...
blockidx = findnext(x -> kind(x) === K"block", kids, blockidx + 1)
continue
end
# Pop the ws and push it into the block instead
block = kids[blockidx]
blockkids = verified_kids(block)
@assert !(kind(blockkids[end]) in KSet"Whitespace NewlineWs")
push!(blockkids, popat!(kids, blockidx + 1))
# Remake the block to recompute the span
kids[blockidx] = make_node(block, blockkids)
# Find next block
blockidx = findnext(x -> kind(x) === K"block", kids, blockidx + 1)
end
end
# Normalize K"Whitespace" nodes in if-elseif-else chains where the node needs to move
# many steps into the last else block...
if kind(node) === K"if"
elseifidx = findfirst(x -> kind(x) === K"elseif", kids)
if elseifidx !== nothing
endidx = findnext(x -> kind(x) === K"end", kids, elseifidx + 1)::Int
if elseifidx + 2 == endidx && kind(kids[elseifidx + 1]) === K"Whitespace"
# Pop the ws and push it into the last block instead
ws = popat!(kids, elseifidx + 1)
elseifnode = insert_into_last_else_block(kids[elseifidx], ws)
@assert elseifnode !== nothing
kids[elseifidx] = elseifnode
end
end
end
# Normalize K"Whitespace" nodes in try-catch-finally-else
if kind(node) === K"try"
catchidx = findfirst(x -> kind(x) in KSet"catch finally else", kids)
while catchidx !== nothing
if kind(kids[catchidx + 1]) === K"Whitespace"
ws = popat!(kids, catchidx + 1)
catchnode = insert_into_last_catchlike_block(kids[catchidx], ws)
@assert catchnode !== nothing
kids[catchidx] = catchnode
end
catchidx = findnext(x -> kind(x) in KSet"catch finally else", kids, catchidx + 1)
end
end
# Normalize K"NewlineWs" nodes in empty do-blocks
if kind(node) === K"do"
tupleidx = findfirst(x -> kind(x) === K"tuple", kids)::Int
blockidx = findnext(x -> kind(x) === K"block", kids, tupleidx + 1)::Int
@assert tupleidx + 1 == blockidx
tuple = kids[tupleidx]
tuplekids = verified_kids(tuple)
if kind(tuplekids[end]) === K"NewlineWs"
# If the tuple ends with a K"NewlineWs" node we move it into the block
block = kids[blockidx]
blockkids = verified_kids(block)
if length(blockkids) > 0
@assert kind(blockkids[1]) !== K"Whitespace"
end
pushfirst!(blockkids, pop!(tuplekids))
# Remake the nodes to recompute the spans
kids[tupleidx] = make_node(tuple, tuplekids)
kids[blockidx] = make_node(block, blockkids)
end
end
@assert kids === verified_kids(node)
for kid in kids
ksp = span(kid)
normalize_tree!(kid)
@assert span(kid) == ksp
end
# We only move around things inside this node so the span should be unchanged
@assert span(node) == mapreduce(span, +, kids; init = 0)
return node
end
function insert_into_last_else_block(node, ws)
@assert kind(node) === K"elseif"
kids = verified_kids(node)
elseifidx = findfirst(x -> !is_leaf(x) && kind(x) === K"elseif", kids)
if elseifidx !== nothing
@assert elseifidx == lastindex(kids)
elseifnode′ = insert_into_last_else_block(kids[elseifidx], ws)
@assert elseifnode′ !== nothing
kids[elseifidx] = elseifnode′
return make_node(node, kids)
end
# Find the else block
elseifblockidx = findfirst(x -> kind(x) === K"block", kids)::Int
elseleafidx = findnext(x -> kind(x) === K"else", kids, elseifblockidx + 1)::Int
elseblockidx = findnext(x -> kind(x) === K"block", kids, elseleafidx + 1)::Int
@assert elseblockidx == lastindex(kids)
elseblock = kids[elseblockidx]
# Insert the node
elseblockkids = verified_kids(elseblock)
@assert !(kind(elseblockkids[end]) in KSet"NewlineWs Whitespace")
push!(elseblockkids, ws)
# Remake the else block
kids[elseblockidx] = make_node(elseblock, elseblockkids)
# Remake and return the elseif node
return make_node(node, kids)
end
function insert_into_last_catchlike_block(node, ws)
@assert kind(node) in KSet"catch finally else"
kids = verified_kids(node)
catchblockidx = findfirst(x -> kind(x) === K"block", kids)::Int
@assert catchblockidx == lastindex(kids)
catchblock = kids[catchblockidx]
catchblockkids = verified_kids(catchblock)
@assert !(kind(catchblockkids[end]) in KSet"NewlineWs Whitespace")
push!(catchblockkids, ws)
# Remake the catch block
kids[catchblockidx] = make_node(catchblock, catchblockkids)
# Remake and return the catch node
return make_node(node, kids)
end
# Node tags #
# This node is responsible for incrementing the indentation level
const TAG_INDENT = TagType(1) << 0
# This node is responsible for decrementing the indentation level
const TAG_DEDENT = TagType(1) << 1
# This (NewlineWs) node is the last one before a TAG_DEDENT
const TAG_PRE_DEDENT = TagType(1) << 2
# This (NewlineWs) node is a line continuation
const TAG_LINE_CONT = UInt32(1) << 31
# Parameters that should have a trailing comma after last item
const TAG_TRAILING_COMMA = TagType(1) << 4
# Parameters that should optinally have a trailing comma after last item
const TAG_TRAILING_COMMA_OPT = TagType(1) << 5
function add_tag(node::Node, tag::TagType)
return Node(head(node), span(node), node.kids, node.tags | tag)
end
# Tags all leading NewlineWs nodes as continuation nodes. Note that comments are skipped
# over so that cases like `\n#comment\ncode` works as expected.
function continue_newlines(node::Node; leading::Bool = true, trailing::Bool = true)
if is_leaf(node)
if kind(node) === K"NewlineWs" && !has_tag(node, TAG_LINE_CONT)
return add_tag(node, TAG_LINE_CONT)
else
return nothing
end
end
kids = verified_kids(node)
if length(kids) == 1
return nothing
end
any_kid_changed = false
if leading
idx = firstindex(kids) - 1
while true
# Skip over whitespace + comments which can mask the newlines
idx = findnext(x -> !(kind(x) in KSet"Whitespace Comment"), kids, idx + 1)
if idx === nothing
# No matching kid found
break
elseif kind(kids[idx]) === K"NewlineWs"
# Kid is a NewlineWs node, tag and keep looking
kid′ = continue_newlines(kids[idx]; leading = leading, trailing = trailing)
if kid′ !== nothing
kids[idx] = kid′
any_kid_changed = false
end
else
# This kid is not Whitespace, Comment or NewlineWs.
# Recurse but break out of the loop
kid′ = continue_newlines(kids[idx]; leading = leading, trailing = trailing)
if kid′ !== nothing
kids[idx] = kid′
any_kid_changed = false
end
break
end
end
end
if trailing
idx = lastindex(kids) + 1
while true
# Skip over whitespace + comments which can mask the newlines
idx = findprev(x -> !(kind(x) in KSet"Whitespace Comment"), kids, idx - 1)
if idx === nothing
# No matching kid found
break
elseif kind(kids[idx]) === K"NewlineWs"
# Kid is a NewlineWs node, tag and keep looking
kid′ = continue_newlines(kids[idx]; leading = leading, trailing = trailing)
if kid′ !== nothing
kids[idx] = kid′
any_kid_changed = false
end
else
# This kid is not Whitespace, Comment or NewlineWs.
# Recurse but break out of the loop
kid′ = continue_newlines(kids[idx]; leading = leading, trailing = trailing)
if kid′ !== nothing
kids[idx] = kid′
any_kid_changed = false
end
break
end
end
end
return any_kid_changed ? node : nothing
end
function has_tag(node::Node, tag::TagType)
return node.tags & tag != 0
end
function stringify_tags(node::Node)
io = IOBuffer()
if has_tag(node, TAG_INDENT)
write(io, "indent,")
end
if has_tag(node, TAG_DEDENT)
write(io, "dedent,")
end
if has_tag(node, TAG_PRE_DEDENT)
write(io, "pre-dedent,")
end
if has_tag(node, TAG_LINE_CONT)
write(io, "line-cont.,")
end
if has_tag(node, TAG_TRAILING_COMMA)
write(io, "trail-comma.,")
end
truncate(io, max(0, position(io) - 1)) # Remove trailing comma
return String(take!(io))
end
# Create a new node with the same head but new kids
function make_node(node::Node, kids′::Vector{Node}, tags = node.tags)
span′ = mapreduce(span, +, kids′; init = 0)
return Node(head(node), span′, kids′, tags)
end
function make_node(node::Node, span′::Integer, tags = node.tags)
@assert is_leaf(node)
return Node(head(node), span′, (), tags)
end
# TODO: Remove?
first_leaf(node::Node) = nth_leaf(node, 1)
function nth_leaf(node::Node, nth::Int)
leaf, n_seen = nth_leaf(node, nth, 0)
return n_seen == nth ? leaf : nothing
end
function nth_leaf(node::Node, nth::Int, n_seen::Int)
if is_leaf(node)
return node, n_seen + 1
else
kids = verified_kids(node)
for kid in kids
leaf, n_seen = nth_leaf(kid, nth, n_seen)
if n_seen == nth
return leaf, n_seen
end
end
return nothing, n_seen
end
end
function second_leaf(node::Node)
return nth_leaf(node, 2)
end
# Return number of non-whitespace kids, basically the length the equivalent
# (expr::Expr).args
function meta_nargs(node::Node)
return is_leaf(node) ? 0 : count(!JuliaSyntax.is_whitespace, verified_kids(node))
end
# Replace the first leaf
# TODO: Append the replacement bytes inside this utility function?
function replace_first_leaf(node::Node, kid′::Union{Node, NullNode})
if is_leaf(node)
return kid′
else
kids′ = copy(verified_kids(node))
kid′′ = replace_first_leaf(kids′[1], kid′)
if kid′′ === nullnode
popfirst!(kids′)
else
kids′[1] = kid′′
end
# kids′[1] = replace_first_leaf(kids′[1], kid′)
@assert length(kids′) > 0
return make_node(node, kids′)
end
end
function replace_last_leaf(node::Node, kid′::Union{Node, NullNode})
if is_leaf(node)
return kid′
else
kids′ = copy(verified_kids(node))
kid′′ = replace_last_leaf(kids′[end], kid′)
if kid′′ === nullnode
pop!(kids′)
else
kids′[end] = kid′′
end
@assert length(kids′) > 0
return make_node(node, kids′)
end
end
# Insert a node before the first leaf (at the same level)
# TODO: Currently only works for inserting a space before a comment
function add_before_first_leaf(node::Node, kid′::Union{Node, NullNode})
@assert !is_leaf(node)
kids = verified_kids(node)
@assert length(kids) > 0
kids′ = copy(kids)
if kind(kids′[1]) === K"Comment"
pushfirst!(kids′, kid′)
else
kids′[1] = add_before_first_leaf(kids′[1], kid′)
end
return make_node(node, kids′)
end
function last_leaf(node::Node)
if is_leaf(node)
return node
else
kids = verified_kids(node)
if length(kids) == 0
return nothing
else
return last_leaf(last(kids))
end
end
end
function second_last_leaf(node::Node)
node, n = second_last_leaf(node, 0)
return n == 2 ? node : nothing
end
function second_last_leaf(node::Node, n_seen::Int)
if is_leaf(node)
return node, n_seen + 1
else
kids = verified_kids(node)
for i in reverse(1:length(kids))
kid, n_seen = second_last_leaf(kids[i], n_seen)
if n_seen == 2
return kid, n_seen
end
end
end
return nothing, n_seen
end
function has_newline_after_non_whitespace(node::Node)
if is_leaf(node)
@assert kind(node) !== K"NewlineWs"
return false
else
kids = verified_kids(node)
idx = findlast(!JuliaSyntax.is_whitespace, kids)
if idx === nothing
@assert false
# Everything is whitespace...
return any(x -> kind(x) === K"NewlineWs", kids)
end
return any(x -> kind(x) === K"NewlineWs", kids[(idx + 1):end]) ||
has_newline_after_non_whitespace(kids[idx])
# if idx === nothing
# # All is whitespace, check if any of the kids is a newline
# return any(x -> kind(x) === K"NewlineWs", kids)
# end
end
end
function is_assignment(node::Node)
return JuliaSyntax.is_prec_assignment(node)
end
# Assignment node but exclude loop variable assignment
function is_variable_assignment(ctx, node::Node)
return !is_leaf(node) && is_assignment(node) &&
!(ctx.lineage_kinds[end] in KSet"for generator cartesian_iterator filter")
end
# K"global" and K"local" nodes can be either `global a, b, c` or `global a = 1`. This method
# checks whether the node is of the former kind.
function is_global_local_list(node)
if !(kind(node) in KSet"global local" && !is_leaf(node))
return false
end
kids = verified_kids(node)
# If it contains assignments it is not a list
if any(x -> is_assignment(x), kids)
return false
end
# If it contains K"," it is a list
if any(x -> kind(x) === K",", kids)
return true
end
# If we reach here we have a single item list (`local a`) or something like
# ```
# global function f()
# # ...
# end
# ```
# For now we only say it is a list if the item is in the subset below
idx = findfirst(x -> kind(x) in KSet"global local", kids)::Int
idx = findnext(x -> !JuliaSyntax.is_whitespace(x), kids, idx + 1)::Int
return kind(kids[idx]) in KSet"Identifier var"
end
function unwrap_to_call_or_tuple(x)
is_leaf(x) && return nothing
@assert !is_leaf(x)
if kind(x) in KSet"call tuple parens"
return x
end
xkids = verified_kids(x)
xi = findfirst(x -> !JuliaSyntax.is_whitespace(x), xkids)::Int
return unwrap_to_call_or_tuple(xkids[xi])
end
# TODO: This should be reworked to be more specific, in particular K"parens" is maybe not
# correct (found in e.g. `function(x * b)\n\nend`).
function is_longform_anon_function(node::Node)
is_leaf(node) && return false
kind(node) === K"function" || return false
kids = verified_kids(node)
kw = findfirst(x -> kind(x) === K"function", kids)
@assert kw !== nothing
sig = findnext(x -> !JuliaSyntax.is_whitespace(x), kids, kw + 1)::Int
sigkid = kids[sig]
maybe_tuple = unwrap_to_call_or_tuple(sigkid)
if maybe_tuple === nothing
return false
else
return kind(maybe_tuple) in KSet"tuple parens"
end
end
function is_longform_functor(node::Node)
is_leaf(node) && return false
kind(node) === K"function" || return false
kids = verified_kids(node)
kw = findfirst(x -> kind(x) === K"function", kids)
@assert kw !== nothing
calli = findnext(x -> !JuliaSyntax.is_whitespace(x), kids, kw + 1)::Int
call = kids[calli]
if !is_leaf(call) && kind(call) == K"call" &&
kind(first(verified_kids(call))) === K"parens"
return true
end
return false
end
# Just like `JuliaSyntax.is_infix_op_call`, but also check that the node is K"call" or
# K"dotcall"
function is_infix_op_call(node::Node)
return kind(node) in KSet"call dotcall" && JuliaSyntax.is_infix_op_call(node)
end
# Extract the operator of an infix op call node
function infix_op_call_op(node::Node)
@assert is_infix_op_call(node) || kind(node) === K"||"
kids = verified_kids(node)
first_operand_index = findfirst(!JuliaSyntax.is_whitespace, kids)::Int
op_index = findnext(JuliaSyntax.is_operator, kids, first_operand_index + 1)::Int
return kids[op_index]
end
# Comparison leaf or a dotted comparison leaf (.<)
function is_comparison_leaf(node::Node)
if is_leaf(node) && JuliaSyntax.is_prec_comparison(node)
return true
elseif !is_leaf(node) && kind(node) === K"." &&
meta_nargs(node) == 2 && is_comparison_leaf(verified_kids(node)[2])
return true
else
return false
end
end
function is_operator_leaf(node::Node)
return is_leaf(node) && JuliaSyntax.is_operator(node)
end
function first_non_whitespace_kid(node::Node)
@assert !is_leaf(node)
kids = verified_kids(node)
idx = findfirst(!JuliaSyntax.is_whitespace, kids)::Int
return kids[idx]
end
function is_begin_block(node::Node)
return kind(node) === K"block" && length(verified_kids(node)) > 0 &&
kind(verified_kids(node)[1]) === K"begin"
end
function is_paren_block(node::Node)
return kind(node) === K"block" && JuliaSyntax.has_flags(node, JuliaSyntax.PARENS_FLAG)
end
function first_leaf_predicate(node::Node, pred::F) where {F}
if is_leaf(node)
return pred(node) ? node : nothing
else
kids = verified_kids(node)
for k in kids
r = first_leaf_predicate(k, pred)
if r !== nothing
return r
end
end
return nothing
end
end
function last_leaf_predicate(node::Node, pred::F) where {F}
if is_leaf(node)
return pred(node) ? node : nothing
else
kids = verified_kids(node)
for k in Iterators.reverse(kids)
r = first_leaf_predicate(k, pred)
if r !== nothing
return r
end
end
return nothing
end
end
function predicate_contains(pred::F, node::Node) where {F}
if pred(node)::Bool
return true
elseif is_leaf(node)
return false
else
for k in verified_kids(node)
r = predicate_contains(pred, k)
r && return r
end
return false
end
end
function contains_outer_newline(kids::Vector{Node}, oidx::Int, cidx::Int; recurse = true)
pred = x -> kind(x) === K"NewlineWs" || !JuliaSyntax.is_whitespace(x)
for i in (oidx + 1):(cidx - 1)
kid = kids[i]
r = first_leaf_predicate(kid, pred)
if r !== nothing && kind(r) === K"NewlineWs"
return true
end
r = last_leaf_predicate(kid, pred)
if r !== nothing && kind(r) === K"NewlineWs"
return true
end
if kind(kid) === K"parameters"
grandkids = verified_kids(kid)
semiidx = findfirst(x -> kind(x) === K";", grandkids)::Int
r = contains_outer_newline(verified_kids(kid), semiidx, length(grandkids) + 1)
if r === true # r can be nothing so `=== true` is intentional
return true
end
end
end
return false
end
function any_leaf(pred::F, node::Node) where {F}
if is_leaf(node)
return pred(node)::Bool
else
kids = verified_kids(node)
for k in kids
any_leaf(pred, k) && return true
end
return false
end
end
# TODO: Alternative non-recursive definition that only looks at the current layer
# ```
# contains_outer_newline(kids, opening_leaf_idx, closing_leaf_idx)
# ```
function is_multiline_between_idxs(ctx, node::Node, opening_idx::Int, closing_idx::Int)
@assert !is_leaf(node)
kids = verified_kids(node)
# Check for newline nodes
if any(y -> any_leaf(x -> kind(x) === K"NewlineWs", kids[y]), (opening_idx + 1):(closing_idx - 1))
return true
end
# Recurse into multiline triple-strings
pos = position(ctx.fmt_io)
for i in 1:opening_idx
accept_node!(ctx, kids[i])
end
for i in (opening_idx + 1):(closing_idx - 1)
kid = kids[i]
ipos = position(ctx.fmt_io)
if contains_multiline_triple_string(ctx, kid)
seek(ctx.fmt_io, pos)
return true
end
seek(ctx.fmt_io, ipos)
accept_node!(ctx, kid)
end
seek(ctx.fmt_io, pos)
return false
end
function contains_multiline_triple_string(ctx, node::Node)
# If this is a leaf just advance the stream
if is_leaf(node)
accept_node!(ctx, node)
return false
end
kids = verified_kids(node)
pos = position(ctx.fmt_io)
# If this is a triple string we inspect it
if kind(node) in KSet"string cmdstring" && JuliaSyntax.has_flags(node, JuliaSyntax.TRIPLE_STRING_FLAG)
triplekind, triplestring, itemkind = kind(node) === K"string" ?
(K"\"\"\"", "\"\"\"", K"String") : (K"```", "```", K"CmdString")
# Look for K"String"s ending in `\n`
for (i, kid) in pairs(kids)
if i === firstindex(kids) || i === lastindex(kids)
@assert kind(kid) === triplekind
@assert String(read_bytes(ctx, kid)) == triplestring
end
if kind(kid) === itemkind
if endswith(String(read_bytes(ctx, kid)), "\n")
return true
end
end
accept_node!(ctx, kid)
end
@assert position(ctx.fmt_io) == pos + span(node)
else
for kid in kids
kpos = position(ctx.fmt_io)
if contains_multiline_triple_string(ctx, kid)
return true
end
seek(ctx.fmt_io, kpos)
accept_node!(ctx, kid)
end
end
return false
end
function is_string_macro(node)
kind(node) === K"macrocall" || return false
@assert !is_leaf(node)
kids = verified_kids(node)
return length(kids) >= 2 &&
kind(kids[1]) in KSet"StringMacroName CmdMacroName core_@cmd" &&
kind(kids[2]) in KSet"string cmdstring"
end
function is_triple_string(node)
return kind(node) in KSet"string cmdstring" &&
JuliaSyntax.has_flags(node, JuliaSyntax.TRIPLE_STRING_FLAG)
end
function is_triple_string_macro(node)
if kind(node) === K"macrocall"
kids = verified_kids(node)
if length(kids) >= 2 &&
kind(kids[1]) in KSet"StringMacroName CmdMacroName core_@cmd" &&
is_triple_string(kids[2])
return true
end
end
return false
end
function is_triple_thing(node)
return is_triple_string(node) || is_triple_string_macro(node) ||
(kind(node) === K"juxtapose" && is_triple_string_macro(verified_kids(node)[1]))
end
# Check whether the sequence of kinds in `kinds` exist in `kids` starting at index `i`.
function kmatch(kids, kinds, i = firstindex(kids))
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
# Extract the macro name as written in the source.
function macrocall_name(ctx, node)
@assert kind(node) === K"macrocall"
kids = verified_kids(node)
pred = x -> kind(x) in KSet"MacroName StringMacroName CmdMacroName core_@cmd"
mkind = kind(first_leaf_predicate(node, pred)::Node)
if kmatch(kids, KSet"@ MacroName")
p = position(ctx.fmt_io)
bytes = read(ctx.fmt_io, span(kids[1]) + span(kids[2]))
seek(ctx.fmt_io, p)
return String(bytes)
elseif kmatch(kids, KSet".") || kmatch(kids, KSet"CmdMacroName") ||
kmatch(kids, KSet"StringMacroName")
bytes = read_bytes(ctx, kids[1])
if mkind === K"CmdMacroName"
append!(bytes, "_cmd")
elseif mkind === K"StringMacroName"
append!(bytes, "_str")
end
return String(bytes)
elseif kmatch(kids, KSet"core_@cmd")
bytes = read_bytes(ctx, kids[1])
@assert length(bytes) == 0
return "core_@cmd"
else
# Don't bother looking in more complex expressions, just return unknown
return "<unknown macro>"
end
end
# Inserting `return` modifies the AST in a way that is visible to macros.. In general it is
# never safe to change the AST inside a macro, but we make an exception for some common
# "known" macros in order to be able to format functions that e.g. have an `@inline`
# annotation in front.
const MACROS_SAFE_TO_INSERT_RETURN = let set = Set{String}()
for m in (
"inline", "noinline", "propagate_inbounds", "generated", "eval",
"assume_effects", "doc",
)
push!(set, "@$m", "Base.@$m")
end
push!(set, "Core.@doc")
set
end
function safe_to_insert_return(ctx, node)
for m in ctx.lineage_macros
m in MACROS_SAFE_TO_INSERT_RETURN || return false
end
return true
end
##########################
# Utilities for IOBuffer #
##########################
# Replace bytes for a node at the current position in the IOBuffer. `size` is the current
# window for the node, i.e. the number of bytes until the next node starts. If `size` is
# smaller or larger than the length of `bytes` this method will shift the bytes for
# remaining nodes to the left or right. Return number of written bytes.
function replace_bytes!(io::IOBuffer, bytes::Union{String, AbstractVector{UInt8}}, size::Int)
pos = position(io)
nb = (bytes isa AbstractVector{UInt8} ? length(bytes) : sizeof(bytes))
if nb == size
nw = write(io, bytes)
@assert nb == nw
else
backup = IOBuffer() # TODO: global const (with lock)?
seek(io, pos + size)
@assert position(io) == pos + size
nb_written_to_backup = write(backup, io)
seek(io, pos)
@assert position(io) == pos
nw = write(io, bytes)
@assert nb == nw
nb_read_from_backup = write(io, seekstart(backup))
@assert nb_written_to_backup == nb_read_from_backup
truncate(io, position(io))
end
seek(io, pos)
@assert position(io) == pos
return nb
end
replace_bytes!(io::IOBuffer, bytes::Union{String, AbstractVector{UInt8}}, size::Integer) =
replace_bytes!(io, bytes, Int(size))