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
24package proto
25
26import (
27	"bytes"
28	"fmt"
29	"text/scanner"
30)
31
32// Option is a protoc compiler option
33type Option struct {
34	Position            scanner.Position
35	Comment             *Comment
36	Name                string
37	Constant            Literal
38	IsEmbedded          bool
39	AggregatedConstants []*NamedLiteral
40	InlineComment       *Comment
41}
42
43// columns returns printable source tokens
44func (o *Option) columns() (cols []aligned) {
45	if !o.IsEmbedded {
46		cols = append(cols, leftAligned("option "))
47	} else {
48		cols = append(cols, leftAligned(" ["))
49	}
50	cols = append(cols, o.keyValuePair(o.IsEmbedded)...)
51	if o.IsEmbedded {
52		cols = append(cols, leftAligned("]"))
53	}
54	if !o.IsEmbedded {
55		cols = append(cols, alignedSemicolon)
56		if o.InlineComment != nil {
57			cols = append(cols, notAligned(" //"), notAligned(o.InlineComment.Message()))
58		}
59	}
60	return
61}
62
63// keyValuePair returns key = value or "value"
64func (o *Option) keyValuePair(embedded bool) (cols []aligned) {
65	equals := alignedEquals
66	name := o.Name
67	if embedded {
68		return append(cols, leftAligned(name), equals, leftAligned(o.Constant.String())) // numbers right, strings left? TODO
69	}
70	return append(cols, rightAligned(name), equals, rightAligned(o.Constant.String()))
71}
72
73// parse reads an Option body
74// ( ident | "(" fullIdent ")" ) { "." ident } "=" constant ";"
75func (o *Option) parse(p *Parser) error {
76	pos, tok, lit := p.nextIdentifier()
77	if tLEFTPAREN == tok {
78		pos, tok, lit = p.nextIdentifier()
79		if tok != tIDENT {
80			if !isKeyword(tok) {
81				return p.unexpected(lit, "option full identifier", o)
82			}
83		}
84		pos, tok, _ = p.next()
85		if tok != tRIGHTPAREN {
86			return p.unexpected(lit, "full identifier closing )", o)
87		}
88		o.Name = fmt.Sprintf("(%s)", lit)
89	} else {
90		// non full ident
91		if tIDENT != tok {
92			if !isKeyword(tok) {
93				return p.unexpected(lit, "option identifier", o)
94			}
95		}
96		o.Name = lit
97	}
98	pos, tok, lit = p.next()
99	if tDOT == tok {
100		// extend identifier
101		pos, tok, lit = p.nextIdentifier()
102		if tok != tIDENT {
103			return p.unexpected(lit, "option postfix identifier", o)
104		}
105		o.Name = fmt.Sprintf("%s.%s", o.Name, lit)
106		pos, tok, lit = p.next()
107	}
108	if tEQUALS != tok {
109		return p.unexpected(lit, "option constant =", o)
110	}
111	r := p.peekNonWhitespace()
112	if '{' == r {
113		p.next() // consume {
114		return o.parseAggregate(p)
115	}
116	// non aggregate
117	l := new(Literal)
118	l.Position = pos
119	if err := l.parse(p); err != nil {
120		return err
121	}
122	o.Constant = *l
123	return nil
124}
125
126// inlineComment is part of commentInliner.
127func (o *Option) inlineComment(c *Comment) {
128	o.InlineComment = c
129}
130
131// Accept dispatches the call to the visitor.
132func (o *Option) Accept(v Visitor) {
133	v.VisitOption(o)
134}
135
136// Doc is part of Documented
137func (o *Option) Doc() *Comment {
138	return o.Comment
139}
140
141// Literal represents intLit,floatLit,strLit or boolLit
142type Literal struct {
143	Position scanner.Position
144	Source   string
145	IsString bool
146}
147
148// String returns the source (if quoted then use double quote).
149func (l Literal) String() string {
150	var buf bytes.Buffer
151	if l.IsString {
152		buf.WriteRune('"')
153	}
154	buf.WriteString(l.Source)
155	if l.IsString {
156		buf.WriteRune('"')
157	}
158	return buf.String()
159}
160
161// parse expects to read a literal constant after =.
162func (l *Literal) parse(p *Parser) error {
163	pos, _, lit := p.next()
164	if "-" == lit {
165		// negative number
166		if err := l.parse(p); err != nil {
167			return err
168		}
169		// modify source and position
170		l.Position, l.Source = pos, "-"+l.Source
171		return nil
172	}
173	source := lit
174	isString := isString(lit)
175	if isString {
176		source = unQuote(source)
177	}
178	l.Position, l.Source, l.IsString = pos, source, isString
179	return nil
180}
181
182// NamedLiteral associates a name with a Literal
183type NamedLiteral struct {
184	*Literal
185	Name string
186}
187
188// parseAggregate reads options written using aggregate syntax
189func (o *Option) parseAggregate(p *Parser) error {
190	o.AggregatedConstants = []*NamedLiteral{}
191	for {
192		pos, tok, lit := p.next()
193		if tRIGHTSQUARE == tok {
194			p.nextPut(pos, tok, lit)
195			// caller has checked for open square ; will consume rightsquare, rightcurly and semicolon
196			return nil
197		}
198		if tRIGHTCURLY == tok {
199			continue
200		}
201		if tSEMICOLON == tok {
202			return nil
203		}
204		if tCOMMA == tok {
205			if len(o.AggregatedConstants) == 0 {
206				return p.unexpected(lit, "non-empty option aggregate key", o)
207			}
208			continue
209		}
210		if tIDENT != tok {
211			return p.unexpected(lit, "option aggregate key", o)
212		}
213		key := lit
214		pos, tok, lit = p.next()
215		if tCOLON != tok {
216			return p.unexpected(lit, "option aggregate key colon :", o)
217		}
218		l := new(Literal)
219		l.Position = pos
220		if err := l.parse(p); err != nil {
221			return err
222		}
223		o.AggregatedConstants = append(o.AggregatedConstants, &NamedLiteral{Name: key, Literal: l})
224	}
225}
226