| // Copyright 2022 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 types_test |
| |
| import ( |
| "fmt" |
| "go/scanner" |
| "go/token" |
| "regexp" |
| "strings" |
| "testing" |
| ) |
| |
| type comment struct { |
| line, col int // comment position |
| text string // comment text, excluding "//", "/*", or "*/" |
| } |
| |
| // commentMap collects all comments in the given src with comment text |
| // that matches the supplied regular expression rx and returns them as |
| // []comment lists in a map indexed by line number. The comment text is |
| // the comment with any comment markers ("//", "/*", or "*/") stripped. |
| // The position for each comment is the position of the token immediately |
| // preceding the comment, with all comments that are on the same line |
| // collected in a slice, in source order. If there is no preceding token |
| // (the matching comment appears at the beginning of the file), then the |
| // recorded position is unknown (line, col = 0, 0). |
| // If there are no matching comments, the result is nil. |
| func commentMap(src []byte, rx *regexp.Regexp) (res map[int][]comment) { |
| fset := token.NewFileSet() |
| file := fset.AddFile("", -1, len(src)) |
| |
| var s scanner.Scanner |
| s.Init(file, src, nil, scanner.ScanComments) |
| var prev token.Pos // position of last non-comment, non-semicolon token |
| |
| for { |
| pos, tok, lit := s.Scan() |
| switch tok { |
| case token.EOF: |
| return |
| case token.COMMENT: |
| if lit[1] == '*' { |
| lit = lit[:len(lit)-2] // strip trailing */ |
| } |
| lit = lit[2:] // strip leading // or /* |
| if rx.MatchString(lit) { |
| p := fset.Position(prev) |
| err := comment{p.Line, p.Column, lit} |
| if res == nil { |
| res = make(map[int][]comment) |
| } |
| res[p.Line] = append(res[p.Line], err) |
| } |
| case token.SEMICOLON: |
| // ignore automatically inserted semicolon |
| if lit == "\n" { |
| continue |
| } |
| fallthrough |
| default: |
| prev = pos |
| } |
| } |
| } |
| |
| func TestCommentMap(t *testing.T) { |
| const src = `/* ERROR "0:0" */ /* ERROR "0:0" */ // ERROR "0:0" |
| // ERROR "0:0" |
| x /* ERROR "3:1" */ // ignore automatically inserted semicolon here |
| /* ERROR "3:1" */ // position of x on previous line |
| x /* ERROR "5:4" */ ; // do not ignore this semicolon |
| /* ERROR "5:24" */ // position of ; on previous line |
| package /* ERROR "7:2" */ // indented with tab |
| import /* ERROR "8:9" */ // indented with blanks |
| ` |
| m := commentMap([]byte(src), regexp.MustCompile("^ ERROR ")) |
| found := 0 // number of errors found |
| for line, errlist := range m { |
| for _, err := range errlist { |
| if err.line != line { |
| t.Errorf("%v: got map line %d; want %d", err, err.line, line) |
| continue |
| } |
| // err.line == line |
| |
| got := strings.TrimSpace(err.text[len(" ERROR "):]) |
| want := fmt.Sprintf(`"%d:%d"`, line, err.col) |
| if got != want { |
| t.Errorf("%v: got msg %q; want %q", err, got, want) |
| continue |
| } |
| found++ |
| } |
| } |
| |
| want := strings.Count(src, " ERROR ") |
| if found != want { |
| t.Errorf("commentMap got %d errors; want %d", found, want) |
| } |
| } |