diff options
author | Tianon Gravi <admwiggin@gmail.com> | 2015-01-15 11:54:00 -0700 |
---|---|---|
committer | Tianon Gravi <admwiggin@gmail.com> | 2015-01-15 11:54:00 -0700 |
commit | f154da9e12608589e8d5f0508f908a0c3e88a1bb (patch) | |
tree | f8255d51e10c6f1e0ed69702200b966c9556a431 /src/text/scanner | |
parent | 8d8329ed5dfb9622c82a9fbec6fd99a580f9c9f6 (diff) | |
download | golang-upstream/1.4.tar.gz |
Imported Upstream version 1.4upstream/1.4
Diffstat (limited to 'src/text/scanner')
-rw-r--r-- | src/text/scanner/scanner.go | 693 | ||||
-rw-r--r-- | src/text/scanner/scanner_test.go | 618 |
2 files changed, 1311 insertions, 0 deletions
diff --git a/src/text/scanner/scanner.go b/src/text/scanner/scanner.go new file mode 100644 index 000000000..5199ee4fc --- /dev/null +++ b/src/text/scanner/scanner.go @@ -0,0 +1,693 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package scanner provides a scanner and tokenizer for UTF-8-encoded text. +// It takes an io.Reader providing the source, which then can be tokenized +// through repeated calls to the Scan function. For compatibility with +// existing tools, the NUL character is not allowed. If the first character +// in the source is a UTF-8 encoded byte order mark (BOM), it is discarded. +// +// By default, a Scanner skips white space and Go comments and recognizes all +// literals as defined by the Go language specification. It may be +// customized to recognize only a subset of those literals and to recognize +// different identifier and white space characters. +// +// Basic usage pattern: +// +// var s scanner.Scanner +// s.Init(src) +// tok := s.Scan() +// for tok != scanner.EOF { +// // do something with tok +// tok = s.Scan() +// } +// +package scanner + +import ( + "bytes" + "fmt" + "io" + "os" + "unicode" + "unicode/utf8" +) + +// A source position is represented by a Position value. +// A position is valid if Line > 0. +type Position struct { + Filename string // filename, if any + Offset int // byte offset, starting at 0 + Line int // line number, starting at 1 + Column int // column number, starting at 1 (character count per line) +} + +// IsValid returns true if the position is valid. +func (pos *Position) IsValid() bool { return pos.Line > 0 } + +func (pos Position) String() string { + s := pos.Filename + if pos.IsValid() { + if s != "" { + s += ":" + } + s += fmt.Sprintf("%d:%d", pos.Line, pos.Column) + } + if s == "" { + s = "???" + } + return s +} + +// Predefined mode bits to control recognition of tokens. For instance, +// to configure a Scanner such that it only recognizes (Go) identifiers, +// integers, and skips comments, set the Scanner's Mode field to: +// +// ScanIdents | ScanInts | SkipComments +// +// With the exceptions of comments, which are skipped if SkipComments is +// set, unrecognized tokens are not ignored. Instead, the scanner simply +// returns the respective individual characters (or possibly sub-tokens). +// For instance, if the mode is ScanIdents (not ScanStrings), the string +// "foo" is scanned as the token sequence '"' Ident '"'. +// +const ( + ScanIdents = 1 << -Ident + ScanInts = 1 << -Int + ScanFloats = 1 << -Float // includes Ints + ScanChars = 1 << -Char + ScanStrings = 1 << -String + ScanRawStrings = 1 << -RawString + ScanComments = 1 << -Comment + SkipComments = 1 << -skipComment // if set with ScanComments, comments become white space + GoTokens = ScanIdents | ScanFloats | ScanChars | ScanStrings | ScanRawStrings | ScanComments | SkipComments +) + +// The result of Scan is one of the following tokens or a Unicode character. +const ( + EOF = -(iota + 1) + Ident + Int + Float + Char + String + RawString + Comment + skipComment +) + +var tokenString = map[rune]string{ + EOF: "EOF", + Ident: "Ident", + Int: "Int", + Float: "Float", + Char: "Char", + String: "String", + RawString: "RawString", + Comment: "Comment", +} + +// TokenString returns a printable string for a token or Unicode character. +func TokenString(tok rune) string { + if s, found := tokenString[tok]; found { + return s + } + return fmt.Sprintf("%q", string(tok)) +} + +// GoWhitespace is the default value for the Scanner's Whitespace field. +// Its value selects Go's white space characters. +const GoWhitespace = 1<<'\t' | 1<<'\n' | 1<<'\r' | 1<<' ' + +const bufLen = 1024 // at least utf8.UTFMax + +// A Scanner implements reading of Unicode characters and tokens from an io.Reader. +type Scanner struct { + // Input + src io.Reader + + // Source buffer + srcBuf [bufLen + 1]byte // +1 for sentinel for common case of s.next() + srcPos int // reading position (srcBuf index) + srcEnd int // source end (srcBuf index) + + // Source position + srcBufOffset int // byte offset of srcBuf[0] in source + line int // line count + column int // character count + lastLineLen int // length of last line in characters (for correct column reporting) + lastCharLen int // length of last character in bytes + + // Token text buffer + // Typically, token text is stored completely in srcBuf, but in general + // the token text's head may be buffered in tokBuf while the token text's + // tail is stored in srcBuf. + tokBuf bytes.Buffer // token text head that is not in srcBuf anymore + tokPos int // token text tail position (srcBuf index); valid if >= 0 + tokEnd int // token text tail end (srcBuf index) + + // One character look-ahead + ch rune // character before current srcPos + + // Error is called for each error encountered. If no Error + // function is set, the error is reported to os.Stderr. + Error func(s *Scanner, msg string) + + // ErrorCount is incremented by one for each error encountered. + ErrorCount int + + // The Mode field controls which tokens are recognized. For instance, + // to recognize Ints, set the ScanInts bit in Mode. The field may be + // changed at any time. + Mode uint + + // The Whitespace field controls which characters are recognized + // as white space. To recognize a character ch <= ' ' as white space, + // set the ch'th bit in Whitespace (the Scanner's behavior is undefined + // for values ch > ' '). The field may be changed at any time. + Whitespace uint64 + + // IsIdentRune is a predicate controlling the characters accepted + // as the ith rune in an identifier. The set of valid characters + // must not intersect with the set of white space characters. + // If no IsIdentRune function is set, regular Go identifiers are + // accepted instead. The field may be changed at any time. + IsIdentRune func(ch rune, i int) bool + + // Start position of most recently scanned token; set by Scan. + // Calling Init or Next invalidates the position (Line == 0). + // The Filename field is always left untouched by the Scanner. + // If an error is reported (via Error) and Position is invalid, + // the scanner is not inside a token. Call Pos to obtain an error + // position in that case. + Position +} + +// Init initializes a Scanner with a new source and returns s. +// Error is set to nil, ErrorCount is set to 0, Mode is set to GoTokens, +// and Whitespace is set to GoWhitespace. +func (s *Scanner) Init(src io.Reader) *Scanner { + s.src = src + + // initialize source buffer + // (the first call to next() will fill it by calling src.Read) + s.srcBuf[0] = utf8.RuneSelf // sentinel + s.srcPos = 0 + s.srcEnd = 0 + + // initialize source position + s.srcBufOffset = 0 + s.line = 1 + s.column = 0 + s.lastLineLen = 0 + s.lastCharLen = 0 + + // initialize token text buffer + // (required for first call to next()). + s.tokPos = -1 + + // initialize one character look-ahead + s.ch = -1 // no char read yet + + // initialize public fields + s.Error = nil + s.ErrorCount = 0 + s.Mode = GoTokens + s.Whitespace = GoWhitespace + s.Line = 0 // invalidate token position + + return s +} + +// next reads and returns the next Unicode character. It is designed such +// that only a minimal amount of work needs to be done in the common ASCII +// case (one test to check for both ASCII and end-of-buffer, and one test +// to check for newlines). +func (s *Scanner) next() rune { + ch, width := rune(s.srcBuf[s.srcPos]), 1 + + if ch >= utf8.RuneSelf { + // uncommon case: not ASCII or not enough bytes + for s.srcPos+utf8.UTFMax > s.srcEnd && !utf8.FullRune(s.srcBuf[s.srcPos:s.srcEnd]) { + // not enough bytes: read some more, but first + // save away token text if any + if s.tokPos >= 0 { + s.tokBuf.Write(s.srcBuf[s.tokPos:s.srcPos]) + s.tokPos = 0 + // s.tokEnd is set by Scan() + } + // move unread bytes to beginning of buffer + copy(s.srcBuf[0:], s.srcBuf[s.srcPos:s.srcEnd]) + s.srcBufOffset += s.srcPos + // read more bytes + // (an io.Reader must return io.EOF when it reaches + // the end of what it is reading - simply returning + // n == 0 will make this loop retry forever; but the + // error is in the reader implementation in that case) + i := s.srcEnd - s.srcPos + n, err := s.src.Read(s.srcBuf[i:bufLen]) + s.srcPos = 0 + s.srcEnd = i + n + s.srcBuf[s.srcEnd] = utf8.RuneSelf // sentinel + if err != nil { + if err != io.EOF { + s.error(err.Error()) + } + if s.srcEnd == 0 { + if s.lastCharLen > 0 { + // previous character was not EOF + s.column++ + } + s.lastCharLen = 0 + return EOF + } + // If err == EOF, we won't be getting more + // bytes; break to avoid infinite loop. If + // err is something else, we don't know if + // we can get more bytes; thus also break. + break + } + } + // at least one byte + ch = rune(s.srcBuf[s.srcPos]) + if ch >= utf8.RuneSelf { + // uncommon case: not ASCII + ch, width = utf8.DecodeRune(s.srcBuf[s.srcPos:s.srcEnd]) + if ch == utf8.RuneError && width == 1 { + // advance for correct error position + s.srcPos += width + s.lastCharLen = width + s.column++ + s.error("illegal UTF-8 encoding") + return ch + } + } + } + + // advance + s.srcPos += width + s.lastCharLen = width + s.column++ + + // special situations + switch ch { + case 0: + // for compatibility with other tools + s.error("illegal character NUL") + case '\n': + s.line++ + s.lastLineLen = s.column + s.column = 0 + } + + return ch +} + +// Next reads and returns the next Unicode character. +// It returns EOF at the end of the source. It reports +// a read error by calling s.Error, if not nil; otherwise +// it prints an error message to os.Stderr. Next does not +// update the Scanner's Position field; use Pos() to +// get the current position. +func (s *Scanner) Next() rune { + s.tokPos = -1 // don't collect token text + s.Line = 0 // invalidate token position + ch := s.Peek() + s.ch = s.next() + return ch +} + +// Peek returns the next Unicode character in the source without advancing +// the scanner. It returns EOF if the scanner's position is at the last +// character of the source. +func (s *Scanner) Peek() rune { + if s.ch < 0 { + // this code is only run for the very first character + s.ch = s.next() + if s.ch == '\uFEFF' { + s.ch = s.next() // ignore BOM + } + } + return s.ch +} + +func (s *Scanner) error(msg string) { + s.ErrorCount++ + if s.Error != nil { + s.Error(s, msg) + return + } + pos := s.Position + if !pos.IsValid() { + pos = s.Pos() + } + fmt.Fprintf(os.Stderr, "%s: %s\n", pos, msg) +} + +func (s *Scanner) isIdentRune(ch rune, i int) bool { + if s.IsIdentRune != nil { + return s.IsIdentRune(ch, i) + } + return ch == '_' || unicode.IsLetter(ch) || unicode.IsDigit(ch) && i > 0 +} + +func (s *Scanner) scanIdentifier() rune { + // we know the zero'th rune is OK; start scanning at the next one + ch := s.next() + for i := 1; s.isIdentRune(ch, i); i++ { + ch = s.next() + } + return ch +} + +func digitVal(ch rune) int { + switch { + case '0' <= ch && ch <= '9': + return int(ch - '0') + case 'a' <= ch && ch <= 'f': + return int(ch - 'a' + 10) + case 'A' <= ch && ch <= 'F': + return int(ch - 'A' + 10) + } + return 16 // larger than any legal digit val +} + +func isDecimal(ch rune) bool { return '0' <= ch && ch <= '9' } + +func (s *Scanner) scanMantissa(ch rune) rune { + for isDecimal(ch) { + ch = s.next() + } + return ch +} + +func (s *Scanner) scanFraction(ch rune) rune { + if ch == '.' { + ch = s.scanMantissa(s.next()) + } + return ch +} + +func (s *Scanner) scanExponent(ch rune) rune { + if ch == 'e' || ch == 'E' { + ch = s.next() + if ch == '-' || ch == '+' { + ch = s.next() + } + ch = s.scanMantissa(ch) + } + return ch +} + +func (s *Scanner) scanNumber(ch rune) (rune, rune) { + // isDecimal(ch) + if ch == '0' { + // int or float + ch = s.next() + if ch == 'x' || ch == 'X' { + // hexadecimal int + ch = s.next() + hasMantissa := false + for digitVal(ch) < 16 { + ch = s.next() + hasMantissa = true + } + if !hasMantissa { + s.error("illegal hexadecimal number") + } + } else { + // octal int or float + has8or9 := false + for isDecimal(ch) { + if ch > '7' { + has8or9 = true + } + ch = s.next() + } + if s.Mode&ScanFloats != 0 && (ch == '.' || ch == 'e' || ch == 'E') { + // float + ch = s.scanFraction(ch) + ch = s.scanExponent(ch) + return Float, ch + } + // octal int + if has8or9 { + s.error("illegal octal number") + } + } + return Int, ch + } + // decimal int or float + ch = s.scanMantissa(ch) + if s.Mode&ScanFloats != 0 && (ch == '.' || ch == 'e' || ch == 'E') { + // float + ch = s.scanFraction(ch) + ch = s.scanExponent(ch) + return Float, ch + } + return Int, ch +} + +func (s *Scanner) scanDigits(ch rune, base, n int) rune { + for n > 0 && digitVal(ch) < base { + ch = s.next() + n-- + } + if n > 0 { + s.error("illegal char escape") + } + return ch +} + +func (s *Scanner) scanEscape(quote rune) rune { + ch := s.next() // read character after '/' + switch ch { + case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', quote: + // nothing to do + ch = s.next() + case '0', '1', '2', '3', '4', '5', '6', '7': + ch = s.scanDigits(ch, 8, 3) + case 'x': + ch = s.scanDigits(s.next(), 16, 2) + case 'u': + ch = s.scanDigits(s.next(), 16, 4) + case 'U': + ch = s.scanDigits(s.next(), 16, 8) + default: + s.error("illegal char escape") + } + return ch +} + +func (s *Scanner) scanString(quote rune) (n int) { + ch := s.next() // read character after quote + for ch != quote { + if ch == '\n' || ch < 0 { + s.error("literal not terminated") + return + } + if ch == '\\' { + ch = s.scanEscape(quote) + } else { + ch = s.next() + } + n++ + } + return +} + +func (s *Scanner) scanRawString() { + ch := s.next() // read character after '`' + for ch != '`' { + if ch < 0 { + s.error("literal not terminated") + return + } + ch = s.next() + } +} + +func (s *Scanner) scanChar() { + if s.scanString('\'') != 1 { + s.error("illegal char literal") + } +} + +func (s *Scanner) scanComment(ch rune) rune { + // ch == '/' || ch == '*' + if ch == '/' { + // line comment + ch = s.next() // read character after "//" + for ch != '\n' && ch >= 0 { + ch = s.next() + } + return ch + } + + // general comment + ch = s.next() // read character after "/*" + for { + if ch < 0 { + s.error("comment not terminated") + break + } + ch0 := ch + ch = s.next() + if ch0 == '*' && ch == '/' { + ch = s.next() + break + } + } + return ch +} + +// Scan reads the next token or Unicode character from source and returns it. +// It only recognizes tokens t for which the respective Mode bit (1<<-t) is set. +// It returns EOF at the end of the source. It reports scanner errors (read and +// token errors) by calling s.Error, if not nil; otherwise it prints an error +// message to os.Stderr. +func (s *Scanner) Scan() rune { + ch := s.Peek() + + // reset token text position + s.tokPos = -1 + s.Line = 0 + +redo: + // skip white space + for s.Whitespace&(1<<uint(ch)) != 0 { + ch = s.next() + } + + // start collecting token text + s.tokBuf.Reset() + s.tokPos = s.srcPos - s.lastCharLen + + // set token position + // (this is a slightly optimized version of the code in Pos()) + s.Offset = s.srcBufOffset + s.tokPos + if s.column > 0 { + // common case: last character was not a '\n' + s.Line = s.line + s.Column = s.column + } else { + // last character was a '\n' + // (we cannot be at the beginning of the source + // since we have called next() at least once) + s.Line = s.line - 1 + s.Column = s.lastLineLen + } + + // determine token value + tok := ch + switch { + case s.isIdentRune(ch, 0): + if s.Mode&ScanIdents != 0 { + tok = Ident + ch = s.scanIdentifier() + } else { + ch = s.next() + } + case isDecimal(ch): + if s.Mode&(ScanInts|ScanFloats) != 0 { + tok, ch = s.scanNumber(ch) + } else { + ch = s.next() + } + default: + switch ch { + case '"': + if s.Mode&ScanStrings != 0 { + s.scanString('"') + tok = String + } + ch = s.next() + case '\'': + if s.Mode&ScanChars != 0 { + s.scanChar() + tok = Char + } + ch = s.next() + case '.': + ch = s.next() + if isDecimal(ch) && s.Mode&ScanFloats != 0 { + tok = Float + ch = s.scanMantissa(ch) + ch = s.scanExponent(ch) + } + case '/': + ch = s.next() + if (ch == '/' || ch == '*') && s.Mode&ScanComments != 0 { + if s.Mode&SkipComments != 0 { + s.tokPos = -1 // don't collect token text + ch = s.scanComment(ch) + goto redo + } + ch = s.scanComment(ch) + tok = Comment + } + case '`': + if s.Mode&ScanRawStrings != 0 { + s.scanRawString() + tok = String + } + ch = s.next() + default: + ch = s.next() + } + } + + // end of token text + s.tokEnd = s.srcPos - s.lastCharLen + + s.ch = ch + return tok +} + +// Pos returns the position of the character immediately after +// the character or token returned by the last call to Next or Scan. +func (s *Scanner) Pos() (pos Position) { + pos.Filename = s.Filename + pos.Offset = s.srcBufOffset + s.srcPos - s.lastCharLen + switch { + case s.column > 0: + // common case: last character was not a '\n' + pos.Line = s.line + pos.Column = s.column + case s.lastLineLen > 0: + // last character was a '\n' + pos.Line = s.line - 1 + pos.Column = s.lastLineLen + default: + // at the beginning of the source + pos.Line = 1 + pos.Column = 1 + } + return +} + +// TokenText returns the string corresponding to the most recently scanned token. +// Valid after calling Scan(). +func (s *Scanner) TokenText() string { + if s.tokPos < 0 { + // no token text + return "" + } + + if s.tokEnd < 0 { + // if EOF was reached, s.tokEnd is set to -1 (s.srcPos == 0) + s.tokEnd = s.tokPos + } + + if s.tokBuf.Len() == 0 { + // common case: the entire token text is still in srcBuf + return string(s.srcBuf[s.tokPos:s.tokEnd]) + } + + // part of the token text was saved in tokBuf: save the rest in + // tokBuf as well and return its content + s.tokBuf.Write(s.srcBuf[s.tokPos:s.tokEnd]) + s.tokPos = s.tokEnd // ensure idempotency of TokenText() call + return s.tokBuf.String() +} diff --git a/src/text/scanner/scanner_test.go b/src/text/scanner/scanner_test.go new file mode 100644 index 000000000..702fac2b1 --- /dev/null +++ b/src/text/scanner/scanner_test.go @@ -0,0 +1,618 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package scanner + +import ( + "bytes" + "fmt" + "io" + "strings" + "testing" + "unicode/utf8" +) + +// A StringReader delivers its data one string segment at a time via Read. +type StringReader struct { + data []string + step int +} + +func (r *StringReader) Read(p []byte) (n int, err error) { + if r.step < len(r.data) { + s := r.data[r.step] + n = copy(p, s) + r.step++ + } else { + err = io.EOF + } + return +} + +func readRuneSegments(t *testing.T, segments []string) { + got := "" + want := strings.Join(segments, "") + s := new(Scanner).Init(&StringReader{data: segments}) + for { + ch := s.Next() + if ch == EOF { + break + } + got += string(ch) + } + if got != want { + t.Errorf("segments=%v got=%s want=%s", segments, got, want) + } +} + +var segmentList = [][]string{ + {}, + {""}, + {"日", "本語"}, + {"\u65e5", "\u672c", "\u8a9e"}, + {"\U000065e5", " ", "\U0000672c", "\U00008a9e"}, + {"\xe6", "\x97\xa5\xe6", "\x9c\xac\xe8\xaa\x9e"}, + {"Hello", ", ", "World", "!"}, + {"Hello", ", ", "", "World", "!"}, +} + +func TestNext(t *testing.T) { + for _, s := range segmentList { + readRuneSegments(t, s) + } +} + +type token struct { + tok rune + text string +} + +var f100 = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + +var tokenList = []token{ + {Comment, "// line comments"}, + {Comment, "//"}, + {Comment, "////"}, + {Comment, "// comment"}, + {Comment, "// /* comment */"}, + {Comment, "// // comment //"}, + {Comment, "//" + f100}, + + {Comment, "// general comments"}, + {Comment, "/**/"}, + {Comment, "/***/"}, + {Comment, "/* comment */"}, + {Comment, "/* // comment */"}, + {Comment, "/* /* comment */"}, + {Comment, "/*\n comment\n*/"}, + {Comment, "/*" + f100 + "*/"}, + + {Comment, "// identifiers"}, + {Ident, "a"}, + {Ident, "a0"}, + {Ident, "foobar"}, + {Ident, "abc123"}, + {Ident, "LGTM"}, + {Ident, "_"}, + {Ident, "_abc123"}, + {Ident, "abc123_"}, + {Ident, "_abc_123_"}, + {Ident, "_äöü"}, + {Ident, "_本"}, + {Ident, "äöü"}, + {Ident, "本"}, + {Ident, "a۰۱۸"}, + {Ident, "foo६४"}, + {Ident, "bar9876"}, + {Ident, f100}, + + {Comment, "// decimal ints"}, + {Int, "0"}, + {Int, "1"}, + {Int, "9"}, + {Int, "42"}, + {Int, "1234567890"}, + + {Comment, "// octal ints"}, + {Int, "00"}, + {Int, "01"}, + {Int, "07"}, + {Int, "042"}, + {Int, "01234567"}, + + {Comment, "// hexadecimal ints"}, + {Int, "0x0"}, + {Int, "0x1"}, + {Int, "0xf"}, + {Int, "0x42"}, + {Int, "0x123456789abcDEF"}, + {Int, "0x" + f100}, + {Int, "0X0"}, + {Int, "0X1"}, + {Int, "0XF"}, + {Int, "0X42"}, + {Int, "0X123456789abcDEF"}, + {Int, "0X" + f100}, + + {Comment, "// floats"}, + {Float, "0."}, + {Float, "1."}, + {Float, "42."}, + {Float, "01234567890."}, + {Float, ".0"}, + {Float, ".1"}, + {Float, ".42"}, + {Float, ".0123456789"}, + {Float, "0.0"}, + {Float, "1.0"}, + {Float, "42.0"}, + {Float, "01234567890.0"}, + {Float, "0e0"}, + {Float, "1e0"}, + {Float, "42e0"}, + {Float, "01234567890e0"}, + {Float, "0E0"}, + {Float, "1E0"}, + {Float, "42E0"}, + {Float, "01234567890E0"}, + {Float, "0e+10"}, + {Float, "1e-10"}, + {Float, "42e+10"}, + {Float, "01234567890e-10"}, + {Float, "0E+10"}, + {Float, "1E-10"}, + {Float, "42E+10"}, + {Float, "01234567890E-10"}, + + {Comment, "// chars"}, + {Char, `' '`}, + {Char, `'a'`}, + {Char, `'本'`}, + {Char, `'\a'`}, + {Char, `'\b'`}, + {Char, `'\f'`}, + {Char, `'\n'`}, + {Char, `'\r'`}, + {Char, `'\t'`}, + {Char, `'\v'`}, + {Char, `'\''`}, + {Char, `'\000'`}, + {Char, `'\777'`}, + {Char, `'\x00'`}, + {Char, `'\xff'`}, + {Char, `'\u0000'`}, + {Char, `'\ufA16'`}, + {Char, `'\U00000000'`}, + {Char, `'\U0000ffAB'`}, + + {Comment, "// strings"}, + {String, `" "`}, + {String, `"a"`}, + {String, `"本"`}, + {String, `"\a"`}, + {String, `"\b"`}, + {String, `"\f"`}, + {String, `"\n"`}, + {String, `"\r"`}, + {String, `"\t"`}, + {String, `"\v"`}, + {String, `"\""`}, + {String, `"\000"`}, + {String, `"\777"`}, + {String, `"\x00"`}, + {String, `"\xff"`}, + {String, `"\u0000"`}, + {String, `"\ufA16"`}, + {String, `"\U00000000"`}, + {String, `"\U0000ffAB"`}, + {String, `"` + f100 + `"`}, + + {Comment, "// raw strings"}, + {String, "``"}, + {String, "`\\`"}, + {String, "`" + "\n\n/* foobar */\n\n" + "`"}, + {String, "`" + f100 + "`"}, + + {Comment, "// individual characters"}, + // NUL character is not allowed + {'\x01', "\x01"}, + {' ' - 1, string(' ' - 1)}, + {'+', "+"}, + {'/', "/"}, + {'.', "."}, + {'~', "~"}, + {'(', "("}, +} + +func makeSource(pattern string) *bytes.Buffer { + var buf bytes.Buffer + for _, k := range tokenList { + fmt.Fprintf(&buf, pattern, k.text) + } + return &buf +} + +func checkTok(t *testing.T, s *Scanner, line int, got, want rune, text string) { + if got != want { + t.Fatalf("tok = %s, want %s for %q", TokenString(got), TokenString(want), text) + } + if s.Line != line { + t.Errorf("line = %d, want %d for %q", s.Line, line, text) + } + stext := s.TokenText() + if stext != text { + t.Errorf("text = %q, want %q", stext, text) + } else { + // check idempotency of TokenText() call + stext = s.TokenText() + if stext != text { + t.Errorf("text = %q, want %q (idempotency check)", stext, text) + } + } +} + +func countNewlines(s string) int { + n := 0 + for _, ch := range s { + if ch == '\n' { + n++ + } + } + return n +} + +func testScan(t *testing.T, mode uint) { + s := new(Scanner).Init(makeSource(" \t%s\n")) + s.Mode = mode + tok := s.Scan() + line := 1 + for _, k := range tokenList { + if mode&SkipComments == 0 || k.tok != Comment { + checkTok(t, s, line, tok, k.tok, k.text) + tok = s.Scan() + } + line += countNewlines(k.text) + 1 // each token is on a new line + } + checkTok(t, s, line, tok, EOF, "") +} + +func TestScan(t *testing.T) { + testScan(t, GoTokens) + testScan(t, GoTokens&^SkipComments) +} + +func TestPosition(t *testing.T) { + src := makeSource("\t\t\t\t%s\n") + s := new(Scanner).Init(src) + s.Mode = GoTokens &^ SkipComments + s.Scan() + pos := Position{"", 4, 1, 5} + for _, k := range tokenList { + if s.Offset != pos.Offset { + t.Errorf("offset = %d, want %d for %q", s.Offset, pos.Offset, k.text) + } + if s.Line != pos.Line { + t.Errorf("line = %d, want %d for %q", s.Line, pos.Line, k.text) + } + if s.Column != pos.Column { + t.Errorf("column = %d, want %d for %q", s.Column, pos.Column, k.text) + } + pos.Offset += 4 + len(k.text) + 1 // 4 tabs + token bytes + newline + pos.Line += countNewlines(k.text) + 1 // each token is on a new line + s.Scan() + } + // make sure there were no token-internal errors reported by scanner + if s.ErrorCount != 0 { + t.Errorf("%d errors", s.ErrorCount) + } +} + +func TestScanZeroMode(t *testing.T) { + src := makeSource("%s\n") + str := src.String() + s := new(Scanner).Init(src) + s.Mode = 0 // don't recognize any token classes + s.Whitespace = 0 // don't skip any whitespace + tok := s.Scan() + for i, ch := range str { + if tok != ch { + t.Fatalf("%d. tok = %s, want %s", i, TokenString(tok), TokenString(ch)) + } + tok = s.Scan() + } + if tok != EOF { + t.Fatalf("tok = %s, want EOF", TokenString(tok)) + } + if s.ErrorCount != 0 { + t.Errorf("%d errors", s.ErrorCount) + } +} + +func testScanSelectedMode(t *testing.T, mode uint, class rune) { + src := makeSource("%s\n") + s := new(Scanner).Init(src) + s.Mode = mode + tok := s.Scan() + for tok != EOF { + if tok < 0 && tok != class { + t.Fatalf("tok = %s, want %s", TokenString(tok), TokenString(class)) + } + tok = s.Scan() + } + if s.ErrorCount != 0 { + t.Errorf("%d errors", s.ErrorCount) + } +} + +func TestScanSelectedMask(t *testing.T) { + testScanSelectedMode(t, 0, 0) + testScanSelectedMode(t, ScanIdents, Ident) + // Don't test ScanInts and ScanNumbers since some parts of + // the floats in the source look like (illegal) octal ints + // and ScanNumbers may return either Int or Float. + testScanSelectedMode(t, ScanChars, Char) + testScanSelectedMode(t, ScanStrings, String) + testScanSelectedMode(t, SkipComments, 0) + testScanSelectedMode(t, ScanComments, Comment) +} + +func TestScanCustomIdent(t *testing.T) { + const src = "faab12345 a12b123 a12 3b" + s := new(Scanner).Init(strings.NewReader(src)) + // ident = ( 'a' | 'b' ) { digit } . + // digit = '0' .. '3' . + // with a maximum length of 4 + s.IsIdentRune = func(ch rune, i int) bool { + return i == 0 && (ch == 'a' || ch == 'b') || 0 < i && i < 4 && '0' <= ch && ch <= '3' + } + checkTok(t, s, 1, s.Scan(), 'f', "f") + checkTok(t, s, 1, s.Scan(), Ident, "a") + checkTok(t, s, 1, s.Scan(), Ident, "a") + checkTok(t, s, 1, s.Scan(), Ident, "b123") + checkTok(t, s, 1, s.Scan(), Int, "45") + checkTok(t, s, 1, s.Scan(), Ident, "a12") + checkTok(t, s, 1, s.Scan(), Ident, "b123") + checkTok(t, s, 1, s.Scan(), Ident, "a12") + checkTok(t, s, 1, s.Scan(), Int, "3") + checkTok(t, s, 1, s.Scan(), Ident, "b") + checkTok(t, s, 1, s.Scan(), EOF, "") +} + +func TestScanNext(t *testing.T) { + const BOM = '\uFEFF' + BOMs := string(BOM) + s := new(Scanner).Init(strings.NewReader(BOMs + "if a == bcd /* com" + BOMs + "ment */ {\n\ta += c\n}" + BOMs + "// line comment ending in eof")) + checkTok(t, s, 1, s.Scan(), Ident, "if") // the first BOM is ignored + checkTok(t, s, 1, s.Scan(), Ident, "a") + checkTok(t, s, 1, s.Scan(), '=', "=") + checkTok(t, s, 0, s.Next(), '=', "") + checkTok(t, s, 0, s.Next(), ' ', "") + checkTok(t, s, 0, s.Next(), 'b', "") + checkTok(t, s, 1, s.Scan(), Ident, "cd") + checkTok(t, s, 1, s.Scan(), '{', "{") + checkTok(t, s, 2, s.Scan(), Ident, "a") + checkTok(t, s, 2, s.Scan(), '+', "+") + checkTok(t, s, 0, s.Next(), '=', "") + checkTok(t, s, 2, s.Scan(), Ident, "c") + checkTok(t, s, 3, s.Scan(), '}', "}") + checkTok(t, s, 3, s.Scan(), BOM, BOMs) + checkTok(t, s, 3, s.Scan(), -1, "") + if s.ErrorCount != 0 { + t.Errorf("%d errors", s.ErrorCount) + } +} + +func TestScanWhitespace(t *testing.T) { + var buf bytes.Buffer + var ws uint64 + // start at 1, NUL character is not allowed + for ch := byte(1); ch < ' '; ch++ { + buf.WriteByte(ch) + ws |= 1 << ch + } + const orig = 'x' + buf.WriteByte(orig) + + s := new(Scanner).Init(&buf) + s.Mode = 0 + s.Whitespace = ws + tok := s.Scan() + if tok != orig { + t.Errorf("tok = %s, want %s", TokenString(tok), TokenString(orig)) + } +} + +func testError(t *testing.T, src, pos, msg string, tok rune) { + s := new(Scanner).Init(strings.NewReader(src)) + errorCalled := false + s.Error = func(s *Scanner, m string) { + if !errorCalled { + // only look at first error + if p := s.Pos().String(); p != pos { + t.Errorf("pos = %q, want %q for %q", p, pos, src) + } + if m != msg { + t.Errorf("msg = %q, want %q for %q", m, msg, src) + } + errorCalled = true + } + } + tk := s.Scan() + if tk != tok { + t.Errorf("tok = %s, want %s for %q", TokenString(tk), TokenString(tok), src) + } + if !errorCalled { + t.Errorf("error handler not called for %q", src) + } + if s.ErrorCount == 0 { + t.Errorf("count = %d, want > 0 for %q", s.ErrorCount, src) + } +} + +func TestError(t *testing.T) { + testError(t, "\x00", "1:1", "illegal character NUL", 0) + testError(t, "\x80", "1:1", "illegal UTF-8 encoding", utf8.RuneError) + testError(t, "\xff", "1:1", "illegal UTF-8 encoding", utf8.RuneError) + + testError(t, "a\x00", "1:2", "illegal character NUL", Ident) + testError(t, "ab\x80", "1:3", "illegal UTF-8 encoding", Ident) + testError(t, "abc\xff", "1:4", "illegal UTF-8 encoding", Ident) + + testError(t, `"a`+"\x00", "1:3", "illegal character NUL", String) + testError(t, `"ab`+"\x80", "1:4", "illegal UTF-8 encoding", String) + testError(t, `"abc`+"\xff", "1:5", "illegal UTF-8 encoding", String) + + testError(t, "`a"+"\x00", "1:3", "illegal character NUL", String) + testError(t, "`ab"+"\x80", "1:4", "illegal UTF-8 encoding", String) + testError(t, "`abc"+"\xff", "1:5", "illegal UTF-8 encoding", String) + + testError(t, `'\"'`, "1:3", "illegal char escape", Char) + testError(t, `"\'"`, "1:3", "illegal char escape", String) + + testError(t, `01238`, "1:6", "illegal octal number", Int) + testError(t, `01238123`, "1:9", "illegal octal number", Int) + testError(t, `0x`, "1:3", "illegal hexadecimal number", Int) + testError(t, `0xg`, "1:3", "illegal hexadecimal number", Int) + testError(t, `'aa'`, "1:4", "illegal char literal", Char) + + testError(t, `'`, "1:2", "literal not terminated", Char) + testError(t, `'`+"\n", "1:2", "literal not terminated", Char) + testError(t, `"abc`, "1:5", "literal not terminated", String) + testError(t, `"abc`+"\n", "1:5", "literal not terminated", String) + testError(t, "`abc\n", "2:1", "literal not terminated", String) + testError(t, `/*/`, "1:4", "comment not terminated", EOF) +} + +// An errReader returns (0, err) where err is not io.EOF. +type errReader struct{} + +func (errReader) Read(b []byte) (int, error) { + return 0, io.ErrNoProgress // some error that is not io.EOF +} + +func TestIOError(t *testing.T) { + s := new(Scanner).Init(errReader{}) + errorCalled := false + s.Error = func(s *Scanner, msg string) { + if !errorCalled { + if want := io.ErrNoProgress.Error(); msg != want { + t.Errorf("msg = %q, want %q", msg, want) + } + errorCalled = true + } + } + tok := s.Scan() + if tok != EOF { + t.Errorf("tok = %s, want EOF", TokenString(tok)) + } + if !errorCalled { + t.Errorf("error handler not called") + } +} + +func checkPos(t *testing.T, got, want Position) { + if got.Offset != want.Offset || got.Line != want.Line || got.Column != want.Column { + t.Errorf("got offset, line, column = %d, %d, %d; want %d, %d, %d", + got.Offset, got.Line, got.Column, want.Offset, want.Line, want.Column) + } +} + +func checkNextPos(t *testing.T, s *Scanner, offset, line, column int, char rune) { + if ch := s.Next(); ch != char { + t.Errorf("ch = %s, want %s", TokenString(ch), TokenString(char)) + } + want := Position{Offset: offset, Line: line, Column: column} + checkPos(t, s.Pos(), want) +} + +func checkScanPos(t *testing.T, s *Scanner, offset, line, column int, char rune) { + want := Position{Offset: offset, Line: line, Column: column} + checkPos(t, s.Pos(), want) + if ch := s.Scan(); ch != char { + t.Errorf("ch = %s, want %s", TokenString(ch), TokenString(char)) + if string(ch) != s.TokenText() { + t.Errorf("tok = %q, want %q", s.TokenText(), string(ch)) + } + } + checkPos(t, s.Position, want) +} + +func TestPos(t *testing.T) { + // corner case: empty source + s := new(Scanner).Init(strings.NewReader("")) + checkPos(t, s.Pos(), Position{Offset: 0, Line: 1, Column: 1}) + s.Peek() // peek doesn't affect the position + checkPos(t, s.Pos(), Position{Offset: 0, Line: 1, Column: 1}) + + // corner case: source with only a newline + s = new(Scanner).Init(strings.NewReader("\n")) + checkPos(t, s.Pos(), Position{Offset: 0, Line: 1, Column: 1}) + checkNextPos(t, s, 1, 2, 1, '\n') + // after EOF position doesn't change + for i := 10; i > 0; i-- { + checkScanPos(t, s, 1, 2, 1, EOF) + } + if s.ErrorCount != 0 { + t.Errorf("%d errors", s.ErrorCount) + } + + // corner case: source with only a single character + s = new(Scanner).Init(strings.NewReader("本")) + checkPos(t, s.Pos(), Position{Offset: 0, Line: 1, Column: 1}) + checkNextPos(t, s, 3, 1, 2, '本') + // after EOF position doesn't change + for i := 10; i > 0; i-- { + checkScanPos(t, s, 3, 1, 2, EOF) + } + if s.ErrorCount != 0 { + t.Errorf("%d errors", s.ErrorCount) + } + + // positions after calling Next + s = new(Scanner).Init(strings.NewReader(" foo६४ \n\n本語\n")) + checkNextPos(t, s, 1, 1, 2, ' ') + s.Peek() // peek doesn't affect the position + checkNextPos(t, s, 2, 1, 3, ' ') + checkNextPos(t, s, 3, 1, 4, 'f') + checkNextPos(t, s, 4, 1, 5, 'o') + checkNextPos(t, s, 5, 1, 6, 'o') + checkNextPos(t, s, 8, 1, 7, '६') + checkNextPos(t, s, 11, 1, 8, '४') + checkNextPos(t, s, 12, 1, 9, ' ') + checkNextPos(t, s, 13, 1, 10, ' ') + checkNextPos(t, s, 14, 2, 1, '\n') + checkNextPos(t, s, 15, 3, 1, '\n') + checkNextPos(t, s, 18, 3, 2, '本') + checkNextPos(t, s, 21, 3, 3, '語') + checkNextPos(t, s, 22, 4, 1, '\n') + // after EOF position doesn't change + for i := 10; i > 0; i-- { + checkScanPos(t, s, 22, 4, 1, EOF) + } + if s.ErrorCount != 0 { + t.Errorf("%d errors", s.ErrorCount) + } + + // positions after calling Scan + s = new(Scanner).Init(strings.NewReader("abc\n本語\n\nx")) + s.Mode = 0 + s.Whitespace = 0 + checkScanPos(t, s, 0, 1, 1, 'a') + s.Peek() // peek doesn't affect the position + checkScanPos(t, s, 1, 1, 2, 'b') + checkScanPos(t, s, 2, 1, 3, 'c') + checkScanPos(t, s, 3, 1, 4, '\n') + checkScanPos(t, s, 4, 2, 1, '本') + checkScanPos(t, s, 7, 2, 2, '語') + checkScanPos(t, s, 10, 2, 3, '\n') + checkScanPos(t, s, 11, 3, 1, '\n') + checkScanPos(t, s, 12, 4, 1, 'x') + // after EOF position doesn't change + for i := 10; i > 0; i-- { + checkScanPos(t, s, 13, 4, 2, EOF) + } + if s.ErrorCount != 0 { + t.Errorf("%d errors", s.ErrorCount) + } +} |