// 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) } }