golang.org/x/tools@v0.21.1-0.20240520172518-788d39e776b1/internal/stack/parse.go (about) 1 // Copyright 2020 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package stack 6 7 import ( 8 "bufio" 9 "errors" 10 "io" 11 "regexp" 12 "strconv" 13 ) 14 15 var ( 16 reBlank = regexp.MustCompile(`^\s*$`) 17 reGoroutine = regexp.MustCompile(`^\s*goroutine (\d+) \[([^\]]*)\]:\s*$`) 18 reCall = regexp.MustCompile(`^\s*` + 19 `(created by )?` + //marker 20 `(([\w/.]+/)?[\w]+)\.` + //package 21 `(\(([^:.)]*)\)\.)?` + //optional type 22 `([\w\.]+)` + //function 23 `(\(.*\))?` + // args 24 `\s*$`) 25 rePos = regexp.MustCompile(`^\s*(.*):(\d+)( .*)?$`) 26 27 errBreakParse = errors.New("break parse") 28 ) 29 30 // Scanner splits an input stream into lines in a way that is consumable by 31 // the parser. 32 type Scanner struct { 33 lines *bufio.Scanner 34 done bool 35 } 36 37 // NewScanner creates a scanner on top of a reader. 38 func NewScanner(r io.Reader) *Scanner { 39 s := &Scanner{ 40 lines: bufio.NewScanner(r), 41 } 42 s.Skip() // prefill 43 return s 44 } 45 46 // Peek returns the next line without consuming it. 47 func (s *Scanner) Peek() string { 48 if s.done { 49 return "" 50 } 51 return s.lines.Text() 52 } 53 54 // Skip consumes the next line without looking at it. 55 // Normally used after it has already been looked at using Peek. 56 func (s *Scanner) Skip() { 57 if !s.lines.Scan() { 58 s.done = true 59 } 60 } 61 62 // Next consumes and returns the next line. 63 func (s *Scanner) Next() string { 64 line := s.Peek() 65 s.Skip() 66 return line 67 } 68 69 // Done returns true if the scanner has reached the end of the underlying 70 // stream. 71 func (s *Scanner) Done() bool { 72 return s.done 73 } 74 75 // Err returns true if the scanner has reached the end of the underlying 76 // stream. 77 func (s *Scanner) Err() error { 78 return s.lines.Err() 79 } 80 81 // Match returns the submatchs of the regular expression against the next line. 82 // If it matched the line is also consumed. 83 func (s *Scanner) Match(re *regexp.Regexp) []string { 84 if s.done { 85 return nil 86 } 87 match := re.FindStringSubmatch(s.Peek()) 88 if match != nil { 89 s.Skip() 90 } 91 return match 92 } 93 94 // SkipBlank skips any number of pure whitespace lines. 95 func (s *Scanner) SkipBlank() { 96 for !s.done { 97 line := s.Peek() 98 if len(line) != 0 && !reBlank.MatchString(line) { 99 return 100 } 101 s.Skip() 102 } 103 } 104 105 // Parse the current contiguous block of goroutine stack traces until the 106 // scanned content no longer matches. 107 func Parse(scanner *Scanner) (Dump, error) { 108 dump := Dump{} 109 for { 110 gr, ok := parseGoroutine(scanner) 111 if !ok { 112 return dump, nil 113 } 114 dump = append(dump, gr) 115 } 116 } 117 118 func parseGoroutine(scanner *Scanner) (Goroutine, bool) { 119 match := scanner.Match(reGoroutine) 120 if match == nil { 121 return Goroutine{}, false 122 } 123 id, _ := strconv.ParseInt(match[1], 0, 32) 124 gr := Goroutine{ 125 ID: int(id), 126 State: match[2], 127 } 128 for { 129 frame, ok := parseFrame(scanner) 130 if !ok { 131 scanner.SkipBlank() 132 return gr, true 133 } 134 if frame.Position.Filename != "" { 135 gr.Stack = append(gr.Stack, frame) 136 } 137 } 138 } 139 140 func parseFrame(scanner *Scanner) (Frame, bool) { 141 fun, ok := parseFunction(scanner) 142 if !ok { 143 return Frame{}, false 144 } 145 frame := Frame{ 146 Function: fun, 147 } 148 frame.Position, ok = parsePosition(scanner) 149 // if ok is false, then this is a broken state. 150 // we got the func but not the file that must follow 151 // the consumed line can be recovered from the frame 152 //TODO: push back the fun raw 153 return frame, ok 154 } 155 156 func parseFunction(scanner *Scanner) (Function, bool) { 157 match := scanner.Match(reCall) 158 if match == nil { 159 return Function{}, false 160 } 161 return Function{ 162 Package: match[2], 163 Type: match[5], 164 Name: match[6], 165 }, true 166 } 167 168 func parsePosition(scanner *Scanner) (Position, bool) { 169 match := scanner.Match(rePos) 170 if match == nil { 171 return Position{}, false 172 } 173 line, _ := strconv.ParseInt(match[2], 0, 32) 174 return Position{Filename: match[1], Line: int(line)}, true 175 }