| // Package parser edits text proto files, applies standard formatting |
| // and preserves comments. |
| // See also: https://github.com/golang/protobuf/blob/master/proto/text_parser.go |
| // |
| // To disable a specific file from getting formatted, add '# txtpbfmt: disable' |
| // at the top of the file. |
| package parser |
| |
| import ( |
| "bufio" |
| "bytes" |
| "fmt" |
| "sort" |
| "strings" |
| |
| log "github.com/golang/glog" |
| "github.com/protocolbuffers/txtpbfmt/ast" |
| ) |
| |
| // Config can be used to pass additional config parameters to the formatter at |
| // the time of the API call. |
| type Config struct { |
| // Expand all children irrespective of the initial state. |
| ExpandAllChildren bool |
| |
| // Skip colons whenever possible. |
| SkipAllColons bool |
| |
| // Allow unnamed nodes everywhere. |
| // Default is to allow only top-level nodes to be unnamed. |
| AllowUnnamedNodesEverywhere bool |
| |
| // Sort fields by field name. |
| SortFieldsByFieldName bool |
| |
| // Sort adjacent scalar fields of the same field name by their contents. |
| SortRepeatedFieldsByContent bool |
| |
| // Remove lines that have the same field name and scalar value as another. |
| RemoveDuplicateValuesForRepeatedFields bool |
| |
| // Permit usage of Python-style """ or ''' delimited strings. |
| AllowTripleQuotedStrings bool |
| |
| // Reflow comments and multi-line strings to stay within 80 character long. |
| EnableLineLimit bool |
| } |
| |
| type parser struct { |
| in []byte |
| index int |
| length int |
| log log.Verbose |
| // Maps the index of '{' characters on 'in' that have the matching '}' on |
| // the same line to 'true'. |
| bracketSameLine map[int]bool |
| config Config |
| line, column int // current position, 1-based. |
| } |
| |
| var defConfig = Config{} |
| |
| const indentSpaces = " " |
| |
| // Max character length allowed when "EnableLineLimit" is enabled. |
| const lineLimit = 80 |
| |
| func getMetaComments(in []byte) map[string]bool { |
| metaComments := map[string]bool{} |
| scanner := bufio.NewScanner(bytes.NewReader(in)) |
| for scanner.Scan() { |
| line := scanner.Text() |
| if len(line) == 0 { |
| continue |
| } |
| if line[0] != byte('#') { |
| break // only process the leading comment block |
| } |
| colon := strings.IndexByte(line, byte(':')) |
| if colon > 1 && strings.TrimSpace(line[1:colon]) == "txtpbfmt" { |
| for _, s := range strings.Split(line[colon+1:], ",") { |
| metaComments[strings.TrimSpace(s)] = true |
| } |
| } |
| } |
| return metaComments |
| } |
| |
| // Format formats a text proto file preserving comments. |
| func Format(in []byte) ([]byte, error) { |
| return FormatWithConfig(in, defConfig) |
| } |
| |
| // FormatWithConfig functions similar to format, but allows the user to pass in |
| // additional configuration options. |
| func FormatWithConfig(in []byte, c Config) ([]byte, error) { |
| metaComments := getMetaComments(in) |
| if metaComments["disable"] { |
| log.Infoln("Ignored file with 'disable' comment.") |
| return in, nil |
| } |
| nodes, err := parseWithConfig(in, &c, metaComments) |
| if err != nil { |
| return nil, err |
| } |
| return out(nodes, c), nil |
| } |
| |
| // Return the byte-positions of each bracket which has the corresponding close on the |
| // same line as a set. |
| func sameLineBrackets(in []byte, allowTripleQuotedStrings bool) (map[int]bool, error) { |
| line := 1 |
| type bracket struct { |
| index int |
| line int |
| } |
| open := []bracket{} // Stack. |
| res := map[int]bool{} |
| insideComment := false |
| insideString := false |
| insideTripleQuotedString := false |
| var stringDelimiter string |
| isEscapedChar := false |
| for i, c := range in { |
| switch c { |
| case '\n': |
| line++ |
| insideComment = false |
| case '{', '<': |
| if insideComment || insideString { |
| continue |
| } |
| open = append(open, bracket{index: i, line: line}) |
| case '}', '>': |
| if insideComment || insideString { |
| continue |
| } |
| if len(open) == 0 { |
| return nil, fmt.Errorf("too many '}' or '>' at index %d", i) |
| } |
| last := len(open) - 1 |
| br := open[last] |
| open = open[:last] |
| if br.line == line { |
| res[br.index] = true |
| } |
| case '#': |
| if insideString { |
| continue |
| } |
| insideComment = true |
| case '"', '\'': |
| if insideComment { |
| continue |
| } |
| delim := string(c) |
| tripleQuoted := false |
| if allowTripleQuotedStrings && i+3 <= len(in) { |
| triple := string(in[i : i+3]) |
| if triple == `"""` || triple == `'''` { |
| delim = triple |
| tripleQuoted = true |
| } |
| } |
| |
| if insideString { |
| if stringDelimiter == delim && (insideTripleQuotedString || !isEscapedChar) { |
| insideString = false |
| insideTripleQuotedString = false |
| } |
| } else { |
| insideString = true |
| if tripleQuoted { |
| insideTripleQuotedString = true |
| } |
| stringDelimiter = delim |
| } |
| } |
| |
| if isEscapedChar { |
| isEscapedChar = false |
| } else if c == '\\' && insideString && !insideTripleQuotedString { |
| isEscapedChar = true |
| } |
| } |
| if insideString { |
| return nil, fmt.Errorf("unterminated string literal") |
| } |
| return res, nil |
| } |
| |
| func removeDeleted(nodes []*ast.Node) []*ast.Node { |
| res := []*ast.Node{} |
| // When removing a node which has an empty line before it, we should keep |
| // the empty line before the next non-removed node to maintain the visual separation. |
| // Consider the following: |
| // foo: { name: "foo1" } |
| // foo: { name: "foo2" } |
| // |
| // bar: { name: "bar1" } |
| // bar: { name: "bar2" } |
| // |
| // If we decide to remove both foo2 and bar1, the result should still have one empty |
| // line between foo1 and bar2. |
| addEmptyLine := false |
| for _, node := range nodes { |
| if node.Deleted { |
| if len(node.PreComments) > 0 && node.PreComments[0] == "" { |
| addEmptyLine = true |
| } |
| continue |
| } |
| if len(node.Children) > 0 { |
| node.Children = removeDeleted(node.Children) |
| } |
| if addEmptyLine && (len(node.PreComments) == 0 || node.PreComments[0] != "") { |
| node.PreComments = append([]string{""}, node.PreComments...) |
| } |
| addEmptyLine = false |
| res = append(res, node) |
| } |
| return res |
| } |
| |
| var ( |
| spaceSeparators = []byte(" \t\n") |
| valueSeparators = []byte(" \t\n{}:,]<>;") |
| ) |
| |
| // Parse returns a tree representation of a textproto file. |
| func Parse(in []byte) ([]*ast.Node, error) { |
| return ParseWithConfig(in, &defConfig) |
| } |
| |
| // ParseWithConfig functions similar to Parse, but allows the user to pass in |
| // additional configuration options. |
| func ParseWithConfig(in []byte, c *Config) ([]*ast.Node, error) { |
| return parseWithConfig(in, c, getMetaComments(in)) |
| } |
| |
| func parseWithConfig(in []byte, c *Config, metaComments map[string]bool) ([]*ast.Node, error) { |
| if metaComments["expand_all_children"] { |
| c.ExpandAllChildren = true |
| } |
| if metaComments["skip_all_colons"] { |
| c.SkipAllColons = true |
| } |
| if metaComments["allow_unnamed_nodes_everywhere"] { |
| c.AllowUnnamedNodesEverywhere = true |
| } |
| if metaComments["sort_fields_by_field_name"] { |
| c.SortFieldsByFieldName = true |
| } |
| if metaComments["sort_repeated_fields_by_content"] { |
| c.SortRepeatedFieldsByContent = true |
| } |
| if metaComments["remove_duplicate_values_for_repeated_fields"] { |
| c.RemoveDuplicateValuesForRepeatedFields = true |
| } |
| if metaComments["allow_triple_quoted_strings"] { |
| c.AllowTripleQuotedStrings = true |
| } |
| if metaComments["enable_line_limit"] { |
| c.EnableLineLimit = true |
| } |
| p, err := newParser(in, *c) |
| if err != nil { |
| return nil, err |
| } |
| if p.log { |
| p.log.Infof("p.in: %q", string(p.in)) |
| p.log.Infof("p.length: %v", p.length) |
| } |
| // Although unnamed nodes aren't strictly allowed, some formats represent a |
| // list of protos as a list of unnamed top-level nodes. |
| nodes, _, err := p.parse( /*isRoot=*/ true) |
| if err != nil { |
| return nil, err |
| } |
| if p.index < p.length { |
| return nil, fmt.Errorf("parser didn't consume all input. Stopped at %s", p.errorContext()) |
| } |
| sortAndFilterNodes(nodes, nodeSortFunction(c.SortFieldsByFieldName, c.SortRepeatedFieldsByContent), c.RemoveDuplicateValuesForRepeatedFields) |
| return nodes, err |
| } |
| |
| func newParser(in []byte, c Config) (*parser, error) { |
| var bracketSameLine map[int]bool |
| if c.ExpandAllChildren { |
| bracketSameLine = map[int]bool{} |
| } else { |
| var err error |
| if bracketSameLine, err = sameLineBrackets(in, c.AllowTripleQuotedStrings); err != nil { |
| return nil, err |
| } |
| } |
| if len(in) > 0 && in[len(in)-1] != '\n' { |
| in = append(in, '\n') |
| } |
| parser := &parser{ |
| in: in, |
| index: 0, |
| length: len(in), |
| log: log.V(2), |
| bracketSameLine: bracketSameLine, |
| config: c, |
| line: 1, |
| column: 1, |
| } |
| return parser, nil |
| } |
| |
| func (p *parser) nextInputIs(b byte) bool { |
| return p.index < p.length && p.in[p.index] == b |
| } |
| |
| func (p *parser) consume(b byte) bool { |
| if !p.nextInputIs(b) { |
| return false |
| } |
| p.index++ |
| p.column++ |
| if b == '\n' { |
| p.line++ |
| p.column = 1 |
| } |
| return true |
| } |
| |
| // consumeString consumes the given string s, which should not have any newlines. |
| func (p *parser) consumeString(s string) bool { |
| if p.index+len(s) > p.length { |
| return false |
| } |
| if string(p.in[p.index:p.index+len(s)]) != s { |
| return false |
| } |
| p.index += len(s) |
| p.column += len(s) |
| return true |
| } |
| |
| // loopDetector detects if the parser is in an infinite loop (ie failing to |
| // make progress). |
| type loopDetector struct { |
| lastIndex int |
| count int |
| parser *parser |
| } |
| |
| func (p *parser) getLoopDetector() *loopDetector { |
| return &loopDetector{lastIndex: p.index, parser: p} |
| } |
| |
| func (l *loopDetector) iter() error { |
| if l.parser.index == l.lastIndex { |
| l.count++ |
| if l.count < 2 { |
| return nil |
| } |
| return fmt.Errorf("parser failed to make progress at %s", l.parser.errorContext()) |
| } |
| l.lastIndex = l.parser.index |
| l.count = 0 |
| return nil |
| } |
| |
| func (p parser) errorContext() string { |
| index := p.index |
| if index >= p.length { |
| index = p.length - 1 |
| } |
| // Provide the surrounding input as context. |
| lastContentIndex := index + 20 |
| if lastContentIndex >= p.length { |
| lastContentIndex = p.length - 1 |
| } |
| previousContentIndex := index - 20 |
| if previousContentIndex < 0 { |
| previousContentIndex = 0 |
| } |
| before := string(p.in[previousContentIndex:index]) |
| after := string(p.in[index:lastContentIndex]) |
| return fmt.Sprintf("index %v\nposition %+v\nbefore: %q\nafter: %q\nbefore+after: %q", index, p.position(), before, after, before+after) |
| } |
| |
| func (p *parser) position() ast.Position { |
| return ast.Position{ |
| Byte: uint32(p.index), |
| Line: int32(p.line), |
| Column: int32(p.column), |
| } |
| } |
| |
| // parse parses a text proto. |
| // It assumes the text to be either conformant with the standard text proto |
| // (i.e. passes proto.UnmarshalText() without error) or the alternative textproto |
| // format (sequence of messages, each of which passes proto.UnmarshalText()). |
| // endPos is the position of the first character on the first line |
| // after parsed nodes: that's the position to append more children. |
| func (p *parser) parse(isRoot bool) (result []*ast.Node, endPos ast.Position, err error) { |
| res := []*ast.Node{} |
| for ld := p.getLoopDetector(); p.index < p.length; { |
| if err := ld.iter(); err != nil { |
| return nil, ast.Position{}, err |
| } |
| |
| startPos := p.position() |
| if p.nextInputIs('\n') { |
| // p.parse is often invoked with the index pointing at the |
| // newline character after the previous item. |
| // We should still report that this item starts in the next line. |
| startPos.Byte++ |
| startPos.Line++ |
| startPos.Column = 1 |
| } |
| |
| // Read PreComments. |
| comments, blankLines := p.skipWhiteSpaceAndReadComments(true /* multiLine */) |
| |
| // Handle blank lines. |
| if blankLines > 1 { |
| if p.log { |
| p.log.Infof("blankLines: %v", blankLines) |
| } |
| comments = append([]string{""}, comments...) |
| } |
| |
| for p.nextInputIs('%') { |
| comments = append(comments, p.readTemplate()) |
| c, _ := p.skipWhiteSpaceAndReadComments(false) |
| comments = append(comments, c...) |
| } |
| |
| if endPos := p.position(); p.consume('}') || p.consume('>') { |
| // Handle comments after last child. |
| |
| if len(comments) > 0 { |
| res = append(res, &ast.Node{Start: startPos, PreComments: comments}) |
| } |
| |
| // endPos points at the closing brace, but we should rather return the position |
| // of the first character after the previous item. Therefore let's rewind a bit: |
| for p.in[endPos.Byte-1] == ' ' { |
| endPos.Byte-- |
| endPos.Column-- |
| } |
| |
| _ = p.consume(';') // Ignore optional ';'. |
| _ = p.consume(',') // Ignore optional ','. |
| |
| // Done parsing children. |
| return res, endPos, nil |
| } |
| |
| nd := &ast.Node{ |
| Start: startPos, |
| PreComments: comments, |
| } |
| if p.log { |
| p.log.Infof("PreComments: %q", strings.Join(nd.PreComments, "\n")) |
| } |
| |
| // Skip white-space other than '\n', which is handled below. |
| for p.consume(' ') || p.consume('\t') { |
| } |
| |
| // Handle multiple comment blocks. |
| // <example> |
| // # comment block 1 |
| // # comment block 1 |
| // |
| // # comment block 2 |
| // # comment block 2 |
| // </example> |
| // Each block that ends on an empty line (instead of a field) gets its own |
| // 'empty' node. |
| if p.nextInputIs('\n') { |
| res = append(res, nd) |
| continue |
| } |
| |
| // Handle end of file. |
| if p.index >= p.length { |
| nd.End = p.position() |
| if len(nd.PreComments) > 0 { |
| res = append(res, nd) |
| } |
| break |
| } |
| |
| if p.consume('[') { |
| // Read Name (of proto extension). |
| nd.Name = fmt.Sprintf("[%s]", p.readExtension()) |
| _ = p.consume(']') // Ignore the ']'. |
| } else { |
| // Read Name. |
| nd.Name = p.readFieldName() |
| if nd.Name == "" && !isRoot && !p.config.AllowUnnamedNodesEverywhere { |
| return nil, ast.Position{}, fmt.Errorf("Failed to find a FieldName at %s", p.errorContext()) |
| } |
| } |
| if p.log { |
| p.log.Infof("name: %q", nd.Name) |
| } |
| // Skip separator. |
| _, _ = p.skipWhiteSpaceAndReadComments(true /* multiLine */) |
| nd.SkipColon = !p.consume(':') |
| previousPos := p.position() |
| _, _ = p.skipWhiteSpaceAndReadComments(true /* multiLine */) |
| |
| if p.consume('{') || p.consume('<') { |
| if p.config.SkipAllColons { |
| nd.SkipColon = true |
| } |
| nd.ChildrenSameLine = p.bracketSameLine[p.index-1] |
| // Recursive call to parse child nodes. |
| nodes, lastPos, err := p.parse( /*isRoot=*/ false) |
| if err != nil { |
| return nil, ast.Position{}, err |
| } |
| nd.Children = nodes |
| nd.End = lastPos |
| |
| nd.ClosingBraceComment = p.readInlineComment() |
| } else if p.consume('[') { |
| // Handle list of values. |
| |
| nd.ValuesAsList = true // We found values in list - keep it as list. |
| openBracketLine := p.line |
| |
| // Skip separator. |
| preComments, _ := p.skipWhiteSpaceAndReadComments(true /* multiLine */) |
| |
| for ld := p.getLoopDetector(); !p.consume(']') && p.index < p.length; { |
| if err := ld.iter(); err != nil { |
| if p.nextInputIs('{') { |
| err = fmt.Errorf("\n\n[{}] notation not supported, see http://b/74558064.\n\nFull error: %s", err) |
| } |
| return nil, ast.Position{}, err |
| } |
| |
| // Read each value in the list. |
| vals, err := p.readValues() |
| if err != nil { |
| return nil, ast.Position{}, err |
| } |
| if len(vals) != 1 { |
| return nil, ast.Position{}, fmt.Errorf("multiple-string value not supported (%v). Please add comma explcitily, see http://b/162070952", vals) |
| } |
| vals[0].PreComments = append(vals[0].PreComments, preComments...) |
| |
| // Skip separator. |
| _, _ = p.skipWhiteSpaceAndReadComments(false /* multiLine */) |
| if p.consume(',') { |
| vals[0].InlineComment = p.readInlineComment() |
| } |
| |
| nd.Values = append(nd.Values, vals...) |
| |
| preComments, _ = p.skipWhiteSpaceAndReadComments(true /* multiLine */) |
| } |
| nd.ChildrenSameLine = (openBracketLine == p.line) |
| |
| res = append(res, nd) |
| |
| // Handle comments after last line (or for empty list) |
| nd.PostValuesComments = preComments |
| nd.ClosingBraceComment = p.readInlineComment() |
| |
| _ = p.consume(';') // Ignore optional ';'. |
| _ = p.consume(',') // Ignore optional ','. |
| continue |
| } else { |
| // Rewind comments. |
| p.index = int(previousPos.Byte) |
| p.line = int(previousPos.Line) |
| p.column = int(previousPos.Column) |
| // Handle Values. |
| nd.Values, err = p.readValues() |
| if err != nil { |
| return nil, ast.Position{}, err |
| } |
| _ = p.consume(';') // Ignore optional ';'. |
| _ = p.consume(',') // Ignore optional ','. |
| } |
| if p.log && p.index < p.length { |
| p.log.Infof("p.in[p.index]: %q", string(p.in[p.index])) |
| } |
| res = append(res, nd) |
| } |
| return res, p.position(), nil |
| } |
| |
| func (p *parser) readFieldName() string { |
| i := p.index |
| for ; i < p.length && !p.isValueSep(i); i++ { |
| } |
| return p.advance(i) |
| } |
| |
| func (p *parser) readExtension() string { |
| i := p.index |
| for ; i < p.length && (p.isBlankSep(i) || !p.isValueSep(i)); i++ { |
| } |
| return removeBlanks(p.advance(i)) |
| } |
| |
| func removeBlanks(in string) string { |
| s := []byte(in) |
| for _, b := range spaceSeparators { |
| s = bytes.Replace(s, []byte{b}, nil, -1) |
| } |
| return string(s) |
| } |
| |
| // skipWhiteSpaceAndReadComments has multiple cases: |
| // - (1) reading a block of comments followed by a blank line |
| // - (2) reading a block of comments followed by non-blank content |
| // - (3) reading the inline comments between the current char and the end of the |
| // current line |
| // Lines of comments and number of blank lines will be returned. |
| func (p *parser) skipWhiteSpaceAndReadComments(multiLine bool) ([]string, int) { |
| i := p.index |
| var foundComment, insideComment bool |
| commentBegin := 0 |
| var comments []string |
| blankLines := 0 |
| for ; i < p.length; i++ { |
| if p.in[i] == '#' && !insideComment { |
| insideComment = true |
| foundComment = true |
| commentBegin = i |
| } else if p.in[i] == '\n' { |
| if insideComment { |
| comments = append(comments, string(p.in[commentBegin:i])) // Exclude the '\n'. |
| insideComment = false |
| } else if foundComment { |
| i-- // Put back the last '\n' so the caller can detect that we're on case (1). |
| break |
| } else { |
| blankLines++ |
| } |
| if !multiLine { |
| break |
| } |
| } |
| if !insideComment && !p.isBlankSep(i) { |
| break |
| } |
| } |
| sep := p.advance(i) |
| if p.log { |
| p.log.Infof("sep: %q\np.index: %v", string(sep), p.index) |
| if p.index < p.length { |
| p.log.Infof("p.in[p.index]: %q", string(p.in[p.index])) |
| } |
| } |
| return comments, blankLines |
| } |
| |
| func (p *parser) isBlankSep(i int) bool { |
| return bytes.Contains(spaceSeparators, p.in[i:i+1]) |
| } |
| |
| func (p *parser) isValueSep(i int) bool { |
| return bytes.Contains(valueSeparators, p.in[i:i+1]) |
| } |
| |
| func (p *parser) advance(i int) string { |
| res := p.in[p.index:i] |
| p.index = i |
| strRes := string(res) |
| newlines := strings.Count(strRes, "\n") |
| if newlines == 0 { |
| p.column += len(strRes) |
| } else { |
| p.column = len(strRes) - strings.LastIndex(strRes, "\n") |
| p.line += newlines |
| } |
| return string(res) |
| } |
| |
| func (p *parser) readValues() ([]*ast.Value, error) { |
| var values []*ast.Value |
| var previousPos ast.Position |
| preComments, _ := p.skipWhiteSpaceAndReadComments(true /* multiLine */) |
| if p.nextInputIs('%') { |
| values = append(values, p.populateValue(p.readTemplate(), nil)) |
| previousPos = p.position() |
| } |
| if p.config.AllowTripleQuotedStrings { |
| v, err := p.readTripleQuotedString() |
| if err != nil { |
| return nil, err |
| } |
| if v != nil { |
| values = append(values, v) |
| previousPos = p.position() |
| } |
| } |
| for p.consume('"') || p.consume('\'') { |
| // Handle string value. |
| stringBegin := p.index - 1 // Index of the quote. |
| i := p.index |
| for ; i < p.length; i++ { |
| if p.in[i] == '\\' { |
| i++ // Skip escaped char. |
| continue |
| } |
| if p.in[i] == '\n' { |
| p.index = i |
| return nil, fmt.Errorf("found literal (unescaped) new line in string at %s", p.errorContext()) |
| } |
| if p.in[i] == p.in[stringBegin] { |
| vl := fixQuotes(p.advance(i)) |
| _ = p.advance(i + 1) // Skip the quote. |
| values = append(values, p.populateValue(vl, preComments)) |
| |
| previousPos = p.position() |
| preComments, _ = p.skipWhiteSpaceAndReadComments(true /* multiLine */) |
| break |
| } |
| } |
| if i == p.length { |
| p.index = i |
| return nil, fmt.Errorf("unfinished string at %s", p.errorContext()) |
| } |
| } |
| if previousPos != (ast.Position{}) { |
| // Rewind comments. |
| p.index = int(previousPos.Byte) |
| p.line = int(previousPos.Line) |
| p.column = int(previousPos.Column) |
| } else { |
| i := p.index |
| // Handle other values. |
| for ; i < p.length; i++ { |
| if p.isValueSep(i) { |
| break |
| } |
| } |
| vl := p.advance(i) |
| values = append(values, p.populateValue(vl, nil)) |
| } |
| if p.log { |
| p.log.Infof("values: %v", values) |
| } |
| return values, nil |
| } |
| |
| func (p *parser) readTripleQuotedString() (*ast.Value, error) { |
| start := p.index |
| stringBegin := p.index |
| delimiter := `"""` |
| if !p.consumeString(delimiter) { |
| delimiter = `'''` |
| if !p.consumeString(delimiter) { |
| return nil, nil |
| } |
| } |
| |
| for { |
| if p.consumeString(delimiter) { |
| break |
| } |
| if p.index == p.length { |
| p.index = start |
| return nil, fmt.Errorf("unfinished string at %s", p.errorContext()) |
| } |
| p.index++ |
| } |
| |
| v := p.populateValue(string(p.in[stringBegin:p.index]), nil) |
| |
| return v, nil |
| } |
| |
| func (p *parser) populateValue(vl string, preComments []string) *ast.Value { |
| if p.log { |
| p.log.Infof("value: %q", vl) |
| } |
| return &ast.Value{ |
| Value: vl, |
| InlineComment: p.readInlineComment(), |
| PreComments: preComments, |
| } |
| } |
| |
| func (p *parser) readInlineComment() string { |
| inlineComment, _ := p.skipWhiteSpaceAndReadComments(false /* multiLine */) |
| if p.log { |
| p.log.Infof("inlineComment: %q", strings.Join(inlineComment, "\n")) |
| } |
| if len(inlineComment) > 0 { |
| return inlineComment[0] |
| } |
| return "" |
| } |
| |
| func (p *parser) readTemplate() string { |
| if !p.nextInputIs('%') { |
| return "" |
| } |
| i := p.index + 1 |
| for ; i < p.length; i++ { |
| if p.in[i] == '"' || p.in[i] == '\'' { |
| stringBegin := i // Index of quote. |
| i++ |
| for ; i < p.length; i++ { |
| if p.in[i] == '\\' { |
| i++ // Skip escaped char. |
| continue |
| } |
| if p.in[i] == p.in[stringBegin] { |
| i++ // Skip end quote. |
| break |
| } |
| } |
| } |
| if p.in[i] == '%' { |
| i++ |
| break |
| } |
| } |
| return p.advance(i) |
| } |
| |
| func sortAndFilterNodes(nodes []*ast.Node, sortFunction func([]*ast.Node), removeDuplicates bool) { |
| if len(nodes) == 0 { |
| return |
| } |
| type nameAndValue struct { |
| name, value string |
| } |
| var seen map[nameAndValue]bool |
| if removeDuplicates { |
| seen = make(map[nameAndValue]bool) |
| } |
| for _, nd := range nodes { |
| if seen != nil && len(nd.Values) == 1 { |
| key := nameAndValue{nd.Name, nd.Values[0].Value} |
| if _, value := seen[key]; value { |
| // Name-Value pair found in the same nesting level, deleting. |
| nd.Deleted = true |
| } else { |
| seen[key] = true |
| } |
| } |
| sortAndFilterNodes(nd.Children, sortFunction, removeDuplicates) |
| } |
| if sortFunction != nil { |
| sortFunction(nodes) |
| } |
| } |
| |
| func fixQuotes(s string) string { |
| res := make([]byte, 0, len(s)) |
| res = append(res, '"') |
| for i := 0; i < len(s); i++ { |
| if s[i] == '"' { |
| res = append(res, '\\') |
| } else if s[i] == '\\' { |
| res = append(res, s[i]) |
| i++ |
| } |
| res = append(res, s[i]) |
| } |
| res = append(res, '"') |
| return string(res) |
| } |
| |
| // DebugFormat returns a textual representation of the specified nodes for |
| // consumption by humans when debugging (e.g. in test failures). No guarantees |
| // are made about the specific output. |
| func DebugFormat(nodes []*ast.Node, depth int) string { |
| res := []string{""} |
| prefix := strings.Repeat(".", depth) |
| for _, nd := range nodes { |
| var value string |
| if nd.Deleted { |
| res = append(res, "DELETED") |
| } |
| if nd.Children != nil { // Also for 0 children. |
| value = fmt.Sprintf("children:%s", DebugFormat(nd.Children, depth+1)) |
| } else { |
| value = fmt.Sprintf("values: %v\n", nd.Values) |
| } |
| res = append(res, |
| fmt.Sprintf("name: %q", nd.Name), |
| fmt.Sprintf("PreComments: %q (len %d)", strings.Join(nd.PreComments, "\n"), len(nd.PreComments)), |
| value) |
| } |
| return strings.Join(res, fmt.Sprintf("\n%s ", prefix)) |
| } |
| |
| // Pretty formats the nodes at the given indentation depth (0 = top-level). |
| func Pretty(nodes []*ast.Node, depth int) string { |
| var result strings.Builder |
| formatter{&result, defConfig}.writeNodes(removeDeleted(nodes), depth, false /* isSameLine */) |
| return result.String() |
| } |
| |
| func out(nodes []*ast.Node, c Config) []byte { |
| var result bytes.Buffer |
| formatter{&result, c}.writeNodes(removeDeleted(nodes), 0, false /* isSameLine */) |
| return result.Bytes() |
| } |
| |
| func nodeSortFunction(sortByFieldName, sortByFieldValue bool) func([]*ast.Node) { |
| switch { |
| case sortByFieldName && sortByFieldValue: |
| return func(ns []*ast.Node) { sort.Stable(ast.ByFieldNameAndValue(ns)) } |
| case sortByFieldName: |
| return func(ns []*ast.Node) { sort.Stable(ast.ByFieldName(ns)) } |
| case sortByFieldValue: |
| return func(ns []*ast.Node) { sort.Stable(ast.ByFieldValue(ns)) } |
| default: |
| return nil |
| } |
| } |
| |
| // stringWriter abstracts over bytes.Buffer and strings.Builder |
| type stringWriter interface { |
| WriteString(s string) (int, error) |
| } |
| |
| // formatter accumulates pretty-printed textproto contents into a stringWriter. |
| type formatter struct { |
| stringWriter |
| Config |
| } |
| |
| func (f formatter) writeNodes(nodes []*ast.Node, depth int, isSameLine bool) { |
| indent := " " |
| if !isSameLine { |
| indent = strings.Repeat(indentSpaces, depth) |
| } |
| for index, nd := range nodes { |
| formattedPreComments := f.formatComments(nd.PreComments, indent) |
| for _, comment := range formattedPreComments { |
| if len(comment) == 0 { |
| if !(depth == 0 && index == 0) { |
| f.WriteString("\n") |
| } |
| continue |
| } |
| f.WriteString(indent) |
| f.WriteString(comment) |
| f.WriteString("\n") |
| } |
| |
| if nd.IsCommentOnly() { |
| // The comments have been printed already, no more work to do. |
| continue |
| } |
| f.WriteString(indent) |
| // Node name may be empty in alternative-style textproto files, because they |
| // contain a sequence of proto messages of the same type: |
| // { name: "first_msg" } |
| // { name: "second_msg" } |
| // In all other cases, nd.Name is not empty and should be printed. |
| if nd.Name != "" { |
| f.WriteString(nd.Name) |
| if !nd.SkipColon { |
| f.WriteString(":") |
| } |
| |
| // The space after the name is required for one-liners and message fields: |
| // title: "there was a space here" |
| // metadata: { ... } |
| // In other cases, there is a newline right after the colon, so no space required. |
| if nd.Children != nil || (len(nd.Values) == 1 && len(nd.Values[0].PreComments) == 0) || nd.ValuesAsList { |
| f.WriteString(" ") |
| } |
| } |
| |
| if nd.ValuesAsList { // For ValuesAsList option we will preserve even empty list `field: []` |
| f.writeValuesAsList(nd, nd.Values, indent+indentSpaces) |
| } else if len(nd.Values) > 0 { |
| f.writeValues(nd.Values, indent+indentSpaces) |
| } |
| if nd.Children != nil { // Also for 0 Children. |
| f.writeChildren(nd.Children, depth+1, (isSameLine || nd.ChildrenSameLine)) |
| } |
| if (nd.Children != nil || nd.ValuesAsList) && len(nd.ClosingBraceComment) > 0 { |
| f.WriteString(indentSpaces) |
| f.WriteString(nd.ClosingBraceComment) |
| } |
| |
| if !isSameLine { |
| f.WriteString("\n") |
| } |
| } |
| } |
| |
| func (f formatter) writeValues(vals []*ast.Value, indent string) { |
| if len(vals) == 0 { |
| // This should never happen: formatValues can be called only if there are some values. |
| return |
| } |
| sep := "\n" + indent |
| if len(vals) == 1 && len(vals[0].PreComments) == 0 { |
| sep = "" |
| } |
| for _, v := range vals { |
| f.WriteString(sep) |
| formattedPreComments := f.formatComments(v.PreComments, indent) |
| for _, comment := range formattedPreComments { |
| f.WriteString(comment) |
| f.WriteString(sep) |
| } |
| f.WriteString(v.Value) |
| if len(v.InlineComment) > 0 { |
| f.WriteString(indentSpaces) |
| f.WriteString(v.InlineComment) |
| } |
| } |
| } |
| |
| func (f formatter) writeValuesAsList(nd *ast.Node, vals []*ast.Value, indent string) { |
| // Checks if it's possible to put whole list in a single line. |
| sameLine := nd.ChildrenSameLine && len(nd.PostValuesComments) == 0 |
| if sameLine { |
| // Parser found all children on a same line, but we need to check again. |
| // It's possible that AST was modified after parsing. |
| for _, val := range vals { |
| if len(val.PreComments) > 0 || len(vals[0].InlineComment) > 0 { |
| sameLine = false |
| break |
| } |
| } |
| } |
| sep := "" |
| if !sameLine { |
| sep = "\n" + indent |
| } |
| f.WriteString("[") |
| |
| for idx, v := range vals { |
| formattedPreComments := f.formatComments(v.PreComments, indent) |
| for _, comment := range formattedPreComments { |
| f.WriteString(sep) |
| f.WriteString(comment) |
| } |
| f.WriteString(sep) |
| f.WriteString(v.Value) |
| if idx < len(vals)-1 { // Don't put trailing comma that fails Python parser. |
| f.WriteString(",") |
| if sameLine { |
| f.WriteString(" ") |
| } |
| } |
| if len(v.InlineComment) > 0 { |
| f.WriteString(indentSpaces) |
| f.WriteString(v.InlineComment) |
| } |
| } |
| formattedPostComments := f.formatComments(nd.PostValuesComments, indent) |
| for _, comment := range formattedPostComments { |
| f.WriteString(sep) |
| f.WriteString(comment) |
| } |
| f.WriteString(strings.Replace(sep, indentSpaces, "", 1)) |
| f.WriteString("]") |
| } |
| |
| // writeChildren writes the child nodes. The result always ends with a closing brace. |
| func (f formatter) writeChildren(children []*ast.Node, depth int, sameLine bool) { |
| switch { |
| case sameLine && len(children) == 0: |
| f.WriteString("{}") |
| case sameLine: |
| f.WriteString("{") |
| f.writeNodes(children, depth, sameLine) |
| f.WriteString(" }") |
| default: |
| f.WriteString("{\n") |
| f.writeNodes(children, depth, sameLine) |
| f.WriteString(strings.Repeat(indentSpaces, depth-1)) |
| f.WriteString("}") |
| } |
| } |
| |
| func (f formatter) formatComments(comments []string, indent string) []string { |
| if f.EnableLineLimit { |
| comments = trimSpaces(comments) |
| return reflowLines(comments, indent, "# ") |
| } |
| return comments |
| } |
| |
| func trimSpaces(lines []string) (trimmedLines []string) { |
| for _, line := range lines { |
| trimmedLines = append(trimmedLines, strings.TrimSpace(line)) |
| } |
| return |
| } |
| |
| func reflowLines(lines []string, indent string, linePrefix string) (formatted []string) { |
| for _, line := range lines { |
| // Empty line. |
| if len(line) == 0 { |
| formatted = append(formatted, line) |
| continue |
| } |
| |
| // Small line. |
| maxLineLength := lineLimit - len([]rune(indent)) |
| if len(line) <= maxLineLength { |
| formatted = append(formatted, line) |
| continue |
| } |
| |
| // Long line. |
| currentLine := "" |
| for _, word := range strings.Split(line, " ") { |
| lineLength := len([]rune(currentLine)) |
| if lineLength == 0 { |
| currentLine += word |
| } else if currentLine == "#" || lineLength+len([]rune(word))+1 <= maxLineLength { |
| currentLine += " " + word |
| } else { |
| formatted = append(formatted, currentLine) |
| currentLine = "" |
| if len(word) > 0 { |
| currentLine = linePrefix + word |
| } |
| } |
| } |
| // Append last line. |
| if len(currentLine) > 0 { |
| formatted = append(formatted, currentLine) |
| } |
| } |
| |
| return |
| } |
| |
| func mergeConsecutiveLines(lines []string, delimiters []string) (mergedLines []string) { |
| endsWithAnyDelimiter := func(line string) bool { |
| for _, delimiter := range delimiters { |
| if strings.HasSuffix(line, delimiter) { |
| return true |
| } |
| } |
| return false |
| } |
| // Group lines in single lines separated by `.` or empty lines. |
| var currentLine string |
| for _, comment := range lines { |
| isEndOfLine := endsWithAnyDelimiter(comment) || len(comment) == 0 |
| if len(currentLine) == 0 { |
| currentLine += strings.TrimSpace(comment) |
| } else { |
| currentLine += " " + strings.TrimLeft(comment, " #\"") |
| } |
| |
| if isEndOfLine { |
| mergedLines = append(mergedLines, currentLine) |
| currentLine = "" |
| } |
| } |
| // Append the last dangling line. |
| if len(currentLine) > 0 { |
| mergedLines = append(mergedLines, currentLine) |
| } |
| return |
| } |