mirror of https://github.com/fredrikekre/Runic.jl
5 changed files with 470 additions and 0 deletions
@ -0,0 +1,22 @@ |
|||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2024 Fredrik Ekre and Runic.jl contributors |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
|
|
||||||
@ -0,0 +1,9 @@ |
|||||||
|
name = "Runic" |
||||||
|
uuid = "62bfec6d-59d7-401d-8490-b29ee721c001" |
||||||
|
version = "1.0.0" |
||||||
|
|
||||||
|
[deps] |
||||||
|
JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4" |
||||||
|
|
||||||
|
[compat] |
||||||
|
JuliaSyntax = "0.4.8" |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
# Runic.jl |
||||||
|
|
||||||
|
*A code formatter with rules set in stone.* |
||||||
@ -0,0 +1,305 @@ |
|||||||
|
module Runic |
||||||
|
|
||||||
|
using JuliaSyntax: |
||||||
|
JuliaSyntax, @K_str, @KSet_str |
||||||
|
|
||||||
|
mutable struct Context |
||||||
|
const io::IO |
||||||
|
const src::String |
||||||
|
indent_level::Int |
||||||
|
offset::Int |
||||||
|
node::JuliaSyntax.GreenNode |
||||||
|
parent::Union{JuliaSyntax.GreenNode, Nothing} |
||||||
|
end |
||||||
|
|
||||||
|
function Context(src)::Union{Tuple{Nothing, Exception}, Tuple{Context, Nothing}} |
||||||
|
root = try |
||||||
|
JuliaSyntax.parseall(JuliaSyntax.GreenNode, src; ignore_warnings=true) |
||||||
|
catch e |
||||||
|
return nothing, e |
||||||
|
end |
||||||
|
return Context(IOBuffer(), src, 0, 0, root, nothing), nothing |
||||||
|
end |
||||||
|
|
||||||
|
# Emit the node like in the original source code. |
||||||
|
function emit!(ctx::Context)::Union{Nothing, Exception} |
||||||
|
node = ctx.node |
||||||
|
# Should never emit nodes with children |
||||||
|
@assert !JuliaSyntax.haschildren(node) |
||||||
|
# First index of the current node, assumed to be valid |
||||||
|
i = ctx.offset + 1 |
||||||
|
@assert isvalid(ctx.src, i) |
||||||
|
# Last index of the current node computed as the previous valid index from the first index of the next node |
||||||
|
j = prevind(ctx.src, ctx.offset + JuliaSyntax.span(node) + 1) |
||||||
|
@assert isvalid(ctx.src, j) |
||||||
|
# String representation of this node |
||||||
|
str = @view ctx.src[i:j] |
||||||
|
@debug "Emitting ..." JuliaSyntax.kind(node) str |
||||||
|
return emit!(ctx, str) |
||||||
|
end |
||||||
|
|
||||||
|
# Emit the node with a replacement string. |
||||||
|
function emit!(ctx::Context, str::AbstractString) |
||||||
|
write(ctx.io, str) |
||||||
|
ctx.offset += JuliaSyntax.span(ctx.node) |
||||||
|
return |
||||||
|
end |
||||||
|
|
||||||
|
function recurse!(ctx)::Union{Nothing, Exception} |
||||||
|
# Stash the family |
||||||
|
grandparent = ctx.parent |
||||||
|
ctx.parent = ctx.node |
||||||
|
for child in JuliaSyntax.children(ctx.node) |
||||||
|
ctx.node = child |
||||||
|
if (err = format_context!(ctx); err !== nothing) |
||||||
|
return err |
||||||
|
end |
||||||
|
end |
||||||
|
# Restore the family |
||||||
|
ctx.node = ctx.parent |
||||||
|
ctx.parent = grandparent |
||||||
|
return nothing |
||||||
|
end |
||||||
|
|
||||||
|
function format_context!(ctx::Context)::Union{Nothing, Exception} |
||||||
|
node = ctx.node |
||||||
|
node_kind = JuliaSyntax.kind(node) |
||||||
|
|
||||||
|
# Nodes that always recurse! |
||||||
|
if ( |
||||||
|
node_kind === K"block" || |
||||||
|
node_kind === K"braces" || |
||||||
|
node_kind === K"bracescat" || # {a; b} |
||||||
|
node_kind === K"call" || |
||||||
|
node_kind === K"cartesian_iterator" || |
||||||
|
node_kind === K"char" || |
||||||
|
node_kind === K"cmdstring" || |
||||||
|
node_kind === K"comparison" || |
||||||
|
node_kind === K"comprehension" || |
||||||
|
node_kind === K"core_@cmd" || |
||||||
|
node_kind === K"curly" || |
||||||
|
node_kind === K"dotcall" || |
||||||
|
node_kind === K"filter" || |
||||||
|
node_kind === K"generator" || |
||||||
|
node_kind === K"hcat" || |
||||||
|
node_kind === K"importpath" || |
||||||
|
node_kind === K"inert" || |
||||||
|
node_kind === K"juxtapose" || |
||||||
|
node_kind === K"macrocall" || |
||||||
|
node_kind === K"ncat" || |
||||||
|
node_kind === K"nrow" || |
||||||
|
node_kind === K"parens" || |
||||||
|
node_kind === K"ref" || |
||||||
|
node_kind === K"row" || |
||||||
|
node_kind === K"string" || |
||||||
|
node_kind === K"toplevel" || |
||||||
|
node_kind === K"typed_comprehension" || |
||||||
|
node_kind === K"typed_hcat" || |
||||||
|
node_kind === K"typed_ncat" || |
||||||
|
node_kind === K"typed_vcat" || |
||||||
|
node_kind === K"vcat" || |
||||||
|
node_kind === K"vect" |
||||||
|
) |
||||||
|
@debug "Recursing always" node_kind |
||||||
|
@assert !JuliaSyntax.is_trivia(node) |
||||||
|
if (err = recurse!(ctx); err !== nothing) |
||||||
|
return err |
||||||
|
end |
||||||
|
|
||||||
|
# Nodes that recurse! if not trivia |
||||||
|
elseif !JuliaSyntax.is_trivia(node) && ( |
||||||
|
node_kind === K"abstract" || |
||||||
|
node_kind === K"as" || |
||||||
|
node_kind === K"break" || |
||||||
|
node_kind === K"catch" || |
||||||
|
node_kind === K"const" || |
||||||
|
node_kind === K"continue" || |
||||||
|
node_kind === K"do" || |
||||||
|
node_kind === K"doc" || |
||||||
|
node_kind === K"elseif" || |
||||||
|
node_kind === K"export" || |
||||||
|
node_kind === K"finally" || |
||||||
|
node_kind === K"for" || |
||||||
|
node_kind === K"function" || |
||||||
|
node_kind === K"global" || |
||||||
|
node_kind === K"if" || |
||||||
|
node_kind === K"import" || |
||||||
|
node_kind === K"let" || |
||||||
|
node_kind === K"local" || |
||||||
|
node_kind === K"macro" || |
||||||
|
node_kind === K"module" || |
||||||
|
node_kind === K"outer" || |
||||||
|
node_kind === K"parameters" || |
||||||
|
node_kind === K"primitive" || |
||||||
|
node_kind === K"quote" || |
||||||
|
node_kind === K"return" || |
||||||
|
node_kind === K"struct" || |
||||||
|
node_kind === K"try" || |
||||||
|
node_kind === K"tuple" || |
||||||
|
node_kind === K"using" || |
||||||
|
node_kind === K"var" || |
||||||
|
node_kind === K"where" || |
||||||
|
node_kind === K"while" |
||||||
|
) |
||||||
|
@debug "Recursing if not trivia" node_kind |
||||||
|
if (err = recurse!(ctx); err !== nothing) |
||||||
|
return err |
||||||
|
end |
||||||
|
|
||||||
|
# Nodes that should recurse if they have children (all??) |
||||||
|
elseif JuliaSyntax.haschildren(node) && ( |
||||||
|
JuliaSyntax.is_operator(node) || |
||||||
|
node_kind === K"else" # try-(catch|finally)-else |
||||||
|
) |
||||||
|
@debug "Recursing because children" node_kind |
||||||
|
if (err = recurse!(ctx); err !== nothing) |
||||||
|
return err |
||||||
|
end |
||||||
|
|
||||||
|
# Whitespace and comments emitted verbatim for now |
||||||
|
elseif node_kind === K"Whitespace" || |
||||||
|
node_kind === K"NewlineWs" || |
||||||
|
node_kind === K"Comment" |
||||||
|
@debug "emit ws" node_kind |
||||||
|
if (err = emit!(ctx); err !== nothing) |
||||||
|
return err |
||||||
|
end |
||||||
|
|
||||||
|
# Nodes that always emit like the source code |
||||||
|
elseif ( |
||||||
|
node_kind === K"(" || |
||||||
|
node_kind === K")" || |
||||||
|
node_kind === K"," || |
||||||
|
node_kind === K"::" || |
||||||
|
node_kind === K";" || |
||||||
|
node_kind === K"<:" || |
||||||
|
node_kind === K"@" || |
||||||
|
node_kind === K"BinInt" || |
||||||
|
node_kind === K"Char" || |
||||||
|
node_kind === K"CmdMacroName" || |
||||||
|
node_kind === K"CmdString" || |
||||||
|
node_kind === K"Float" || |
||||||
|
node_kind === K"Float32" || |
||||||
|
node_kind === K"HexInt" || |
||||||
|
node_kind === K"Identifier" || |
||||||
|
node_kind === K"Integer" || |
||||||
|
node_kind === K"MacroName" || |
||||||
|
node_kind === K"OctInt" || |
||||||
|
node_kind === K"String" || |
||||||
|
node_kind === K"StringMacroName" || |
||||||
|
node_kind === K"false" || |
||||||
|
node_kind === K"true" || |
||||||
|
node_kind === K"type" || |
||||||
|
JuliaSyntax.is_operator(node) || |
||||||
|
JuliaSyntax.is_trivia(node) && ( |
||||||
|
node_kind === K"$" || |
||||||
|
node_kind === K"=" || |
||||||
|
node_kind === K"[" || |
||||||
|
node_kind === K"\"" || |
||||||
|
node_kind === K"\"\"\"" || |
||||||
|
node_kind === K"]" || |
||||||
|
node_kind === K"`" || |
||||||
|
node_kind === K"```" || |
||||||
|
node_kind === K"abstract" || |
||||||
|
node_kind === K"as" || |
||||||
|
node_kind === K"baremodule" || |
||||||
|
node_kind === K"begin" || |
||||||
|
node_kind === K"break" || |
||||||
|
node_kind === K"catch" || |
||||||
|
node_kind === K"const" || |
||||||
|
node_kind === K"continue" || |
||||||
|
node_kind === K"do" || |
||||||
|
node_kind === K"else" || |
||||||
|
node_kind === K"elseif" || |
||||||
|
node_kind === K"end" || |
||||||
|
node_kind === K"export" || |
||||||
|
node_kind === K"finally" || |
||||||
|
node_kind === K"for" || |
||||||
|
node_kind === K"function" || |
||||||
|
node_kind === K"global" || |
||||||
|
node_kind === K"if" || |
||||||
|
node_kind === K"import" || |
||||||
|
node_kind === K"in" || |
||||||
|
node_kind === K"let" || |
||||||
|
node_kind === K"local" || |
||||||
|
node_kind === K"macro" || |
||||||
|
node_kind === K"module" || |
||||||
|
node_kind === K"mutable" || |
||||||
|
node_kind === K"outer" || |
||||||
|
node_kind === K"primitive" || |
||||||
|
node_kind === K"quote" || |
||||||
|
node_kind === K"return" || |
||||||
|
node_kind === K"struct" || |
||||||
|
node_kind === K"try" || |
||||||
|
node_kind === K"using" || |
||||||
|
node_kind === K"var" || |
||||||
|
node_kind === K"while" || |
||||||
|
node_kind === K"{" || |
||||||
|
node_kind === K"}" |
||||||
|
) |
||||||
|
) |
||||||
|
@debug "Emitting raw" node_kind |
||||||
|
if (err = emit!(ctx); err !== nothing) |
||||||
|
return err |
||||||
|
end |
||||||
|
else |
||||||
|
return ErrorException("unhandled node of type $(JuliaSyntax.kind(ctx.node)), current text:\n" * String(take!(ctx.io))) |
||||||
|
end |
||||||
|
return nothing |
||||||
|
end |
||||||
|
|
||||||
|
function format_context(sourcetext)::Tuple{Any,Union{Nothing, Exception}} |
||||||
|
# Build the context |
||||||
|
ctx, err = Context(sourcetext) |
||||||
|
if err !== nothing |
||||||
|
return ctx, err |
||||||
|
end |
||||||
|
# Run the formatter |
||||||
|
err = format_context!(ctx) |
||||||
|
return ctx, err |
||||||
|
end |
||||||
|
|
||||||
|
""" |
||||||
|
format_string(sourcetext::AbstractString) -> String |
||||||
|
|
||||||
|
Format a string. |
||||||
|
""" |
||||||
|
function format_string(sourcetext::AbstractString) |
||||||
|
# Format it! |
||||||
|
ctx, err = format_context(sourcetext) |
||||||
|
if err !== nothing |
||||||
|
throw(err) |
||||||
|
end |
||||||
|
# Return the string |
||||||
|
return String(take!(ctx.io)) |
||||||
|
end |
||||||
|
|
||||||
|
""" |
||||||
|
format_file(inputfile::AbstractString, outputfile::AbstractString; inplace::Bool=false) |
||||||
|
|
||||||
|
Format a file. |
||||||
|
""" |
||||||
|
function format_file(inputfile::AbstractString, outputfile::Union{AbstractString, Nothing} = nothing; inplace::Bool=false) |
||||||
|
# Argument handling |
||||||
|
sourcetext = read(inputfile, String) |
||||||
|
if outputfile === nothing && !inplace |
||||||
|
error("output file required when `inplace = false`") |
||||||
|
end |
||||||
|
if isfile(outputfile) && samefile(inputfile, outputfile) && !inplace |
||||||
|
error("must not use same input and output file when `inplace = false`") |
||||||
|
end |
||||||
|
# Format it |
||||||
|
ctx, err = format_context(sourcetext) |
||||||
|
if err !== nothing |
||||||
|
throw(err) |
||||||
|
end |
||||||
|
# Write the output on success |
||||||
|
write(outputfile, take!(ctx.io)) |
||||||
|
return |
||||||
|
end |
||||||
|
|
||||||
|
if isdefined(Base, Symbol("@main")) |
||||||
|
include("main.jl") |
||||||
|
end |
||||||
|
|
||||||
|
end # module |
||||||
@ -0,0 +1,131 @@ |
|||||||
|
errno::Cint = 0 |
||||||
|
|
||||||
|
function panic(msg...) |
||||||
|
printstyled(stderr, "ERROR: "; color = :red, bold = true) |
||||||
|
for m in msg |
||||||
|
if m isa Exception |
||||||
|
showerror(stderr, m) |
||||||
|
else |
||||||
|
print(stderr, msg...) |
||||||
|
end |
||||||
|
end |
||||||
|
println(stderr) |
||||||
|
global errno = 1 |
||||||
|
return errno |
||||||
|
end |
||||||
|
|
||||||
|
function (@main)(argv) |
||||||
|
# Reset errno |
||||||
|
global errno = 0 |
||||||
|
|
||||||
|
# Default values |
||||||
|
inputfiles = String[] |
||||||
|
outputfile = nothing |
||||||
|
inplace::Bool = false |
||||||
|
|
||||||
|
# Parse the arguments |
||||||
|
while length(argv) > 0 |
||||||
|
x = popfirst!(argv) |
||||||
|
if x == "-i" |
||||||
|
inplace = true |
||||||
|
elseif x == "-o" |
||||||
|
if length(argv) < 1 |
||||||
|
return panic("expected output file as argument after `-o`") |
||||||
|
end |
||||||
|
outputfile = popfirst!(argv) |
||||||
|
else |
||||||
|
# Remaining arguments must be inputfile(s) |
||||||
|
push!(inputfiles, x) |
||||||
|
for x in argv |
||||||
|
if x == "-" |
||||||
|
return panic("input \"-\" can not be used with multiple files") |
||||||
|
end |
||||||
|
push!(inputfiles, x) |
||||||
|
end |
||||||
|
break |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
# stdin is the default input |
||||||
|
if isempty(inputfiles) |
||||||
|
push!(inputfiles, "-") |
||||||
|
end |
||||||
|
|
||||||
|
# multiple files require -i and no -o |
||||||
|
if length(inputfiles) > 1 |
||||||
|
if !inplace |
||||||
|
return panic("option `-i` is required for multiple input files") |
||||||
|
elseif outputfile !== nothing |
||||||
|
return panic("option `-o` is incompatible with multiple input files") |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
# inplace = true is incompatible with given output |
||||||
|
if inplace && outputfile !== nothing |
||||||
|
@assert length(inputfiles) == 1 |
||||||
|
return panic("option `-i` is incompatible with option `-o $(outputfile)`") |
||||||
|
end |
||||||
|
|
||||||
|
# inplace = true is incompatible with stdin as input |
||||||
|
if inplace && first(inputfiles) == "-" |
||||||
|
return panic("option `-i` is incompatible with stdin as input") |
||||||
|
end |
||||||
|
|
||||||
|
# Loop over the input files |
||||||
|
for inputfile in inputfiles |
||||||
|
# Read the input |
||||||
|
input_is_file = true |
||||||
|
if inputfile == "-" |
||||||
|
input_is_file = false |
||||||
|
sourcetext = try |
||||||
|
read(stdin, String) |
||||||
|
catch err |
||||||
|
return panic("could not read input from stdin: ", err) |
||||||
|
end |
||||||
|
elseif isfile(inputfile) |
||||||
|
sourcetext = try |
||||||
|
read(inputfile, String) |
||||||
|
catch err |
||||||
|
panic("could not read input from file `$(inputfile)`: ", err) |
||||||
|
continue |
||||||
|
end |
||||||
|
else |
||||||
|
panic("input file does not exist: `$(inputfile)`") |
||||||
|
continue |
||||||
|
end |
||||||
|
|
||||||
|
# Check output |
||||||
|
if inplace |
||||||
|
@assert outputfile === nothing |
||||||
|
@assert isfile(inputfile) |
||||||
|
@assert input_is_file |
||||||
|
# @assert length(inputfiles) == 1 # checked above |
||||||
|
output = inputfile |
||||||
|
else |
||||||
|
@assert length(inputfiles) == 1 |
||||||
|
if outputfile === nothing || outputfile == "-" |
||||||
|
output = stdout |
||||||
|
elseif isfile(outputfile) && input_is_file && samefile(outputfile, inputfile) |
||||||
|
return panic("can not use same file for input and output, use `-i` to modify a file in place") |
||||||
|
else |
||||||
|
output = outputfile |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
# Call the library to format the text |
||||||
|
ctx, err = format_context(sourcetext) |
||||||
|
if err !== nothing |
||||||
|
panic(err) |
||||||
|
continue |
||||||
|
end |
||||||
|
|
||||||
|
# Write the output |
||||||
|
try |
||||||
|
write(output, take!(ctx.io)) |
||||||
|
catch err |
||||||
|
panic("could not write to output: ", err) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
return errno |
||||||
|
end |
||||||
Loading…
Reference in new issue