195 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			195 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package testshared
 | 
						|
 | 
						|
import (
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"go/parser"
 | 
						|
	"go/token"
 | 
						|
	"os"
 | 
						|
	"regexp"
 | 
						|
	"sort"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
	"testing"
 | 
						|
	"text/scanner"
 | 
						|
 | 
						|
	"github.com/stretchr/testify/require"
 | 
						|
 | 
						|
	"github.com/golangci/golangci-lint/pkg/result"
 | 
						|
)
 | 
						|
 | 
						|
const keyword = "want"
 | 
						|
 | 
						|
type jsonResult struct {
 | 
						|
	Issues []*result.Issue
 | 
						|
}
 | 
						|
 | 
						|
type expectation struct {
 | 
						|
	kind string // either "fact" or "diagnostic"
 | 
						|
	name string // name of object to which fact belongs, or "package" ("fact" only)
 | 
						|
	rx   *regexp.Regexp
 | 
						|
}
 | 
						|
 | 
						|
type key struct {
 | 
						|
	file string
 | 
						|
	line int
 | 
						|
}
 | 
						|
 | 
						|
// Analyze analyzes the test expectations ('want').
 | 
						|
// inspired by https://github.com/golang/tools/blob/b3b5c13b291f9653da6f31b95db100a2e26bd186/go/analysis/analysistest/analysistest.go
 | 
						|
