...

Source file src/github.com/emicklei/proto/option.go

Documentation: github.com/emicklei/proto

     1  // Copyright (c) 2017 Ernest Micklei
     2  //
     3  // MIT License
     4  //
     5  // Permission is hereby granted, free of charge, to any person obtaining
     6  // a copy of this software and associated documentation files (the
     7  // "Software"), to deal in the Software without restriction, including
     8  // without limitation the rights to use, copy, modify, merge, publish,
     9  // distribute, sublicense, and/or sell copies of the Software, and to
    10  // permit persons to whom the Software is furnished to do so, subject to
    11  // the following conditions:
    12  //
    13  // The above copyright notice and this permission notice shall be
    14  // included in all copies or substantial portions of the Software.
    15  //
    16  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    17  // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    18  // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    19  // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
    20  // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    21  // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
    22  // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    23  
    24  package proto
    25  
    26  import (
    27  	"bytes"
    28  	"fmt"
    29  	"sort"
    30  	"text/scanner"
    31  )
    32  
    33  // Option is a protoc compiler option
    34  type Option struct {
    35  	Position   scanner.Position
    36  	Comment    *Comment
    37  	Name       string
    38  	Constant   Literal
    39  	IsEmbedded bool
    40  	// AggregatedConstants is DEPRECATED. These Literals are populated into Constant.OrderedMap
    41  	AggregatedConstants []*NamedLiteral
    42  	InlineComment       *Comment
    43  	Parent              Visitee
    44  }
    45  
    46  // parse reads an Option body
    47  // ( ident | "(" fullIdent ")" ) { "." ident } "=" constant ";"
    48  func (o *Option) parse(p *Parser) error {
    49  	pos, tok, lit := p.nextIdentifier()
    50  	if tLEFTPAREN == tok {
    51  		pos, tok, lit = p.nextIdentifier()
    52  		if tok != tIDENT {
    53  			if !isKeyword(tok) {
    54  				return p.unexpected(lit, "option full identifier", o)
    55  			}
    56  		}
    57  		pos, tok, _ = p.next()
    58  		if tok != tRIGHTPAREN {
    59  			return p.unexpected(lit, "option full identifier closing )", o)
    60  		}
    61  		o.Name = fmt.Sprintf("(%s)", lit)
    62  	} else {
    63  		if tCOMMENT == tok {
    64  			nc := newComment(pos, lit)
    65  			if o.Comment != nil {
    66  				o.Comment.Merge(nc)
    67  			} else {
    68  				o.Comment = nc
    69  			}
    70  			return o.parse(p)
    71  		}
    72  		// non full ident
    73  		if tIDENT != tok {
    74  			if !isKeyword(tok) {
    75  				return p.unexpected(lit, "option identifier", o)
    76  			}
    77  		}
    78  		o.Name = lit
    79  	}
    80  	pos, tok, lit = p.next()
    81  	if tDOT == tok {
    82  		// extend identifier
    83  		pos, tok, lit = p.nextIdent(true) // keyword allowed as start
    84  		if tok != tIDENT {
    85  			if !isKeyword(tok) {
    86  				return p.unexpected(lit, "option postfix identifier", o)
    87  			}
    88  		}
    89  		o.Name = fmt.Sprintf("%s.%s", o.Name, lit)
    90  		pos, tok, lit = p.next()
    91  	}
    92  	if tEQUALS != tok {
    93  		return p.unexpected(lit, "option value assignment =", o)
    94  	}
    95  	r := p.peekNonWhitespace()
    96  	var err error
    97  	// values of an option can have illegal escape sequences
    98  	// for the standard Go scanner used by this package.
    99  	p.ignoreIllegalEscapesWhile(func() {
   100  		if '{' == r {
   101  			// aggregate
   102  			p.next() // consume {
   103  			err = o.parseAggregate(p)
   104  		} else {
   105  			// non aggregate
   106  			l := new(Literal)
   107  			l.Position = pos
   108  			if e := l.parse(p); e != nil {
   109  				err = e
   110  			}
   111  			o.Constant = *l
   112  		}
   113  	})
   114  	return err
   115  }
   116  
   117  // inlineComment is part of commentInliner.
   118  func (o *Option) inlineComment(c *Comment) {
   119  	o.InlineComment = c
   120  }
   121  
   122  // Accept dispatches the call to the visitor.
   123  func (o *Option) Accept(v Visitor) {
   124  	v.VisitOption(o)
   125  }
   126  
   127  // Doc is part of Documented
   128  func (o *Option) Doc() *Comment {
   129  	return o.Comment
   130  }
   131  
   132  // Literal represents intLit,floatLit,strLit or boolLit or a nested structure thereof.
   133  type Literal struct {
   134  	Position scanner.Position
   135  	Source   string
   136  	IsString bool
   137  
   138  	// It not nil then the entry is actually a comment with line(s)
   139  	// modelled this way because Literal is not an elementContainer
   140  	Comment *Comment
   141  
   142  	// The rune use to delimit the string value (only valid iff IsString)
   143  	QuoteRune rune
   144  
   145  	// literal value can be an array literal value (even nested)
   146  	Array []*Literal
   147  
   148  	// literal value can be a map of literals (even nested)
   149  	// DEPRECATED: use OrderedMap instead
   150  	Map map[string]*Literal
   151  
   152  	// literal value can be a map of literals (even nested)
   153  	// this is done as pairs of name keys and literal values so the original ordering is preserved
   154  	OrderedMap LiteralMap
   155  }
   156  
   157  var emptyRune rune
   158  
   159  // LiteralMap is like a map of *Literal but preserved the ordering.
   160  // Can be iterated yielding *NamedLiteral values.
   161  type LiteralMap []*NamedLiteral
   162  
   163  // Get returns a Literal from the map.
   164  func (m LiteralMap) Get(key string) (*Literal, bool) {
   165  	for _, each := range m {
   166  		if each.Name == key {
   167  			// exit on the first match
   168  			return each.Literal, true
   169  		}
   170  	}
   171  	return new(Literal), false
   172  }
   173  
   174  // SourceRepresentation returns the source (use the same rune that was used to delimit the string).
   175  func (l Literal) SourceRepresentation() string {
   176  	var buf bytes.Buffer
   177  	if l.IsString {
   178  		if l.QuoteRune == emptyRune {
   179  			buf.WriteRune('"')
   180  		} else {
   181  			buf.WriteRune(l.QuoteRune)
   182  		}
   183  	}
   184  	buf.WriteString(l.Source)
   185  	if l.IsString {
   186  		if l.QuoteRune == emptyRune {
   187  			buf.WriteRune('"')
   188  		} else {
   189  			buf.WriteRune(l.QuoteRune)
   190  		}
   191  	}
   192  	return buf.String()
   193  }
   194  
   195  // parse expects to read a literal constant after =.
   196  func (l *Literal) parse(p *Parser) error {
   197  	pos, tok, lit := p.next()
   198  	// handle special element inside literal, a comment line
   199  	if isComment(lit) {
   200  		nc := newComment(pos, lit)
   201  		if l.Comment == nil {
   202  			l.Comment = nc
   203  		} else {
   204  			l.Comment.Merge(nc)
   205  		}
   206  		// continue with remaining entries
   207  		return l.parse(p)
   208  	}
   209  	if tok == tLEFTSQUARE {
   210  		// collect array elements
   211  		array := []*Literal{}
   212  
   213  		// if it's an empty array, consume the close bracket, set the Array to
   214  		// an empty array, and return
   215  		r := p.peekNonWhitespace()
   216  		if ']' == r {
   217  			pos, _, _ := p.next()
   218  			l.Array = array
   219  			l.IsString = false
   220  			l.Position = pos
   221  			return nil
   222  		}
   223  
   224  		for {
   225  			e := new(Literal)
   226  			if err := e.parse(p); err != nil {
   227  				return err
   228  			}
   229  			array = append(array, e)
   230  			_, tok, lit := p.next()
   231  			if tok == tCOMMA {
   232  				continue
   233  			}
   234  			if tok == tRIGHTSQUARE {
   235  				break
   236  			}
   237  			return p.unexpected(lit, ", or ]", l)
   238  		}
   239  		l.Array = array
   240  		l.IsString = false
   241  		l.Position = pos
   242  		return nil
   243  	}
   244  	if tLEFTCURLY == tok {
   245  		l.Position, l.Source, l.IsString = pos, "", false
   246  		constants, err := parseAggregateConstants(p, l)
   247  		if err != nil {
   248  			return nil
   249  		}
   250  		l.OrderedMap = LiteralMap(constants)
   251  		return nil
   252  	}
   253  	if "-" == lit {
   254  		// negative number
   255  		if err := l.parse(p); err != nil {
   256  			return err
   257  		}
   258  		// modify source and position
   259  		l.Position, l.Source = pos, "-"+l.Source
   260  		return nil
   261  	}
   262  	source := lit
   263  	iss := isString(lit)
   264  	if iss {
   265  		source, l.QuoteRune = unQuote(source)
   266  	}
   267  	l.Position, l.Source, l.IsString = pos, source, iss
   268  
   269  	// peek for multiline strings
   270  	for {
   271  		pos, tok, lit := p.next()
   272  		if isString(lit) {
   273  			line, _ := unQuote(lit)
   274  			l.Source += line
   275  		} else {
   276  			p.nextPut(pos, tok, lit)
   277  			break
   278  		}
   279  	}
   280  	return nil
   281  }
   282  
   283  // NamedLiteral associates a name with a Literal
   284  type NamedLiteral struct {
   285  	*Literal
   286  	Name string
   287  	// PrintsColon is true when the Name must be printed with a colon suffix
   288  	PrintsColon bool
   289  }
   290  
   291  // parseAggregate reads options written using aggregate syntax.
   292  // tLEFTCURLY { has been consumed
   293  func (o *Option) parseAggregate(p *Parser) error {
   294  	constants, err := parseAggregateConstants(p, o)
   295  	literalMap := map[string]*Literal{}
   296  	for _, each := range constants {
   297  		literalMap[each.Name] = each.Literal
   298  	}
   299  	o.Constant = Literal{Map: literalMap, OrderedMap: constants, Position: o.Position}
   300  
   301  	// reconstruct the old, deprecated field
   302  	o.AggregatedConstants = collectAggregatedConstants(literalMap)
   303  	return err
   304  }
   305  
   306  // flatten the maps of each literal, recursively
   307  // this func exists for deprecated Option.AggregatedConstants.
   308  func collectAggregatedConstants(m map[string]*Literal) (list []*NamedLiteral) {
   309  	for k, v := range m {
   310  		if v.Map != nil {
   311  			sublist := collectAggregatedConstants(v.Map)
   312  			for _, each := range sublist {
   313  				list = append(list, &NamedLiteral{
   314  					Name:        k + "." + each.Name,
   315  					PrintsColon: true,
   316  					Literal:     each.Literal,
   317  				})
   318  			}
   319  		} else {
   320  			list = append(list, &NamedLiteral{
   321  				Name:        k,
   322  				PrintsColon: true,
   323  				Literal:     v,
   324  			})
   325  		}
   326  	}
   327  	// sort list by position of literal
   328  	sort.Sort(byPosition(list))
   329  	return
   330  }
   331  
   332  type byPosition []*NamedLiteral
   333  
   334  func (b byPosition) Less(i, j int) bool {
   335  	return b[i].Literal.Position.Line < b[j].Literal.Position.Line
   336  }
   337  func (b byPosition) Len() int      { return len(b) }
   338  func (b byPosition) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
   339  
   340  func parseAggregateConstants(p *Parser, container interface{}) (list []*NamedLiteral, err error) {
   341  	for {
   342  		pos, tok, lit := p.nextIdentifier()
   343  		if tRIGHTSQUARE == tok {
   344  			p.nextPut(pos, tok, lit)
   345  			// caller has checked for open square ; will consume rightsquare, rightcurly and semicolon
   346  			return
   347  		}
   348  		if tRIGHTCURLY == tok {
   349  			return
   350  		}
   351  		if tSEMICOLON == tok {
   352  			// just consume it
   353  			continue
   354  			//return
   355  		}
   356  		if tCOMMENT == tok {
   357  			// assign to last parsed literal
   358  			// TODO: see TestUseOfSemicolonsInAggregatedConstants
   359  			continue
   360  		}
   361  		if tCOMMA == tok {
   362  			if len(list) == 0 {
   363  				err = p.unexpected(lit, "non-empty option aggregate key", container)
   364  				return
   365  			}
   366  			continue
   367  		}
   368  		if tIDENT != tok && !isKeyword(tok) {
   369  			err = p.unexpected(lit, "option aggregate key", container)
   370  			return
   371  		}
   372  		// workaround issue #59 TODO
   373  		if isString(lit) && len(list) > 0 {
   374  			// concatenate with previous constant
   375  			s, _ := unQuote(lit)
   376  			list[len(list)-1].Source += s
   377  			continue
   378  		}
   379  		key := lit
   380  		printsColon := false
   381  		// expect colon, aggregate or plain literal
   382  		pos, tok, lit = p.next()
   383  		if tCOLON == tok {
   384  			// consume it
   385  			printsColon = true
   386  			pos, tok, lit = p.next()
   387  		}
   388  		// see if nested aggregate is started
   389  		if tLEFTCURLY == tok {
   390  			nested, fault := parseAggregateConstants(p, container)
   391  			if fault != nil {
   392  				err = fault
   393  				return
   394  			}
   395  
   396  			// create the map
   397  			m := map[string]*Literal{}
   398  			for _, each := range nested {
   399  				m[each.Name] = each.Literal
   400  			}
   401  			list = append(list, &NamedLiteral{
   402  				Name:        key,
   403  				PrintsColon: printsColon,
   404  				Literal:     &Literal{Map: m, OrderedMap: LiteralMap(nested)}})
   405  			continue
   406  		}
   407  		// no aggregate, put back token
   408  		p.nextPut(pos, tok, lit)
   409  		// now we see plain literal
   410  		l := new(Literal)
   411  		l.Position = pos
   412  		if err = l.parse(p); err != nil {
   413  			return
   414  		}
   415  		list = append(list, &NamedLiteral{Name: key, Literal: l, PrintsColon: printsColon})
   416  	}
   417  }
   418  
   419  func (o *Option) parent(v Visitee) { o.Parent = v }
   420  

View as plain text