| // Package ast provides data structure representing textproto syntax tree. |
| package ast |
| |
| import ( |
| "fmt" |
| "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. gqui style: |
| // { 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 |
| } |
| |
| // Unquote returns the value of the string node. |
| // Calling Unquote on non-string node doesn't panic, but is otherwise undefined. |
| func (n *Node) Unquote() (string, error) { |
| var ret strings.Builder |
| for _, v := range n.Values { |
| uq, err := strconv.Unquote(v.Value) |
| if err != nil { |
| return "", err |
| } |
| ret.WriteString(uq) |
| } |
| return ret.String(), nil |
| } |
| |
| // IsCommentOnly returns true if this is a comment-only node. |
| func (n *Node) IsCommentOnly() bool { |
| return n.Name == "" && n.Children == nil |
| } |
| |
| 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 (n *Node) fix() fixData { |
| 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, |
| } |
| |
| 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 == "", |
| } |
| } |
| |
| // 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 |
| } |