blob: 0a5191d8e7c2e898931d06d10892fe38150a61d0 [file] [log] [blame]
// Package ast provides data structure representing textproto syntax tree.
package ast
import (
"fmt"
"sort"
"strconv"
"strings"
)
// Position describes a position of a token in the input.
// Both byte-based and line/column-based positions are maintained
// because different downstream consumers need different formats
// and we don't want to keep the entire input in memory to be able
// to convert between the two.
// Fields Byte, Line and Column should be interpreted as
// ByteRange.start_byte, TextRange.start_line, and TextRange.start_column
// of devtools.api.Range proto.
type Position struct {
Byte uint32
Line int32
Column int32
}
// Node represents a field with a value in a proto message, or a comment unattached to a field.
type Node struct {
// Start describes the start position of the node.
// For nodes that span entire lines, this is the first character
// of the first line attributed to the node; possible a whitespace if the node is indented.
// For nodes that are members of one-line message literals,
// this is the first non-whitespace character encountered.
Start Position
// Lines of comments appearing before the field.
// Each non-empty line starts with a # and does not contain the trailing newline.
PreComments []string
// Name of proto field (eg 'presubmit'). Will be an empty string for comment-only
// nodes and unqualified messages, e.g.
// { name: "first_msg" }
// { name: "second_msg" }
Name string
// Values, for nodes that don't have children.
Values []*Value
// Children for nodes that have children.
Children []*Node
// Whether or not this node was deleted by edits.
Deleted bool
// Should the colon after the field name be omitted?
// (e.g. "presubmit: {" vs "presubmit {")
SkipColon bool
// Whether or not all children are in the same line.
// (eg "base { id: "id" }")
ChildrenSameLine bool
// Comment in the same line as the "}".
ClosingBraceComment string
// End holds the position suitable for inserting new items.
// For multi-line nodes, this is the first character on the line with the closing brace.
// For single-line nodes, this is the first character after the last item (usually a space).
// For non-message nodes, this is Position zero value.
End Position
// Keep values in list (e.g "list: [1, 2]").
ValuesAsList bool
// Keep children in list (e.g "list: [ { value: 1 }, { value: 2 } ]").
ChildrenAsList bool
// Lines of comments appearing after last value inside list.
// Each non-empty line starts with a # and does not contain the trailing newline.
// e.g
// field: [
// value
// # Comment
// ]
PostValuesComments []string
// Whether the braces used for the children of this node are curly braces or angle brackets.
IsAngleBracket bool
// If this is not empty, it means that formatting was disabled for this node and it contains the
// raw, unformatted node string.
Raw string
// Used when we want to break between the field name and values when a
// single-line node exceeds the requested wrap column.
PutSingleValueOnNextLine bool
// Field number from proto definition (0 if unknown/not applicable).
FieldNumber int32
}
// NodeLess is a sorting function that compares two *Nodes, possibly using the parent Node
// for context, returning whether a is less than b.
type NodeLess func(parent, a, b *Node, isWholeSlice bool) bool
// ChainNodeLess combines two NodeLess functions that returns the first comparison if values are
// not equal, else returns the second.
func ChainNodeLess(first, second NodeLess) NodeLess {
if first == nil {
return second
}
if second == nil {
return first
}
return func(parent, a, b *Node, isWholeSlice bool) bool {
if isALess := first(parent, a, b, isWholeSlice); isALess {
return true
}
if isBLess := first(parent, b, a, isWholeSlice); isBLess {
return false
}
return second(parent, a, b, isWholeSlice)
}
}
type sortOptions struct {
reverse bool
}
// A SortOption configures SortNodes.
type SortOption func(*sortOptions)
// ReverseOrdering controls whether to sort the Nodes in ascending or descending order. By default
// the Nodes are sorted in ascending order. By setting this option to true, the Nodes will be sorted
// in descending order.
//
// Default: false.
func ReverseOrdering(enabled bool) SortOption {
return func(opts *sortOptions) {
opts.reverse = enabled
}
}
// SortNodes sorts nodes by the given less function.
func SortNodes(parent *Node, ns []*Node, less NodeLess, opts ...SortOption) {
var options sortOptions
for _, opt := range opts {
opt(&options)
}
if options.reverse {
sort.Stable(sort.Reverse(sortableNodes(parent, ns, less, true /* isWholeSlice */)))
} else {
sort.Stable(sortableNodes(parent, ns, less, true /* isWholeSlice */))
}
end := 0
for begin := 0; begin < len(ns); begin = end {
for end = begin + 1; end < len(ns) && ns[begin].Name == ns[end].Name; end++ {
}
if options.reverse {
sort.Stable(sort.Reverse(sortableNodes(parent, ns[begin:end], less, false /* isWholeSlice */)))
} else {
sort.Stable(sortableNodes(parent, ns[begin:end], less, false /* isWholeSlice */))
}
}
}
// sortableNodes returns a sort.Interface that sorts nodes by the given less function.
func sortableNodes(parent *Node, ns []*Node, less NodeLess, isWholeSlice bool) sort.Interface {
return sortable{parent, ns, less, isWholeSlice}
}
type sortable struct {
parent *Node
ns []*Node
less NodeLess
isWholeSlice bool
}
func (s sortable) Len() int {
return len(s.ns)
}
func (s sortable) Swap(i, j int) {
s.ns[i], s.ns[j] = s.ns[j], s.ns[i]
}
func (s sortable) Less(i, j int) bool {
if s.less == nil {
return false
}
return s.less(s.parent, s.ns[i], s.ns[j], s.isWholeSlice)
}
// ByFieldName is a NodeLess function that orders nodes by their field name.
func ByFieldName(_, ni, nj *Node, isWholeSlice bool) bool {
return ni.Name < nj.Name
}
func getFieldValueForByFieldValue(n *Node) *Value {
if len(n.Values) != 1 {
return nil
}
return n.Values[0]
}
// ByFieldValue returns a NodeLess function that orders adjacent scalar nodes
// with the same name by their scalar value. The values are passed through
// `projection` before sorting.
func ByFieldValue(projection func(string) string) NodeLess {
return func(_, ni, nj *Node, isWholeSlice bool) bool {
if isWholeSlice {
return false
}
vi := getFieldValueForByFieldValue(ni)
vj := getFieldValueForByFieldValue(nj)
if vi == nil {
return vj != nil
}
if vj == nil {
return false
}
return projection(vi.Value) < projection(vj.Value)
}
}
func getChildValueByFieldSubfield(field, subfield string, n *Node) *Value {
if field != "" {
if n.Name != field {
return nil
}
}
return n.getChildValue(subfield)
}
func getChildValueByFieldSubfieldPath(field string, subfieldPath []string, n *Node) *Value {
if field != "" && n.Name != field {
return nil
}
nodes := GetFromPath(n.Children, subfieldPath)
if len(nodes) != 1 {
return nil
}
if len(nodes[0].Values) != 1 {
return nil
}
return nodes[0].Values[0]
}
// ByFieldSubfield returns a NodeLess function that orders adjacent message nodes with the given
// field name by the given subfield name value. If no field name is provided, it compares the
// subfields of any adjacent nodes with matching names.
func ByFieldSubfield(field, subfield string) NodeLess {
return func(_, ni, nj *Node, isWholeSlice bool) bool {
if isWholeSlice {
return false
}
vi := getChildValueByFieldSubfield(field, subfield, ni)
vj := getChildValueByFieldSubfield(field, subfield, nj)
if vi == nil {
return vj != nil
}
if vj == nil {
return false
}
return vi.Value < vj.Value
}
}
// ByFieldSubfieldPath returns a NodeLess function that orders adjacent message nodes with the given
// field name by the given subfield path value. If no field name is provided, it compares the
// subfields of any adjacent nodes with matching names. Values are passed
// through `projection` before sorting.
func ByFieldSubfieldPath(field string, subfieldPath []string, projection func(string) string) NodeLess {
return func(_, ni, nj *Node, isWholeSlice bool) bool {
if isWholeSlice {
return false
}
vi := getChildValueByFieldSubfieldPath(field, subfieldPath, ni)
vj := getChildValueByFieldSubfieldPath(field, subfieldPath, nj)
if vi == nil {
return vj != nil
}
if vj == nil {
return false
}
return projection(vi.Value) < projection(vj.Value)
}
}
// ByFieldNumber is a NodeLess function that orders fields by their field numbers.
// Field numbers are populated during parsing from descriptor information.
func ByFieldNumber(_, ni, nj *Node, isWholeSlice bool) bool {
if !isWholeSlice {
return false
}
numI, numJ := ni.FieldNumber, nj.FieldNumber
// If both have field numbers, sort by field number
if numI > 0 && numJ > 0 {
return numI < numJ
}
// If only one has field number, prioritize it
if numI > 0 && numJ == 0 {
return true // ni has priority
}
if numI == 0 && numJ > 0 {
return false // nj has priority
}
// If neither has field number, fall back to alphabetical order
return ni.Name < nj.Name
}
// getChildValue returns the Value of the child with the given field name,
// or nil if no single such child exists.
func (n *Node) getChildValue(field string) *Value {
for _, c := range n.Children {
if c.Name == field {
if len(c.Values) != 1 {
return nil
}
return c.Values[0]
}
}
return nil
}
// IsCommentOnly returns true if this is a comment-only node. Even a node that
// only contains a blank line is considered a comment-only node in the sense
// that it has no proto content.
func (n *Node) IsCommentOnly() bool {
return n.Name == "" && n.Children == nil
}
// IsBlankLine returns true if this is a blank line node.
func (n *Node) IsBlankLine() bool {
return n.IsCommentOnly() && len(n.PreComments) == 1 && n.PreComments[0] == ""
}
type fixData struct {
inline bool
}
// Fix fixes inconsistencies that may arise after manipulation.
//
// For example if a node is ChildrenSameLine but has non-inline children, or
// children with comments ChildrenSameLine will be set to false.
func (n *Node) Fix() {
n.fix()
}
func isRealPosition(p Position) bool {
return p.Byte != 0 || p.Line != 0 || p.Column != 0
}
func (n *Node) fix() fixData {
isEmptyAndWasOriginallyInline := !(isRealPosition(n.Start) && isRealPosition(n.End) && n.End.Line-n.Start.Line > 0)
d := fixData{
// ChildrenSameLine may be false for cases with no children such as a
// value `foo: false`. We don't want these to trigger expansion.
inline: n.ChildrenSameLine || (len(n.Children) == 0 && isEmptyAndWasOriginallyInline && len(n.Values) <= 1),
}
for _, c := range n.Children {
if c.Deleted {
continue
}
cd := c.fix()
if !cd.inline {
d.inline = false
}
}
for _, v := range n.Values {
vd := v.fix()
if !vd.inline {
d.inline = false
}
}
n.ChildrenSameLine = d.inline
// textproto comments go until the end of the line, so we must force parents
// to be multiline otherwise we will partially comment them out.
if len(n.PreComments) > 0 || len(n.ClosingBraceComment) > 0 {
d.inline = false
}
return d
}
// StringNode is a helper for constructing simple string nodes.
func StringNode(name, unquoted string) *Node {
return &Node{Name: name, Values: []*Value{{Value: strconv.Quote(unquoted)}}}
}
// Value represents a field value in a proto message.
type Value struct {
// Lines of comments appearing before the value (for multi-line strings).
// Each non-empty line starts with a # and does not contain the trailing newline.
PreComments []string
// Node value (eg 'ERROR').
Value string
// Comment in the same line as the value.
InlineComment string
}
func (v *Value) String() string {
return fmt.Sprintf("{Value: %q, PreComments: %q, InlineComment: %q}", v.Value, strings.Join(v.PreComments, "\n"), v.InlineComment)
}
func (v *Value) fix() fixData {
return fixData{
inline: len(v.PreComments) == 0 && v.InlineComment == "",
}
}
// SortValues sorts values by their value.
func SortValues(values []*Value) {
sort.SliceStable(values, func(i, j int) bool {
return values[i].Value < values[j].Value
})
}
// SortValuesReverse reverse sorts values by their value.
func SortValuesReverse(values []*Value) {
sort.SliceStable(values, func(i, j int) bool {
return values[i].Value > values[j].Value
})
}
// GetFromPath returns all nodes with a given string path in the parse tree. See ast_test.go for examples.
func GetFromPath(nodes []*Node, path []string) []*Node {
if len(path) == 0 {
return nil
}
var res []*Node
for _, node := range nodes {
if node.Name == path[0] {
if len(path) == 1 {
res = append(res, node)
} else {
res = append(res, GetFromPath(node.Children, path[1:])...)
}
}
}
return res
}