blob: d4a73f0a7dfa3696e27bea4a449de495072fc386 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"fmt"
"io"
"strings"
"text/scanner"
)
const (
indentWidth = 4
disableComment = "// gidl-format off"
enableComment = "// gidl-format on"
)
type formatter struct {
scanner.Scanner
enabled bool
err error
// Stack of open brackets that have not yet been closed.
brackets []rune
}
// format formats GIDL syntax from src to dst.
// It uses filename for error messages about src.
func format(dst io.StringWriter, src io.Reader, filename string) error {
var f formatter
f.Init(src)
f.Filename = filename
// Don't skip comments.
f.Mode &^= scanner.SkipComments
f.Error = func(s *scanner.Scanner, msg string) {
f.fail(msg)
}
f.enable()
f.write(dst)
return f.err
}
func (f *formatter) fail(format string, args ...interface{}) {
f.err = fmt.Errorf("%s: %s", f.Position, fmt.Sprintf(format, args...))
}
func (f *formatter) enable() {
f.enabled = true
// Skip all whitespace except newlines.
f.Whitespace = scanner.GoWhitespace &^ (1 << '\n')
}
func (f *formatter) disable() {
f.enabled = false
// Preserve whitespace while disabled.
f.Whitespace = 0
}
func (f *formatter) write(dst io.StringWriter) {
var (
prev rune
pendingNewlines int
)
for tok := f.Scan(); tok != scanner.EOF && f.err == nil; tok = f.Scan() {
// Keep track of bracket nesting, whether formatting is enabled or not.
enclosingBracket := f.innermostBracket()
if ok := f.updateBrackets(tok); !ok {
break
}
// Toggle enabled/disabled based on special comments.
if f.enabled {
if tok == scanner.Comment && f.TokenText() == disableComment {
f.disable()
// Reset prev to avoid incorrect spacing after re-enabling.
prev = 0
}
} else {
if tok == scanner.Comment && f.TokenText() == enableComment {
f.enable()
}
// While disabled, copy the input unchanged.
dst.WriteString(f.TokenText())
continue
}
// Count newlines but don't print them yet.
if tok == '\n' {
pendingNewlines++
continue
}
// Enforce trailing commas when there is a line break.
if pendingNewlines > 0 && prev != 0 && needCommaBetween(prev, tok) {
dst.WriteString(",")
}
// Delete trailing commas when there is no line break.
if tok == ',' && isCloseBracket(f.Peek()) {
continue
}
// Having reached a non-newline character, print the correct number of
// newlines. Don't print newlines between empty brackets.
wroteNewline := false
if pendingNewlines >= 1 && !(isOpenBracket(prev) && tok == closeBracket(prev)) {
wroteNewline = true
dst.WriteString("\n")
// Collapse multiple blank lines to a single blank line.
if pendingNewlines >= 2 {
dst.WriteString("\n")
}
}
pendingNewlines = 0
// Enforce a blank line between top-level declarations.
if len(f.brackets) == 0 && tok == '}' {
pendingNewlines = 2
}
// Add whitespace before the token.
if prev == 0 || wroteNewline {
depth := len(f.brackets)
// At the start of a line, a close bracket should dedent itself, but
// an open bracket should not indent itself.
if isOpenBracket(tok) {
depth--
}
dst.WriteString(strings.Repeat(" ", indentWidth*depth))
} else if needSpaceBetween(prev, tok, enclosingBracket) {
dst.WriteString(" ")
}
// Finally, write the token itself.
dst.WriteString(f.TokenText())
prev = tok
}
// End the file with a single newline.
if f.err == nil && f.enabled {
dst.WriteString("\n")
}
}
func (f *formatter) innermostBracket() rune {
if len(f.brackets) == 0 {
return 0
}
return f.brackets[len(f.brackets)-1]
}
func (f *formatter) updateBrackets(tok rune) bool {
if isOpenBracket(tok) {
f.brackets = append(f.brackets, tok)
} else if isCloseBracket(tok) {
if len(f.brackets) == 0 {
f.fail("extraenous closing bracket '%c'", tok)
return false
}
var open rune
open, f.brackets = f.brackets[len(f.brackets)-1], f.brackets[:len(f.brackets)-1]
if tok != closeBracket(open) {
f.fail("mismatched closing bracket '%c' (expected '%c')", tok, closeBracket(open))
return false
}
}
return true
}
func isOpenBracket(tok rune) bool {
return tok == '(' || tok == '[' || tok == '{'
}
func isCloseBracket(tok rune) bool {
return tok == ')' || tok == ']' || tok == '}'
}
func closeBracket(open rune) rune {
switch open {
case '(':
return ')'
case '[':
return ']'
case '{':
return '}'
default:
panic("invalid open bracket")
}
}
// needCommaBetween assumes there is a newline between lhs and rhs, and returns
// true if there should be a comma before the newline.
func needCommaBetween(lhs, rhs rune) bool {
if lhs == 0 || rhs == 0 || lhs == '\n' || rhs == '\n' {
panic("invalid character")
}
switch lhs {
case ',', scanner.Comment:
return false
}
return !isOpenBracket(lhs) && isCloseBracket(rhs)
}
// needSpaceBetween returns true if there should be a space between lhs and rhs,
// assuming the most recent open bracket before lhs was enclosingBracket.
func needSpaceBetween(lhs, rhs rune, enclosingBracket rune) bool {
if lhs == 0 || rhs == 0 || lhs == '\n' || rhs == '\n' {
panic("invalid character")
}
// Add a space after lhs?
switch lhs {
case ',', '=', '+':
return true
case '-':
// Add space if it's a binary operation (handle rights).
return rhs == scanner.Ident
case ':':
// Don't add a space for the byte syntax like "padding:3" or
// "repeat(0xff):8", which occurs in a square bracket list.
return enclosingBracket != '['
}
// Add a space before rhs?
switch rhs {
case '=', '+', scanner.Comment:
return true
case '-':
// Add space if it's a binary operation (handle rights).
return lhs == scanner.Ident
case '{':
// This adds the space in `success("Foo") {`.
return lhs == ')'
}
return false
}