1 // Copyright 2022 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 types_test
6 7 import (
8 "fmt"
9 "go/scanner"
10 "go/token"
11 "regexp"
12 "strings"
13 "testing"
14 )
15 16 type comment struct {
17 line, col int // comment position
18 text string // comment text, excluding "//", "/*", or "*/"
19 }
20 21 // commentMap collects all comments in the given src with comment text
22 // that matches the supplied regular expression rx and returns them as
23 // []comment lists in a map indexed by line number. The comment text is
24 // the comment with any comment markers ("//", "/*", or "*/") stripped.
25 // The position for each comment is the position of the token immediately
26 // preceding the comment, with all comments that are on the same line
27 // collected in a slice, in source order. If there is no preceding token
28 // (the matching comment appears at the beginning of the file), then the
29 // recorded position is unknown (line, col = 0, 0).
30 // If there are no matching comments, the result is nil.
31 func commentMap(src []byte, rx *regexp.Regexp) (res map[int][]comment) {
32 fset := token.NewFileSet()
33 file := fset.AddFile("", -1, len(src))
34 35 var s scanner.Scanner
36 s.Init(file, src, nil, scanner.ScanComments)
37 var prev token.Pos // position of last non-comment, non-semicolon token
38 39 for {
40 pos, tok, lit := s.Scan()
41 switch tok {
42 case token.EOF:
43 return
44 case token.COMMENT:
45 if lit[1] == '*' {
46 lit = lit[:len(lit)-2] // strip trailing */
47 }
48 lit = lit[2:] // strip leading // or /*
49 if rx.MatchString(lit) {
50 p := fset.Position(prev)
51 err := comment{p.Line, p.Column, lit}
52 if res == nil {
53 res = make(map[int][]comment)
54 }
55 res[p.Line] = append(res[p.Line], err)
56 }
57 case token.SEMICOLON:
58 // ignore automatically inserted semicolon
59 if lit == "\n" {
60 continue
61 }
62 fallthrough
63 default:
64 prev = pos
65 }
66 }
67 }
68 69 func TestCommentMap(t *testing.T) {
70 const src = `/* ERROR "0:0" */ /* ERROR "0:0" */ // ERROR "0:0"
71 // ERROR "0:0"
72 x /* ERROR "3:1" */ // ignore automatically inserted semicolon here
73 /* ERROR "3:1" */ // position of x on previous line
74 x /* ERROR "5:4" */ ; // do not ignore this semicolon
75 /* ERROR "5:24" */ // position of ; on previous line
76 package /* ERROR "7:2" */ // indented with tab
77 import /* ERROR "8:9" */ // indented with blanks
78 `
79 m := commentMap([]byte(src), regexp.MustCompile("^ ERROR "))
80 found := 0 // number of errors found
81 for line, errlist := range m {
82 for _, err := range errlist {
83 if err.line != line {
84 t.Errorf("%v: got map line %d; want %d", err, err.line, line)
85 continue
86 }
87 // err.line == line
88 89 got := strings.TrimSpace(err.text[len(" ERROR "):])
90 want := fmt.Sprintf(`"%d:%d"`, line, err.col)
91 if got != want {
92 t.Errorf("%v: got msg %q; want %q", err, got, want)
93 continue
94 }
95 found++
96 }
97 }
98 99 want := strings.Count(src, " ERROR ")
100 if found != want {
101 t.Errorf("commentMap got %d errors; want %d", found, want)
102 }
103 }
104