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