162 lines
3.6 KiB
Go
162 lines
3.6 KiB
Go
|
package common
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
"text/scanner"
|
||
|
|
||
|
"github.com/graph-gophers/graphql-go/errors"
|
||
|
)
|
||
|
|
||
|
type syntaxError string
|
||
|
|
||
|
type Lexer struct {
|
||
|
sc *scanner.Scanner
|
||
|
next rune
|
||
|
descComment string
|
||
|
}
|
||
|
|
||
|
type Ident struct {
|
||
|
Name string
|
||
|
Loc errors.Location
|
||
|
}
|
||
|
|
||
|
func NewLexer(s string) *Lexer {
|
||
|
sc := &scanner.Scanner{
|
||
|
Mode: scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats | scanner.ScanStrings,
|
||
|
}
|
||
|
sc.Init(strings.NewReader(s))
|
||
|
|
||
|
return &Lexer{sc: sc}
|
||
|
}
|
||
|
|
||
|
func (l *Lexer) CatchSyntaxError(f func()) (errRes *errors.QueryError) {
|
||
|
defer func() {
|
||
|
if err := recover(); err != nil {
|
||
|
if err, ok := err.(syntaxError); ok {
|
||
|
errRes = errors.Errorf("syntax error: %s", err)
|
||
|
errRes.Locations = []errors.Location{l.Location()}
|
||
|
return
|
||
|
}
|
||
|
panic(err)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
f()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (l *Lexer) Peek() rune {
|
||
|
return l.next
|
||
|
}
|
||
|
|
||
|
// Consume whitespace and tokens equivalent to whitespace (e.g. commas and comments).
|
||
|
//
|
||
|
// Consumed comment characters will build the description for the next type or field encountered.
|
||
|
// The description is available from `DescComment()`, and will be reset every time `Consume()` is
|
||
|
// executed.
|
||
|
func (l *Lexer) Consume() {
|
||
|
l.descComment = ""
|
||
|
for {
|
||
|
l.next = l.sc.Scan()
|
||
|
|
||
|
if l.next == ',' {
|
||
|
// Similar to white space and line terminators, commas (',') are used to improve the
|
||
|
// legibility of source text and separate lexical tokens but are otherwise syntactically and
|
||
|
// semantically insignificant within GraphQL documents.
|
||
|
//
|
||
|
// http://facebook.github.io/graphql/draft/#sec-Insignificant-Commas
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if l.next == '#' {
|
||
|
// GraphQL source documents may contain single-line comments, starting with the '#' marker.
|
||
|
//
|
||
|
// A comment can contain any Unicode code point except `LineTerminator` so a comment always
|
||
|
// consists of all code points starting with the '#' character up to but not including the
|
||
|
// line terminator.
|
||
|
|
||
|
l.consumeComment()
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (l *Lexer) ConsumeIdent() string {
|
||
|
name := l.sc.TokenText()
|
||
|
l.ConsumeToken(scanner.Ident)
|
||
|
return name
|
||
|
}
|
||
|
|
||
|
func (l *Lexer) ConsumeIdentWithLoc() Ident {
|
||
|
loc := l.Location()
|
||
|
name := l.sc.TokenText()
|
||
|
l.ConsumeToken(scanner.Ident)
|
||
|
return Ident{name, loc}
|
||
|
}
|
||
|
|
||
|
func (l *Lexer) ConsumeKeyword(keyword string) {
|
||
|
if l.next != scanner.Ident || l.sc.TokenText() != keyword {
|
||
|
l.SyntaxError(fmt.Sprintf("unexpected %q, expecting %q", l.sc.TokenText(), keyword))
|
||
|
}
|
||
|
l.Consume()
|
||
|
}
|
||
|
|
||
|
func (l *Lexer) ConsumeLiteral() *BasicLit {
|
||
|
lit := &BasicLit{Type: l.next, Text: l.sc.TokenText()}
|
||
|
l.Consume()
|
||
|
return lit
|
||
|
}
|
||
|
|
||
|
func (l *Lexer) ConsumeToken(expected rune) {
|
||
|
if l.next != expected {
|
||
|
l.SyntaxError(fmt.Sprintf("unexpected %q, expecting %s", l.sc.TokenText(), scanner.TokenString(expected)))
|
||
|
}
|
||
|
l.Consume()
|
||
|
}
|
||
|
|
||
|
func (l *Lexer) DescComment() string {
|
||
|
return l.descComment
|
||
|
}
|
||
|
|
||
|
func (l *Lexer) SyntaxError(message string) {
|
||
|
panic(syntaxError(message))
|
||
|
}
|
||
|
|
||
|
func (l *Lexer) Location() errors.Location {
|
||
|
return errors.Location{
|
||
|
Line: l.sc.Line,
|
||
|
Column: l.sc.Column,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// consumeComment consumes all characters from `#` to the first encountered line terminator.
|
||
|
// The characters are appended to `l.descComment`.
|
||
|
func (l *Lexer) consumeComment() {
|
||
|
if l.next != '#' {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// TODO: count and trim whitespace so we can dedent any following lines.
|
||
|
if l.sc.Peek() == ' ' {
|
||
|
l.sc.Next()
|
||
|
}
|
||
|
|
||
|
if l.descComment != "" {
|
||
|
// TODO: use a bytes.Buffer or strings.Builder instead of this.
|
||
|
l.descComment += "\n"
|
||
|
}
|
||
|
|
||
|
for {
|
||
|
next := l.sc.Next()
|
||
|
if next == '\r' || next == '\n' || next == scanner.EOF {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
// TODO: use a bytes.Buffer or strings.Build instead of this.
|
||
|
l.descComment += string(next)
|
||
|
}
|
||
|
}
|