mirror of https://github.com/fredrikekre/Runic.jl
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.
880 lines
30 KiB
880 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) |
|
return kind(node) in KSet"global local" && !is_leaf(node) && |
|
all(x -> kind(x) in KSet"global local Identifier , Whitespace NewlineWs Comment", verified_kids(node)) |
|
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))
|
|
|