github.com/azazeal/revive@v1.0.9/test/utils.go (about)

     1  package test
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"go/ast"
     8  	"go/parser"
     9  	"go/printer"
    10  	"go/token"
    11  	"go/types"
    12  	"io/ioutil"
    13  	"os"
    14  	"strconv"
    15  	"strings"
    16  	"testing"
    17  
    18  	"github.com/azazeal/revive/lint"
    19  	"github.com/pkg/errors"
    20  )
    21  
    22  func testRule(t *testing.T, filename string, rule lint.Rule, config ...*lint.RuleConfig) {
    23  	baseDir := "../testdata/"
    24  	filename = filename + ".go"
    25  	src, err := ioutil.ReadFile(baseDir + filename)
    26  	if err != nil {
    27  		t.Fatalf("Bad filename path in test for %s: %v", rule.Name(), err)
    28  	}
    29  	stat, err := os.Stat(baseDir + filename)
    30  	if err != nil {
    31  		t.Fatalf("Cannot get file info for %s: %v", rule.Name(), err)
    32  	}
    33  	c := map[string]lint.RuleConfig{}
    34  	if config != nil {
    35  		c[rule.Name()] = *config[0]
    36  	}
    37  	if parseInstructions(t, filename, src) == nil {
    38  		assertSuccess(t, baseDir, stat, []lint.Rule{rule}, c)
    39  		return
    40  	}
    41  	assertFailures(t, baseDir, stat, src, []lint.Rule{rule}, c)
    42  }
    43  
    44  func assertSuccess(t *testing.T, baseDir string, fi os.FileInfo, rules []lint.Rule, config map[string]lint.RuleConfig) error {
    45  	l := lint.New(func(file string) ([]byte, error) {
    46  		return ioutil.ReadFile(baseDir + file)
    47  	})
    48  
    49  	ps, err := l.Lint([][]string{[]string{fi.Name()}}, rules, lint.Config{
    50  		Rules: config,
    51  	})
    52  	if err != nil {
    53  		return err
    54  	}
    55  
    56  	failures := ""
    57  	for p := range ps {
    58  		failures += p.Failure
    59  	}
    60  	if failures != "" {
    61  		t.Errorf("Expected the rule to pass but got the following failures: %s", failures)
    62  	}
    63  	return nil
    64  }
    65  
    66  func assertFailures(t *testing.T, baseDir string, fi os.FileInfo, src []byte, rules []lint.Rule, config map[string]lint.RuleConfig) error {
    67  	l := lint.New(func(file string) ([]byte, error) {
    68  		return ioutil.ReadFile(baseDir + file)
    69  	})
    70  
    71  	ins := parseInstructions(t, fi.Name(), src)
    72  	if ins == nil {
    73  		return errors.Errorf("Test file %v does not have instructions", fi.Name())
    74  	}
    75  
    76  	ps, err := l.Lint([][]string{[]string{fi.Name()}}, rules, lint.Config{
    77  		Rules: config,
    78  	})
    79  	if err != nil {
    80  		return err
    81  	}
    82  
    83  	failures := []lint.Failure{}
    84  	for f := range ps {
    85  		failures = append(failures, f)
    86  	}
    87  
    88  	for _, in := range ins {
    89  		ok := false
    90  		for i, p := range failures {
    91  			if p.Position.Start.Line != in.Line {
    92  				continue
    93  			}
    94  			if in.Match == p.Failure {
    95  				// check replacement if we are expecting one
    96  				if in.Replacement != "" {
    97  					// ignore any inline comments, since that would be recursive
    98  					r := p.ReplacementLine
    99  					if i := strings.Index(r, " //"); i >= 0 {
   100  						r = r[:i]
   101  					}
   102  					if r != in.Replacement {
   103  						t.Errorf("Lint failed at %s:%d; got replacement %q, want %q", fi.Name(), in.Line, r, in.Replacement)
   104  					}
   105  				}
   106  
   107  				// remove this problem from ps
   108  				copy(failures[i:], failures[i+1:])
   109  				failures = failures[:len(failures)-1]
   110  
   111  				// t.Logf("/%v/ matched at %s:%d", in.Match, fi.Name(), in.Line)
   112  				ok = true
   113  				break
   114  			}
   115  		}
   116  		if !ok {
   117  			t.Errorf("Lint failed at %s:%d; /%v/ did not match", fi.Name(), in.Line, in.Match)
   118  		}
   119  	}
   120  	for _, p := range failures {
   121  		t.Errorf("Unexpected problem at %s:%d: %v", fi.Name(), p.Position.Start.Line, p.Failure)
   122  	}
   123  	return nil
   124  }
   125  
   126  type instruction struct {
   127  	Line        int    // the line number this applies to
   128  	Match       string // what pattern to match
   129  	Replacement string // what the suggested replacement line should be
   130  }
   131  
   132  // parseInstructions parses instructions from the comments in a Go source file.
   133  // It returns nil if none were parsed.
   134  func parseInstructions(t *testing.T, filename string, src []byte) []instruction {
   135  	fset := token.NewFileSet()
   136  	f, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
   137  	if err != nil {
   138  		t.Fatalf("Test file %v does not parse: %v", filename, err)
   139  	}
   140  	var ins []instruction
   141  	for _, cg := range f.Comments {
   142  		ln := fset.Position(cg.Pos()).Line
   143  		raw := cg.Text()
   144  		for _, line := range strings.Split(raw, "\n") {
   145  			if line == "" || strings.HasPrefix(line, "#") {
   146  				continue
   147  			}
   148  			if line == "OK" && ins == nil {
   149  				// so our return value will be non-nil
   150  				ins = make([]instruction, 0)
   151  				continue
   152  			}
   153  			if strings.Contains(line, "MATCH") {
   154  				match, err := extractPattern(line)
   155  				if err != nil {
   156  					t.Fatalf("At %v:%d: %v", filename, ln, err)
   157  				}
   158  				matchLine := ln
   159  				if i := strings.Index(line, "MATCH:"); i >= 0 {
   160  					// This is a match for a different line.
   161  					lns := strings.TrimPrefix(line[i:], "MATCH:")
   162  					lns = lns[:strings.Index(lns, " ")]
   163  					matchLine, err = strconv.Atoi(lns)
   164  					if err != nil {
   165  						t.Fatalf("Bad match line number %q at %v:%d: %v", lns, filename, ln, err)
   166  					}
   167  				}
   168  				var repl string
   169  				if r, ok := extractReplacement(line); ok {
   170  					repl = r
   171  				}
   172  				ins = append(ins, instruction{
   173  					Line:        matchLine,
   174  					Match:       match,
   175  					Replacement: repl,
   176  				})
   177  			}
   178  		}
   179  	}
   180  	return ins
   181  }
   182  
   183  func extractPattern(line string) (string, error) {
   184  	a, b := strings.Index(line, "/"), strings.LastIndex(line, "/")
   185  	if a == -1 || a == b {
   186  		return "", fmt.Errorf("malformed match instruction %q", line)
   187  	}
   188  	return line[a+1 : b], nil
   189  }
   190  
   191  func extractReplacement(line string) (string, bool) {
   192  	// Look for this:  / -> `
   193  	// (the end of a match and start of a backtick string),
   194  	// and then the closing backtick.
   195  	const start = "/ -> `"
   196  	a, b := strings.Index(line, start), strings.LastIndex(line, "`")
   197  	if a < 0 || a > b {
   198  		return "", false
   199  	}
   200  	return line[a+len(start) : b], true
   201  }
   202  
   203  func render(fset *token.FileSet, x interface{}) string {
   204  	var buf bytes.Buffer
   205  	if err := printer.Fprint(&buf, fset, x); err != nil {
   206  		panic(err)
   207  	}
   208  	return buf.String()
   209  }
   210  
   211  func srcLine(src []byte, p token.Position) string {
   212  	// Run to end of line in both directions if not at line start/end.
   213  	lo, hi := p.Offset, p.Offset+1
   214  	for lo > 0 && src[lo-1] != '\n' {
   215  		lo--
   216  	}
   217  	for hi < len(src) && src[hi-1] != '\n' {
   218  		hi++
   219  	}
   220  	return string(src[lo:hi])
   221  }
   222  
   223  // TestLine tests srcLine function
   224  func TestLine(t *testing.T) { //revive:disable-line:exported
   225  	tests := []struct {
   226  		src    string
   227  		offset int
   228  		want   string
   229  	}{
   230  		{"single line file", 5, "single line file"},
   231  		{"single line file with newline\n", 5, "single line file with newline\n"},
   232  		{"first\nsecond\nthird\n", 2, "first\n"},
   233  		{"first\nsecond\nthird\n", 9, "second\n"},
   234  		{"first\nsecond\nthird\n", 14, "third\n"},
   235  		{"first\nsecond\nthird with no newline", 16, "third with no newline"},
   236  		{"first byte\n", 0, "first byte\n"},
   237  	}
   238  	for _, test := range tests {
   239  		got := srcLine([]byte(test.src), token.Position{Offset: test.offset})
   240  		if got != test.want {
   241  			t.Errorf("srcLine(%q, offset=%d) = %q, want %q", test.src, test.offset, got, test.want)
   242  		}
   243  	}
   244  }
   245  
   246  // TestLintName tests lint.Name function
   247  func TestLintName(t *testing.T) { //revive:disable-line:exported
   248  	tests := []struct {
   249  		name, want string
   250  	}{
   251  		{"foo_bar", "fooBar"},
   252  		{"foo_bar_baz", "fooBarBaz"},
   253  		{"Foo_bar", "FooBar"},
   254  		{"foo_WiFi", "fooWiFi"},
   255  		{"id", "id"},
   256  		{"Id", "ID"},
   257  		{"foo_id", "fooID"},
   258  		{"fooId", "fooID"},
   259  		{"fooUid", "fooUID"},
   260  		{"idFoo", "idFoo"},
   261  		{"uidFoo", "uidFoo"},
   262  		{"midIdDle", "midIDDle"},
   263  		{"APIProxy", "APIProxy"},
   264  		{"ApiProxy", "APIProxy"},
   265  		{"apiProxy", "apiProxy"},
   266  		{"_Leading", "_Leading"},
   267  		{"___Leading", "_Leading"},
   268  		{"trailing_", "trailing"},
   269  		{"trailing___", "trailing"},
   270  		{"a_b", "aB"},
   271  		{"a__b", "aB"},
   272  		{"a___b", "aB"},
   273  		{"Rpc1150", "RPC1150"},
   274  		{"case3_1", "case3_1"},
   275  		{"case3__1", "case3_1"},
   276  		{"IEEE802_16bit", "IEEE802_16bit"},
   277  		{"IEEE802_16Bit", "IEEE802_16Bit"},
   278  	}
   279  	for _, test := range tests {
   280  		got := lint.Name(test.name, nil, nil)
   281  		if got != test.want {
   282  			t.Errorf("lintName(%q) = %q, want %q", test.name, got, test.want)
   283  		}
   284  	}
   285  }
   286  
   287  // exportedType reports whether typ is an exported type.
   288  // It is imprecise, and will err on the side of returning true,
   289  // such as for composite types.
   290  func exportedType(typ types.Type) bool {
   291  	switch T := typ.(type) {
   292  	case *types.Named:
   293  		// Builtin types have no package.
   294  		return T.Obj().Pkg() == nil || T.Obj().Exported()
   295  	case *types.Map:
   296  		return exportedType(T.Key()) && exportedType(T.Elem())
   297  	case interface {
   298  		Elem() types.Type
   299  	}: // array, slice, pointer, chan
   300  		return exportedType(T.Elem())
   301  	}
   302  	// Be conservative about other types, such as struct, interface, etc.
   303  	return true
   304  }
   305  
   306  // TestExportedType tests exportedType function
   307  func TestExportedType(t *testing.T) { //revive:disable-line:exported
   308  	tests := []struct {
   309  		typString string
   310  		exp       bool
   311  	}{
   312  		{"int", true},
   313  		{"string", false}, // references the shadowed builtin "string"
   314  		{"T", true},
   315  		{"t", false},
   316  		{"*T", true},
   317  		{"*t", false},
   318  		{"map[int]complex128", true},
   319  	}
   320  	for _, test := range tests {
   321  		src := `package foo; type T int; type t int; type string struct{}`
   322  		fset := token.NewFileSet()
   323  		file, err := parser.ParseFile(fset, "foo.go", src, 0)
   324  		if err != nil {
   325  			t.Fatalf("Parsing %q: %v", src, err)
   326  		}
   327  		// use the package name as package path
   328  		config := &types.Config{}
   329  		pkg, err := config.Check(file.Name.Name, fset, []*ast.File{file}, nil)
   330  		if err != nil {
   331  			t.Fatalf("Type checking %q: %v", src, err)
   332  		}
   333  		tv, err := types.Eval(fset, pkg, token.NoPos, test.typString)
   334  		if err != nil {
   335  			t.Errorf("types.Eval(%q): %v", test.typString, err)
   336  			continue
   337  		}
   338  		if got := exportedType(tv.Type); got != test.exp {
   339  			t.Errorf("exportedType(%v) = %t, want %t", tv.Type, got, test.exp)
   340  		}
   341  	}
   342  }
   343  
   344  var (
   345  	genHdr = []byte("// Code generated ")
   346  	genFtr = []byte(" DO NOT EDIT.")
   347  )
   348  
   349  // isGenerated reports whether the source file is generated code
   350  // according the rules from https://golang.org/s/generatedcode.
   351  func isGenerated(src []byte) bool {
   352  	sc := bufio.NewScanner(bytes.NewReader(src))
   353  	for sc.Scan() {
   354  		b := sc.Bytes()
   355  		if bytes.HasPrefix(b, genHdr) && bytes.HasSuffix(b, genFtr) && len(b) >= len(genHdr)+len(genFtr) {
   356  			return true
   357  		}
   358  	}
   359  	return false
   360  }
   361  
   362  // TestIsGenerated tests isGenerated function
   363  func TestIsGenerated(t *testing.T) { //revive:disable-line:exported
   364  	tests := []struct {
   365  		source    string
   366  		generated bool
   367  	}{
   368  		{"// Code Generated by some tool. DO NOT EDIT.", false},
   369  		{"// Code generated by some tool. DO NOT EDIT.", true},
   370  		{"// Code generated by some tool. DO NOT EDIT", false},
   371  		{"// Code generated  DO NOT EDIT.", true},
   372  		{"// Code generated DO NOT EDIT.", false},
   373  		{"\t\t// Code generated by some tool. DO NOT EDIT.\npackage foo\n", false},
   374  		{"// Code generated by some tool. DO NOT EDIT.\npackage foo\n", true},
   375  		{"package foo\n// Code generated by some tool. DO NOT EDIT.\ntype foo int\n", true},
   376  		{"package foo\n // Code generated by some tool. DO NOT EDIT.\ntype foo int\n", false},
   377  		{"package foo\n// Code generated by some tool. DO NOT EDIT. \ntype foo int\n", false},
   378  		{"package foo\ntype foo int\n// Code generated by some tool. DO NOT EDIT.\n", true},
   379  		{"package foo\ntype foo int\n// Code generated by some tool. DO NOT EDIT.", true},
   380  	}
   381  
   382  	for i, test := range tests {
   383  		got := isGenerated([]byte(test.source))
   384  		if got != test.generated {
   385  			t.Errorf("test %d, isGenerated() = %v, want %v", i, got, test.generated)
   386  		}
   387  	}
   388  }