366 lines
9.2 KiB
Go
366 lines
9.2 KiB
Go
|
// Copyright (c) 2017 Ernest Micklei
|
||
|
//
|
||
|
// MIT License
|
||
|
//
|
||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||
|
// a copy of this software and associated documentation files (the
|
||
|
// "Software"), to deal in the Software without restriction, including
|
||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||
|
// the following conditions:
|
||
|
//
|
||
|
// The above copyright notice and this permission notice shall be
|
||
|
// included in all copies or substantial portions of the Software.
|
||
|
//
|
||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||
|
|
||
|
package proto
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"sort"
|
||
|
"text/scanner"
|
||
|
)
|
||
|
|
||
|
// Option is a protoc compiler option
|
||
|
type Option struct {
|
||
|
Position scanner.Position
|
||
|
Comment *Comment
|
||
|
Name string
|
||
|
Constant Literal
|
||
|
IsEmbedded bool
|
||
|
// AggregatedConstants is DEPRECATED. These Literals are populated into Constant.OrderedMap
|
||
|
AggregatedConstants []*NamedLiteral
|
||
|
InlineComment *Comment
|
||
|
Parent Visitee
|
||
|
}
|
||
|
|
||
|
// parse reads an Option body
|
||
|
// ( ident | "(" fullIdent ")" ) { "." ident } "=" constant ";"
|
||
|
func (o *Option) parse(p *Parser) error {
|
||
|
pos, tok, lit := p.nextIdentifier()
|
||
|
if tLEFTPAREN == tok {
|
||
|
pos, tok, lit = p.nextIdentifier()
|
||
|
if tok != tIDENT {
|
||
|
if !isKeyword(tok) {
|
||
|
return p.unexpected(lit, "option full identifier", o)
|
||
|
}
|
||
|
}
|
||
|
pos, tok, _ = p.next()
|
||
|
if tok != tRIGHTPAREN {
|
||
|
return p.unexpected(lit, "option full identifier closing )", o)
|
||
|
}
|
||
|
o.Name = fmt.Sprintf("(%s)", lit)
|
||
|
} else {
|
||
|
// non full ident
|
||
|
if tIDENT != tok {
|
||
|
if !isKeyword(tok) {
|
||
|
return p.unexpected(lit, "option identifier", o)
|
||
|
}
|
||
|
}
|
||
|
o.Name = lit
|
||
|
}
|
||
|
pos, tok, lit = p.next()
|
||
|
if tDOT == tok {
|
||
|
// extend identifier
|
||
|
pos, tok, lit = p.nextIdent(true) // keyword allowed as start
|
||
|
if tok != tIDENT {
|
||
|
if !isKeyword(tok) {
|
||
|
return p.unexpected(lit, "option postfix identifier", o)
|
||
|
}
|
||
|
}
|
||
|
o.Name = fmt.Sprintf("%s.%s", o.Name, lit)
|
||
|
pos, tok, lit = p.next()
|
||
|
}
|
||
|
if tEQUALS != tok {
|
||
|
return p.unexpected(lit, "option value assignment =", o)
|
||
|
}
|
||
|
r := p.peekNonWhitespace()
|
||
|
var err error
|
||
|
// values of an option can have illegal escape sequences
|
||
|
// for the standard Go scanner used by this package.
|
||
|
p.ignoreIllegalEscapesWhile(func() {
|
||
|
if '{' == r {
|
||
|
// aggregate
|
||
|
p.next() // consume {
|
||
|
err = o.parseAggregate(p)
|
||
|
} else {
|
||
|
// non aggregate
|
||
|
l := new(Literal)
|
||
|
l.Position = pos
|
||
|
if e := l.parse(p); e != nil {
|
||
|
err = e
|
||
|
}
|
||
|
o.Constant = *l
|
||
|
}
|
||
|
})
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// inlineComment is part of commentInliner.
|
||
|
func (o *Option) inlineComment(c *Comment) {
|
||
|
o.InlineComment = c
|
||
|
}
|
||
|
|
||
|
// Accept dispatches the call to the visitor.
|
||
|
func (o *Option) Accept(v Visitor) {
|
||
|
v.VisitOption(o)
|
||
|
}
|
||
|
|
||
|
// Doc is part of Documented
|
||
|
func (o *Option) Doc() *Comment {
|
||
|
return o.Comment
|
||
|
}
|
||
|
|
||
|
// Literal represents intLit,floatLit,strLit or boolLit or a nested structure thereof.
|
||
|
type Literal struct {
|
||
|
Position scanner.Position
|
||
|
Source string
|
||
|
IsString bool
|
||
|
// literal value can be an array literal value (even nested)
|
||
|
Array []*Literal
|
||
|
// literal value can be a map of literals (even nested)
|
||
|
// DEPRECATED: use OrderedMap instead
|
||
|
Map map[string]*Literal
|
||
|
// literal value can be a map of literals (even nested)
|
||
|
// this is done as pairs of name keys and literal values so the original ordering is preserved
|
||
|
OrderedMap LiteralMap
|
||
|
}
|
||
|
|
||
|
// LiteralMap is like a map of *Literal but preserved the ordering.
|
||
|
// Can be iterated yielding *NamedLiteral values.
|
||
|
type LiteralMap []*NamedLiteral
|
||
|
|
||
|
// Get returns a Literal from the map.
|
||
|
func (m LiteralMap) Get(key string) (*Literal, bool) {
|
||
|
for _, each := range m {
|
||
|
if each.Name == key {
|
||
|
// exit on the first match
|
||
|
return each.Literal, true
|
||
|
}
|
||
|
}
|
||
|
return new(Literal), false
|
||
|
}
|
||
|
|
||
|
// SourceRepresentation returns the source (if quoted then use double quote).
|
||
|
func (l Literal) SourceRepresentation() string {
|
||
|
var buf bytes.Buffer
|
||
|
if l.IsString {
|
||
|
buf.WriteRune('"')
|
||
|
}
|
||
|
buf.WriteString(l.Source)
|
||
|
if l.IsString {
|
||
|
buf.WriteRune('"')
|
||
|
}
|
||
|
return buf.String()
|
||
|
}
|
||
|
|
||
|
// parse expects to read a literal constant after =.
|
||
|
func (l *Literal) parse(p *Parser) error {
|
||
|
pos, tok, lit := p.next()
|
||
|
if tok == tLEFTSQUARE {
|
||
|
// collect array elements
|
||
|
array := []*Literal{}
|
||
|
for {
|
||
|
e := new(Literal)
|
||
|
if err := e.parse(p); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
array = append(array, e)
|
||
|
_, tok, lit = p.next()
|
||
|
if tok == tCOMMA {
|
||
|
continue
|
||
|
}
|
||
|
if tok == tRIGHTSQUARE {
|
||
|
break
|
||
|
}
|
||
|
return p.unexpected(lit, ", or ]", l)
|
||
|
}
|
||
|
l.Array = array
|
||
|
l.IsString = false
|
||
|
l.Position = pos
|
||
|
return nil
|
||
|
}
|
||
|
if tLEFTCURLY == tok {
|
||
|
l.Position, l.Source, l.IsString = pos, "", false
|
||
|
constants, err := parseAggregateConstants(p, l)
|
||
|
if err != nil {
|
||
|
return nil
|
||
|
}
|
||
|
l.OrderedMap = LiteralMap(constants)
|
||
|
return nil
|
||
|
}
|
||
|
if "-" == lit {
|
||
|
// negative number
|
||
|
if err := l.parse(p); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
// modify source and position
|
||
|
l.Position, l.Source = pos, "-"+l.Source
|
||
|
return nil
|
||
|
}
|
||
|
source := lit
|
||
|
iss := isString(lit)
|
||
|
if iss {
|
||
|
source = unQuote(source)
|
||
|
}
|
||
|
l.Position, l.Source, l.IsString = pos, source, iss
|
||
|
|
||
|
// peek for multiline strings
|
||
|
for {
|
||
|
pos, tok, lit = p.next()
|
||
|
if isString(lit) {
|
||
|
l.Source += unQuote(lit)
|
||
|
} else {
|
||
|
p.nextPut(pos, tok, lit)
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// NamedLiteral associates a name with a Literal
|
||
|
type NamedLiteral struct {
|
||
|
*Literal
|
||
|
Name string
|
||
|
// PrintsColon is true when the Name must be printed with a colon suffix
|
||
|
PrintsColon bool
|
||
|
}
|
||
|
|
||
|
// parseAggregate reads options written using aggregate syntax.
|
||
|
// tLEFTCURLY { has been consumed
|
||
|
func (o *Option) parseAggregate(p *Parser) error {
|
||
|
constants, err := parseAggregateConstants(p, o)
|
||
|
literalMap := map[string]*Literal{}
|
||
|
for _, each := range constants {
|
||
|
literalMap[each.Name] = each.Literal
|
||
|
}
|
||
|
o.Constant = Literal{Map: literalMap, OrderedMap: constants, Position: o.Position}
|
||
|
|
||
|
// reconstruct the old, deprecated field
|
||
|
o.AggregatedConstants = collectAggregatedConstants(literalMap)
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// flatten the maps of each literal, recursively
|
||
|
// this func exists for deprecated Option.AggregatedConstants.
|
||
|
func collectAggregatedConstants(m map[string]*Literal) (list []*NamedLiteral) {
|
||
|
for k, v := range m {
|
||
|
if v.Map != nil {
|
||
|
sublist := collectAggregatedConstants(v.Map)
|
||
|
for _, each := range sublist {
|
||
|
list = append(list, &NamedLiteral{
|
||
|
Name: k + "." + each.Name,
|
||
|
PrintsColon: true,
|
||
|
Literal: each.Literal,
|
||
|
})
|
||
|
}
|
||
|
} else {
|
||
|
list = append(list, &NamedLiteral{
|
||
|
Name: k,
|
||
|
PrintsColon: true,
|
||
|
Literal: v,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
// sort list by position of literal
|
||
|
sort.Sort(byPosition(list))
|
||
|
return
|
||
|
}
|
||
|
|
||
|
type byPosition []*NamedLiteral
|
||
|
|
||
|
func (b byPosition) Less(i, j int) bool {
|
||
|
return b[i].Literal.Position.Line < b[j].Literal.Position.Line
|
||
|
}
|
||
|
func (b byPosition) Len() int { return len(b) }
|
||
|
func (b byPosition) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
||
|
|
||
|
func parseAggregateConstants(p *Parser, container interface{}) (list []*NamedLiteral, err error) {
|
||
|
for {
|
||
|
pos, tok, lit := p.nextIdentifier()
|
||
|
if tRIGHTSQUARE == tok {
|
||
|
p.nextPut(pos, tok, lit)
|
||
|
// caller has checked for open square ; will consume rightsquare, rightcurly and semicolon
|
||
|
return
|
||
|
}
|
||
|
if tRIGHTCURLY == tok {
|
||
|
return
|
||
|
}
|
||
|
if tSEMICOLON == tok {
|
||
|
// just consume it
|
||
|
continue
|
||
|
//return
|
||
|
}
|
||
|
if tCOMMENT == tok {
|
||
|
// assign to last parsed literal
|
||
|
// TODO: see TestUseOfSemicolonsInAggregatedConstants
|
||
|
continue
|
||
|
}
|
||
|
if tCOMMA == tok {
|
||
|
if len(list) == 0 {
|
||
|
err = p.unexpected(lit, "non-empty option aggregate key", container)
|
||
|
return
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
if tIDENT != tok && !isKeyword(tok) {
|
||
|
err = p.unexpected(lit, "option aggregate key", container)
|
||
|
return
|
||
|
}
|
||
|
// workaround issue #59 TODO
|
||
|
if isString(lit) && len(list) > 0 {
|
||
|
// concatenate with previous constant
|
||
|
list[len(list)-1].Source += unQuote(lit)
|
||
|
continue
|
||
|
}
|
||
|
key := lit
|
||
|
printsColon := false
|
||
|
// expect colon, aggregate or plain literal
|
||
|
pos, tok, lit = p.next()
|
||
|
if tCOLON == tok {
|
||
|
// consume it
|
||
|
printsColon = true
|
||
|
pos, tok, lit = p.next()
|
||
|
}
|
||
|
// see if nested aggregate is started
|
||
|
if tLEFTCURLY == tok {
|
||
|
nested, fault := parseAggregateConstants(p, container)
|
||
|
if fault != nil {
|
||
|
err = fault
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// create the map
|
||
|
m := map[string]*Literal{}
|
||
|
for _, each := range nested {
|
||
|
m[each.Name] = each.Literal
|
||
|
}
|
||
|
list = append(list, &NamedLiteral{
|
||
|
Name: key,
|
||
|
PrintsColon: printsColon,
|
||
|
Literal: &Literal{Map: m, OrderedMap: LiteralMap(nested)}})
|
||
|
continue
|
||
|
}
|
||
|
// no aggregate, put back token
|
||
|
p.nextPut(pos, tok, lit)
|
||
|
// now we see plain literal
|
||
|
l := new(Literal)
|
||
|
l.Position = pos
|
||
|
if err = l.parse(p); err != nil {
|
||
|
return
|
||
|
}
|
||
|
list = append(list, &NamedLiteral{Name: key, Literal: l, PrintsColon: printsColon})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (o *Option) parent(v Visitee) { o.Parent = v }
|