func Analyze(t *testing.T, sourcePath string, rawData []byte) {
 | 
						|
	fileData, err := os.ReadFile(sourcePath)
 | 
						|
	require.NoError(t, err)
 | 
						|
 | 
						|
	want, err := parseComments(sourcePath, fileData)
 | 
						|
	require.NoError(t, err)
 | 
						|
 | 
						|
	var reportData jsonResult
 | 
						|
	err = json.Unmarshal(rawData, &reportData)
 | 
						|
	require.NoError(t, err, string(rawData))
 | 
						|
 | 
						|
	for _, issue := range reportData.Issues {
 | 
						|
		checkMessage(t, want, issue.Pos, "diagnostic", issue.FromLinter, issue.Text)
 | 
						|
	}
 | 
						|
 | 
						|
	var surplus []string
 | 
						|
	for key, expects := range want {
 | 
						|
		for _, exp := range expects {
 | 
						|
			err := fmt.Sprintf("%s:%d: no %s was reported matching %#q", key.file, key.line, exp.kind, exp.rx)
 | 
						|
			surplus = append(surplus, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	sort.Strings(surplus)
 | 
						|
 | 
						|
	for _, err := range surplus {
 | 
						|
		t.Errorf("%s", err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// inspired by https://github.com/golang/tools/blob/b3b5c13b291f9653da6f31b95db100a2e26bd186/go/analysis/analysistest/analysistest.go
 | 
						|
func parseComments(sourcePath string, fileData []byte) (map[key][]expectation, error) {
 | 
						|
	fset := token.NewFileSet()
 | 
						|
 | 
						|
	// the error is ignored to let 'typecheck' handle compilation error
 | 
						|
	f, _ := parser.ParseFile(fset, sourcePath, fileData, parser.ParseComments)
 | 
						|
 | 
						|
	want := make(map[key][]expectation)
 | 
						|
 | 
						|
	for _, comment := range f.Comments {
 | 
						|
		for _, c := range comment.List {
 | 
						|
			text := strings.TrimPrefix(c.Text, "//")
 | 
						|
			if text == c.Text { // not a //-comment.
 | 
						|
				text = strings.TrimPrefix(text, "/*")
 | 
						|
				text = strings.TrimSuffix(text, "*/")
 | 
						|
			}
 | 
						|
 | 
						|
			if i := strings.Index(text, "// "+keyword); i >= 0 {
 | 
						|
				text = text[i+len("// "):]
 | 
						|
			}
 | 
						|
 | 
						|
			posn := fset.Position(c.Pos())
 | 
						|
 | 
						|
			text = strings.TrimSpace(text)
 | 
						|
 | 
						|
			if rest := strings.TrimPrefix(text, keyword); rest != text {
 | 
						|
				delta, expects, err := parseExpectations(rest)
 | 
						|
				if err != nil {
 | 
						|
					return nil, err
 | 
						|
				}
 | 
						|
 | 
						|
				want[key{sourcePath, posn.Line + delta}] = expects
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return want, nil
 | 
						|
}
 | 
						|
 | 
						|
// inspired by https://github.com/golang/tools/blob/b3b5c13b291f9653da6f31b95db100a2e26bd186/go/analysis/analysistest/analysistest.go
 | 
						|
func parseExpectations(text string) (lineDelta int, expects []expectation, err error) {
 | 
						|
	var scanErr string
 | 
						|
	sc := new(scanner.Scanner).Init(strings.NewReader(text))
 | 
						|
	sc.Error = func(s *scanner.Scanner, msg string) {
 | 
						|
		scanErr = msg // e.g. bad string escape
 | 
						|
	}
 | 
						|
	sc.Mode = scanner.ScanIdents | scanner.ScanStrings | scanner.ScanRawStrings | scanner.ScanInts
 | 
						|
 | 
						|
	scanRegexp := func(tok rune) (*regexp.Regexp, error) {
 | 
						|
		if tok != scanner.String && tok != scanner.RawString {
 | 
						|
			return nil, fmt.Errorf("got %s, want regular expression",
 | 
						|
				scanner.TokenString(tok))
 | 
						|
		}
 | 
						|
		pattern, _ := strconv.Unquote(sc.TokenText()) // can't fail
 | 
						|
		return regexp.Compile(pattern)
 | 
						|
	}
 | 
						|
 | 
						|
	for {
 | 
						|
		tok := sc.Scan()
 | 
						|
		switch tok {
 | 
						|
		case '+':
 | 
						|
			tok = sc.Scan()
 | 
						|
			if tok != scanner.Int {
 | 
						|
				return 0, nil, fmt.Errorf("got +%s, want +Int", scanner.TokenString(tok))
 | 
						|
			}
 | 
						|
			lineDelta, _ = strconv.Atoi(sc.TokenText())
 | 
						|
		case scanner.String, scanner.RawString:
 | 
						|
			rx, err := scanRegexp(tok)
 | 
						|
			if err != nil {
 | 
						|
				return 0, nil, err
 | 
						|
			}
 | 
						|
			expects = append(expects, expectation{"diagnostic", "", rx})
 | 
						|
 | 
						|
		case scanner.Ident:
 | 
						|
			name := sc.TokenText()
 | 
						|
			tok = sc.Scan()
 | 
						|
			if tok != ':' {
 | 
						|
				return 0, nil, fmt.Errorf("got %s after %s, want ':'",
 | 
						|
					scanner.TokenString(tok), name)
 | 
						|
			}
 | 
						|
			tok = sc.Scan()
 | 
						|
			rx, err := scanRegexp(tok)
 | 
						|
			if err != nil {
 | 
						|
				return 0, nil, err
 | 
						|
			}
 | 
						|
			expects = append(expects, expectation{"diagnostic", name, rx})
 | 
						|
 | 
						|
		case scanner.EOF:
 | 
						|
			if scanErr != "" {
 | 
						|
				return 0, nil, fmt.Errorf("%s", scanErr)
 | 
						|
			}
 | 
						|
			return lineDelta, expects, nil
 | 
						|
 | 
						|
		default:
 | 
						|
			return 0, nil, fmt.Errorf("unexpected %s", scanner.TokenString(tok))
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// inspired by https://github.com/golang/tools/blob/b3b5c13b291f9653da6f31b95db100a2e26bd186/go/analysis/analysistest/analysistest.go
 | 
						|
func checkMessage(t *testing.T, want map[key][]expectation, posn token.Position, kind, name, message string) {
 | 
						|
	k := key{posn.Filename, posn.Line}
 | 
						|
	expects := want[k]
 | 
						|
	var unmatched []string
 | 
						|
 | 
						|
	for i, exp := range expects {
 | 
						|
		if exp.kind == kind && (exp.name == "" || exp.name == name) {
 | 
						|
			if exp.rx.MatchString(message) {
 | 
						|
				// matched: remove the expectation.
 | 
						|
				expects[i] = expects[len(expects)-1]
 | 
						|
				expects = expects[:len(expects)-1]
 | 
						|
				want[k] = expects
 | 
						|
				return
 | 
						|
			}
 | 
						|
			unmatched = append(unmatched, fmt.Sprintf("%#q", exp.rx))
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if unmatched == nil {
 | 
						|
		t.Errorf("%v: unexpected %s: %v", posn, kind, message)
 | 
						|
	} else {
 | 
						|
		t.Errorf("%v: %s %q does not match pattern %s",
 | 
						|
			posn, kind, message, strings.Join(unmatched, " or "))
 | 
						|
	}
 | 
						|
}
 |