github.com/vanstinator/golangci-lint@v0.0.0-20240223191551-cc572f00d9d1/test/testshared/analysis.go (about)

     1  package testshared
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"go/parser"
     7  	"go/token"
     8  	"os"
     9  	"regexp"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  	"testing"
    14  	"text/scanner"
    15  
    16  	"github.com/stretchr/testify/require"
    17  
    18  	"github.com/vanstinator/golangci-lint/pkg/result"
    19  )
    20  
    21  const keyword = "want"
    22  
    23  type jsonResult struct {
    24  	Issues []*result.Issue
    25  }
    26  
    27  type expectation struct {
    28  	kind string // either "fact" or "diagnostic"
    29  	name string // name of object to which fact belongs, or "package" ("fact" only)
    30  	rx   *regexp.Regexp
    31  }
    32  
    33  type key struct {
    34  	file string
    35  	line int
    36  }
    37  
    38  // Analyze analyzes the test expectations ('want').
    39  // inspired by https://github.com/golang/tools/blob/b3b5c13b291f9653da6f31b95db100a2e26bd186/go/analysis/analysistest/analysistest.go
    40  func Analyze(t *testing.T, sourcePath string, rawData []byte) {
    41  	fileData, err := os.ReadFile(sourcePath)
    42  	require.NoError(t, err)
    43  
    44  	want, err := parseComments(sourcePath, fileData)
    45  	require.NoError(t, err)
    46  
    47  	var reportData jsonResult
    48  	err = json.Unmarshal(rawData, &reportData)
    49  	require.NoError(t, err, string(rawData))
    50  
    51  	for _, issue := range reportData.Issues {
    52  		checkMessage(t, want, issue.Pos, "diagnostic", issue.FromLinter, issue.Text)
    53  	}
    54  
    55  	var surplus []string
    56  	for key, expects := range want {
    57  		for _, exp := range expects {
    58  			err := fmt.Sprintf("%s:%d: no %s was reported matching %#q", key.file, key.line, exp.kind, exp.rx)
    59  			surplus = append(surplus, err)
    60  		}
    61  	}
    62  
    63  	sort.Strings(surplus)
    64  
    65  	for _, err := range surplus {
    66  		t.Errorf("%s", err)
    67  	}
    68  }
    69  
    70  // inspired by https://github.com/golang/tools/blob/b3b5c13b291f9653da6f31b95db100a2e26bd186/go/analysis/analysistest/analysistest.go
    71  func parseComments(sourcePath string, fileData []byte) (map[key][]expectation, error) {
    72  	fset := token.NewFileSet()
    73  
    74  	// the error is ignored to let 'typecheck' handle compilation error
    75  	f, _ := parser.ParseFile(fset, sourcePath, fileData, parser.ParseComments)
    76  
    77  	want := make(map[key][]expectation)
    78  
    79  	for _, comment := range f.Comments {
    80  		for _, c := range comment.List {
    81  			text := strings.TrimPrefix(c.Text, "//")
    82  			if text == c.Text { // not a //-comment.
    83  				text = strings.TrimPrefix(text, "/*")
    84  				text = strings.TrimSuffix(text, "*/")
    85  			}
    86  
    87  			if i := strings.Index(text, "// "+keyword); i >= 0 {
    88  				text = text[i+len("// "):]
    89  			}
    90  
    91  			posn := fset.Position(c.Pos())
    92  
    93  			text = strings.TrimSpace(text)
    94  
    95  			if rest := strings.TrimPrefix(text, keyword); rest != text {
    96  				delta, expects, err := parseExpectations(rest)
    97  				if err != nil {
    98  					return nil, err
    99  				}
   100  
   101  				want[key{sourcePath, posn.Line + delta}] = expects
   102  			}
   103  		}
   104  	}
   105  
   106  	return want, nil
   107  }
   108  
   109  // inspired by https://github.com/golang/tools/blob/b3b5c13b291f9653da6f31b95db100a2e26bd186/go/analysis/analysistest/analysistest.go
   110  func parseExpectations(text string) (lineDelta int, expects []expectation, err error) {
   111  	var scanErr string
   112  	sc := new(scanner.Scanner).Init(strings.NewReader(text))
   113  	sc.Error = func(s *scanner.Scanner, msg string) {
   114  		scanErr = msg // e.g. bad string escape
   115  	}
   116  	sc.Mode = scanner.ScanIdents | scanner.ScanStrings | scanner.ScanRawStrings | scanner.ScanInts
   117  
   118  	scanRegexp := func(tok rune) (*regexp.Regexp, error) {
   119  		if tok != scanner.String && tok != scanner.RawString {
   120  			return nil, fmt.Errorf("got %s, want regular expression",
   121  				scanner.TokenString(tok))
   122  		}
   123  		pattern, _ := strconv.Unquote(sc.TokenText()) // can't fail
   124  		return regexp.Compile(pattern)
   125  	}
   126  
   127  	for {
   128  		tok := sc.Scan()
   129  		switch tok {
   130  		case '+':
   131  			tok = sc.Scan()
   132  			if tok != scanner.Int {
   133  				return 0, nil, fmt.Errorf("got +%s, want +Int", scanner.TokenString(tok))
   134  			}
   135  			lineDelta, _ = strconv.Atoi(sc.TokenText())
   136  		case scanner.String, scanner.RawString:
   137  			rx, err := scanRegexp(tok)
   138  			if err != nil {
   139  				return 0, nil, err
   140  			}
   141  			expects = append(expects, expectation{"diagnostic", "", rx})
   142  
   143  		case scanner.Ident:
   144  			name := sc.TokenText()
   145  			tok = sc.Scan()
   146  			if tok != ':' {
   147  				return 0, nil, fmt.Errorf("got %s after %s, want ':'",
   148  					scanner.TokenString(tok), name)
   149  			}
   150  			tok = sc.Scan()
   151  			rx, err := scanRegexp(tok)
   152  			if err != nil {
   153  				return 0, nil, err
   154  			}
   155  			expects = append(expects, expectation{"diagnostic", name, rx})
   156  
   157  		case scanner.EOF:
   158  			if scanErr != "" {
   159  				return 0, nil, fmt.Errorf("%s", scanErr)
   160  			}
   161  			return lineDelta, expects, nil
   162  
   163  		default:
   164  			return 0, nil, fmt.Errorf("unexpected %s", scanner.TokenString(tok))
   165  		}
   166  	}
   167  }
   168  
   169  // inspired by https://github.com/golang/tools/blob/b3b5c13b291f9653da6f31b95db100a2e26bd186/go/analysis/analysistest/analysistest.go
   170  func checkMessage(t *testing.T, want map[key][]expectation, posn token.Position, kind, name, message string) {
   171  	k := key{posn.Filename, posn.Line}
   172  	expects := want[k]
   173  	var unmatched []string
   174  
   175  	for i, exp := range expects {
   176  		if exp.kind == kind && (exp.name == "" || exp.name == name) {
   177  			if exp.rx.MatchString(message) {
   178  				// matched: remove the expectation.
   179  				expects[i] = expects[len(expects)-1]
   180  				expects = expects[:len(expects)-1]
   181  				want[k] = expects
   182  				return
   183  			}
   184  			unmatched = append(unmatched, fmt.Sprintf("%#q", exp.rx))
   185  		}
   186  	}
   187  
   188  	if unmatched == nil {
   189  		t.Errorf("%v: unexpected %s: %v", posn, kind, message)
   190  	} else {
   191  		t.Errorf("%v: %s %q does not match pattern %s",
   192  			posn, kind, message, strings.Join(unmatched, " or "))
   193  	}
   194  }