diff --git a/pattern/lexer.go b/pattern/lexer.go index ec7735db3..8ab8f5696 100644 --- a/pattern/lexer.go +++ b/pattern/lexer.go @@ -3,10 +3,24 @@ package pattern import ( "fmt" "go/token" + "iter" "unicode" "unicode/utf8" ) +// lex returns the sequence of tokens in the input. +func lex(f *token.File, input string) iter.Seq[item] { + return func(yield func(item) bool) { + lex := &lexer{ + f: f, + input: input, + yield: yield, + } + lex.run() + } +} + +// lexer holds the state of a single [lex] iteration. type lexer struct { f *token.File @@ -14,7 +28,8 @@ type lexer struct { start int pos int width int - items chan item + + yield func(item) bool } type itemType int @@ -79,40 +94,55 @@ func (l *lexer) run() { for state := lexStart; state != nil; { state = state(l) } - close(l.items) } -func (l *lexer) emitValue(t itemType, value string) { - l.items <- item{t, value, l.start} +func (l *lexer) emitValue(t itemType, value string) bool { + ok := l.yield(item{t, value, l.start}) l.start = l.pos + return ok } -func (l *lexer) emit(t itemType) { - l.items <- item{t, l.input[l.start:l.pos], l.start} +func (l *lexer) emit(t itemType) bool { + ok := l.yield(item{t, l.input[l.start:l.pos], l.start}) l.start = l.pos + return ok } func lexStart(l *lexer) stateFn { switch r := l.next(); { case r == eof: - l.emit(itemEOF) + _ = l.emit(itemEOF) return nil case unicode.IsSpace(r): l.ignore() case r == '(': - l.emit(itemLeftParen) + if !l.emit(itemLeftParen) { + return nil + } case r == ')': - l.emit(itemRightParen) + if !l.emit(itemRightParen) { + return nil + } case r == '[': - l.emit(itemLeftBracket) + if !l.emit(itemLeftBracket) { + return nil + } case r == ']': - l.emit(itemRightBracket) + if !l.emit(itemRightBracket) { + return nil + } case r == '@': - l.emit(itemAt) + if !l.emit(itemAt) { + return nil + } case r == ':': - l.emit(itemColon) + if !l.emit(itemColon) { + return nil + } case r == '_': - l.emit(itemBlank) + if !l.emit(itemBlank) { + return nil + } case r == '"': l.backup() return lexString @@ -154,11 +184,11 @@ func (l *lexer) backup() { func (l *lexer) errorf(format string, args ...any) stateFn { // TODO(dh): emit position information in errors - l.items <- item{ + _ = l.yield(item{ itemError, fmt.Sprintf(format, args...), l.start, - } + }) return nil } @@ -179,7 +209,9 @@ func lexString(l *lexer) stateFn { return l.errorf("unterminated string") case '"': if !escape { - l.emitValue(itemString, string(runes)) + if !l.emitValue(itemString, string(runes)) { + return nil + } return lexStart } else { runes = append(runes, '"') @@ -203,7 +235,9 @@ func lexType(l *lexer) stateFn { for { if !isAlphaNumeric(l.next()) { l.backup() - l.emit(itemTypeName) + if !l.emit(itemTypeName) { + return nil + } return lexStart } } @@ -214,7 +248,9 @@ func lexVariable(l *lexer) stateFn { for { if !isAlphaNumeric(l.next()) { l.backup() - l.emit(itemVariable) + if !l.emit(itemVariable) { + return nil + } return lexStart } } diff --git a/pattern/match.go b/pattern/match.go index d90bcae6c..5ccb8d321 100644 --- a/pattern/match.go +++ b/pattern/match.go @@ -6,8 +6,6 @@ import ( "go/token" "go/types" "reflect" - - "golang.org/x/tools/go/ast/astutil" ) var tokensByString = map[string]Token{ @@ -569,9 +567,8 @@ func (fn Symbol) Match(m *Matcher, node any) (any, bool) { case *ast.IndexListExpr: fun = idx.X } - fun = astutil.Unparen(fun) - switch fun := fun.(type) { + switch fun := ast.Unparen(fun).(type) { case *ast.Ident: obj = m.TypesInfo.ObjectOf(fun) case *ast.SelectorExpr: diff --git a/pattern/parser.go b/pattern/parser.go index 26214babf..27d7ca977 100644 --- a/pattern/parser.go +++ b/pattern/parser.go @@ -5,8 +5,11 @@ import ( "fmt" "go/ast" "go/token" + "iter" + "log" "reflect" "strings" + "time" ) type Pattern struct { @@ -339,10 +342,10 @@ type Parser struct { // Allow nodes that rely on type information AllowTypeInfo bool - lex *lexer - cur item - last *item - items chan item + f *token.File + cur item + last *item + nextItem func() (item, bool) bindings map[string]int } @@ -360,26 +363,27 @@ func (p *Parser) bindingIndex(name string) int { } func (p *Parser) Parse(s string) (Pattern, error) { + f := token.NewFileSet().AddFile("", -1, len(s)) + + // Run the lexer iterator as a coroutine. + // The parser will call 'next' to consume each item. + // After the parser returns, we must call 'stop' to + // terminate the coroutine. + next, stop := iter.Pull(lex(f, s)) + defer stop() + p.cur = item{} p.last = nil - p.items = nil + p.f = f + p.nextItem = next - fset := token.NewFileSet() - p.lex = &lexer{ - f: fset.AddFile("", -1, len(s)), - input: s, - items: make(chan item), - } - go p.lex.run() - p.items = p.lex.items + // Parse. root, err := p.node() if err != nil { - // drain lexer if parsing failed - for range p.lex.items { - } return Pattern{}, err } - if item := <-p.lex.items; item.typ != itemEOF { + // Consume final EOF token. + if item, ok := next(); !ok || item.typ != itemEOF { return Pattern{}, fmt.Errorf("unexpected token %s after end of pattern", item.typ) } @@ -417,7 +421,7 @@ func (p *Parser) next() item { return n } var ok bool - p.cur, ok = <-p.items + p.cur, ok = p.nextItem() if !ok { p.cur = item{typ: eof} } @@ -455,7 +459,7 @@ func (p *Parser) unexpectedToken(valid string) error { got = "'" + p.cur.typ.String() + "'" } - pos := p.lex.f.Position(token.Pos(p.cur.pos)) + pos := p.f.Position(token.Pos(p.cur.pos)) return fmt.Errorf("%s: expected %s, found %s", pos, valid, got) } diff --git a/pattern/parser_test.go b/pattern/parser_test.go index f15743275..eb025d0db 100644 --- a/pattern/parser_test.go +++ b/pattern/parser_test.go @@ -245,3 +245,40 @@ func TestCollectSymbols(t *testing.T) { } } } + +func BenchmarkParser(b *testing.B) { + const input = ` + (CallExpr + (Symbol + name@(Or + "math/rand.Int31n" + "math/rand.Int63n" + "math/rand.Intn" + "(*math/rand.Rand).Int31n" + "(*math/rand.Rand).Int63n" + "(*math/rand.Rand).Intn" + + "math/rand/v2.Int32N" + "math/rand/v2.Int64N" + "math/rand/v2.IntN" + "math/rand/v2.N" + "math/rand/v2.Uint32N" + "math/rand/v2.Uint64N" + "math/rand/v2.UintN" + + "(*math/rand/v2.Rand).Int32N" + "(*math/rand/v2.Rand).Int64N" + "(*math/rand/v2.Rand).IntN" + "(*math/rand/v2.Rand).Uint32N" + "(*math/rand/v2.Rand).Uint64N" + "(*math/rand/v2.Rand).UintN")) + [(IntegerLiteral "1")])` + + for range b.N { + p := &Parser{AllowTypeInfo: true} + _, err := p.Parse(input) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/quickfix/qf1012/qf1012.go b/quickfix/qf1012/qf1012.go index eb89e65df..45784d94e 100644 --- a/quickfix/qf1012/qf1012.go +++ b/quickfix/qf1012/qf1012.go @@ -66,7 +66,7 @@ func run(pass *analysis.Pass) (any, error) { // is a named non-interface type, since the pointer // has a larger method set (https://staticcheck.dev/issues/1097). // We assume the receiver expression is addressable - // since otherwise thre code wouldn't compile. + // since otherwise the code wouldn't compile. if _, ok := types.Unalias(recvT).(*types.Named); ok && !types.IsInterface(recvT) { recvT = types.NewPointer(recvT) recv = &ast.UnaryExpr{Op: token.AND, X: recv}