-
Notifications
You must be signed in to change notification settings - Fork 19
/
ast.go
330 lines (295 loc) · 9.59 KB
/
ast.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
// 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
}
// 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)
}
}
// SortNodes sorts nodes by the given less function.
func SortNodes(parent *Node, ns []*Node, less NodeLess) {
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++ {
}
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 is a NodeLess function that orders adjacent scalar nodes with the same name by
// their scalar value.
func ByFieldValue(_, 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 vi.Value < vj.Value
}
func getChildValueByFieldSubfield(field, subfield string, n *Node) *Value {
if field != "" {
if n.Name != field {
return nil
}
}
return n.getChildValue(subfield)
}
// 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
}
}
// 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
})
}
// 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
}
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
}