mirror of https://github.com/fredrikekre/Runic.jl
5 changed files with 470 additions and 0 deletions
@ -0,0 +1,22 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,3 @@
|
||||
# Runic.jl |
||||
|
||||
*A code formatter with rules set in stone.* |
||||
@ -0,0 +1,305 @@
@@ -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 @@
@@ -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