commentMap_test.go raw

   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