Skip to content
Closed
62 changes: 59 additions & 3 deletions format/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"bytes"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/therxwold/elu/ast"
Expand All @@ -31,17 +33,61 @@ func Bytes(path string, src []byte) ([]byte, error) {
return []byte(out), nil
}

// Path formats the file at path in place. Overwrites the original.
// Path formats the file at path in place. Overwrites the original. It creates a temp file first, then renames it to the original file.
func Path(path string) error {
success := false
// create a temp file
temp, err := os.CreateTemp(filepath.Dir(path), "elu-format-*")
if err != nil {
return err
}
// deleting the temp file in case of crash
defer func() {
if !success {
os.Remove(temp.Name())
}
}()
// read the original file
b, err := os.ReadFile(path)
if err != nil {
return err
}
out, err := Bytes(path, b)
// write the formatted file
out, err := Bytes(temp.Name(), b)
if err != nil {
return err
}
_, err = temp.Write(out)
if err != nil {
return err
}
return os.WriteFile(path, out, 0o644)
// close the temp file before renaming it to the original file
temp.Close()
// rename the temp file to the original file
err = os.Rename(temp.Name(), path)
if err != nil {
// Windows fallback;
// try to delete the original file and rename the temp file to it
if runtime.GOOS == "windows" {
// creating a bak file
bak := path + ".bak"
if err = os.Rename(path, bak); err != nil {
return err
}
// renaming the temp file to the original file
err = os.Rename(temp.Name(), path)
if err != nil {
// restoring the bak file
os.Rename(bak, path)
return err
}
os.Remove(bak)
return nil
}
return err
}
success = true
return nil
}

// File renders a parsed ELU AST back to canonical formatted text.
Expand All @@ -50,10 +96,13 @@ func File(f *ast.File) string {
var b bytes.Buffer
fmt.Fprintf(&b, "pack %q version %d\n", f.PackID, f.Version)
fmt.Fprintf(&b, "type = %q\n", f.Type)
// Render all nodes in the file.
for _, n := range f.Nodes {
b.WriteByte('\n')
writeNode(&b, n, 0)
}
// Add a newline if the file didn't end with one.
// This is important for tools that expect a newline at the end of files.
if !bytes.HasSuffix(b.Bytes(), []byte("\n")) {
b.WriteByte('\n')
}
Expand All @@ -65,18 +114,22 @@ func writeNode(b *bytes.Buffer, n *ast.Node, indent int) {
pad := strings.Repeat(" ", indent)
switch n.Kind {
case ast.NodeBlock:
// Blocks are rendered as key: name\n{ children }.
fmt.Fprintf(b, "%s%s %q:\n", pad, n.Key, n.Name)
for _, c := range n.Children {
writeNode(b, c, indent+2)
}
case ast.NodeSection:
// Sections are rendered as key:\n{ children }.
fmt.Fprintf(b, "%s%s:\n", pad, sectionKey(n.Key))
for _, c := range n.Children {
writeNode(b, c, indent+2)
}
case ast.NodeAssign:
// Assignments are rendered as key = value\n.
fmt.Fprintf(b, "%s%s = %s\n", pad, n.Key, renderValue(n.Value))
case ast.NodeListItem:
// List items are rendered as either an expression, a value, or a section.
writeListItem(b, n, indent)
}
}
Expand Down Expand Up @@ -132,13 +185,16 @@ func isBareKey(s string) bool {
if s == "" {
return false
}
// Check first rune separately, since it can't be a digit or dot.
for i, r := range s {
if i == 0 {
// Check if the rune is a valid first rune.
if !(r == '_' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z')) {
return false
}
continue
}
// Check if the rune is a valid identifier rune.
if !(r == '_' || r == '.' || r == '-' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9')) {
return false
}
Expand Down
Loading