1package common
2
3import (
4	"bytes"
5	"fmt"
6	"strconv"
7	"strings"
8	"text/scanner"
9
10	"github.com/graph-gophers/graphql-go/errors"
11)
12
13type syntaxError string
14
15type Lexer struct {
16	sc                    *scanner.Scanner
17	next                  rune
18	comment               bytes.Buffer
19	useStringDescriptions bool
20}
21
22type Ident struct {
23	Name string
24	Loc  errors.Location
25}
26
27func NewLexer(s string, useStringDescriptions bool) *Lexer {
28	sc := &scanner.Scanner{
29		Mode: scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats | scanner.ScanStrings,
30	}
31	sc.Init(strings.NewReader(s))
32
33
34	l := Lexer{sc: sc, useStringDescriptions: useStringDescriptions}
35	l.sc.Error = l.CatchScannerError
36
37	return &l
38}
39
40func (l *Lexer) CatchSyntaxError(f func()) (errRes *errors.QueryError) {
41	defer func() {
42		if err := recover(); err != nil {
43			if err, ok := err.(syntaxError); ok {
44				errRes = errors.Errorf("syntax error: %s", err)
45				errRes.Locations = []errors.Location{l.Location()}
46				return
47			}
48			panic(err)
49		}
50	}()
51
52	f()
53	return
54}
55
56func (l *Lexer) Peek() rune {
57	return l.next
58}
59
60// ConsumeWhitespace consumes whitespace and tokens equivalent to whitespace (e.g. commas and comments).
61//
62// Consumed comment characters will build the description for the next type or field encountered.
63// The description is available from `DescComment()`, and will be reset every time `ConsumeWhitespace()` is
64// executed unless l.useStringDescriptions is set.
65func (l *Lexer) ConsumeWhitespace() {
66	l.comment.Reset()
67	for {
68		l.next = l.sc.Scan()
69
70		if l.next == ',' {
71			// Similar to white space and line terminators, commas (',') are used to improve the
72			// legibility of source text and separate lexical tokens but are otherwise syntactically and
73			// semantically insignificant within GraphQL documents.
74			//
75			// http://facebook.github.io/graphql/draft/#sec-Insignificant-Commas
76			continue
77		}
78
79		if l.next == '#' {
80			// GraphQL source documents may contain single-line comments, starting with the '#' marker.
81			//
82			// A comment can contain any Unicode code point except `LineTerminator` so a comment always
83			// consists of all code points starting with the '#' character up to but not including the
84			// line terminator.
85			l.consumeComment()
86			continue
87		}
88
89		break
90	}
91}
92
93// consumeDescription optionally consumes a description based on the June 2018 graphql spec if any are present.
94//
95// Single quote strings are also single line. Triple quote strings can be multi-line. Triple quote strings
96// whitespace trimmed on both ends.
97// If a description is found, consume any following comments as well
98//
99// http://facebook.github.io/graphql/June2018/#sec-Descriptions
100func (l *Lexer) consumeDescription() string {
101	// If the next token is not a string, we don't consume it
102	if l.next != scanner.String {
103		return ""
104	}
105	// Triple quote string is an empty "string" followed by an open quote due to the way the parser treats strings as one token
106	var desc string
107	if l.sc.Peek() == '"' {
108		desc = l.consumeTripleQuoteComment()
109	} else {
110		desc = l.consumeStringComment()
111	}
112	l.ConsumeWhitespace()
113	return desc
114}
115
116func (l *Lexer) ConsumeIdent() string {
117	name := l.sc.TokenText()
118	l.ConsumeToken(scanner.Ident)
119	return name
120}
121
122func (l *Lexer) ConsumeIdentWithLoc() Ident {
123	loc := l.Location()
124	name := l.sc.TokenText()
125	l.ConsumeToken(scanner.Ident)
126	return Ident{name, loc}
127}
128
129func (l *Lexer) ConsumeKeyword(keyword string) {
130	if l.next != scanner.Ident || l.sc.TokenText() != keyword {
131		l.SyntaxError(fmt.Sprintf("unexpected %q, expecting %q", l.sc.TokenText(), keyword))
132	}
133	l.ConsumeWhitespace()
134}
135
136func (l *Lexer) ConsumeLiteral() *BasicLit {
137	lit := &BasicLit{Type: l.next, Text: l.sc.TokenText()}
138	l.ConsumeWhitespace()
139	return lit
140}
141
142func (l *Lexer) ConsumeToken(expected rune) {
143	if l.next != expected {
144		l.SyntaxError(fmt.Sprintf("unexpected %q, expecting %s", l.sc.TokenText(), scanner.TokenString(expected)))
145	}
146	l.ConsumeWhitespace()
147}
148
149func (l *Lexer) DescComment() string {
150	comment := l.comment.String()
151	desc := l.consumeDescription()
152	if l.useStringDescriptions {
153		return desc
154	}
155	return comment
156}
157
158func (l *Lexer) SyntaxError(message string) {
159	panic(syntaxError(message))
160}
161
162func (l *Lexer) Location() errors.Location {
163	return errors.Location{
164		Line:   l.sc.Line,
165		Column: l.sc.Column,
166	}
167}
168
169func (l *Lexer) consumeTripleQuoteComment() string {
170	l.next = l.sc.Next()
171	if l.next != '"' {
172		panic("consumeTripleQuoteComment used in wrong context: no third quote?")
173	}
174
175	var buf bytes.Buffer
176	var numQuotes int
177	for {
178		l.next = l.sc.Next()
179		if l.next == '"' {
180			numQuotes++
181		} else {
182			numQuotes = 0
183		}
184		buf.WriteRune(l.next)
185		if numQuotes == 3 || l.next == scanner.EOF {
186			break
187		}
188	}
189	val := buf.String()
190	val = val[:len(val)-numQuotes]
191	return blockString(val)
192}
193
194func (l *Lexer) consumeStringComment() string {
195	val, err := strconv.Unquote(l.sc.TokenText())
196	if err != nil {
197		panic(err)
198	}
199	return val
200}
201
202// consumeComment consumes all characters from `#` to the first encountered line terminator.
203// The characters are appended to `l.comment`.
204func (l *Lexer) consumeComment() {
205	if l.next != '#' {
206		panic("consumeComment used in wrong context")
207	}
208
209	// TODO: count and trim whitespace so we can dedent any following lines.
210	if l.sc.Peek() == ' ' {
211		l.sc.Next()
212	}
213
214	if l.comment.Len() > 0 {
215		l.comment.WriteRune('\n')
216	}
217
218	for {
219		next := l.sc.Next()
220		if next == '\r' || next == '\n' || next == scanner.EOF {
221			break
222		}
223		l.comment.WriteRune(next)
224	}
225}
226
227func (l *Lexer) CatchScannerError(s *scanner.Scanner, msg string) {
228	l.SyntaxError(msg)
229}
230