github.com/vanstinator/golangci-lint@v0.0.0-20240223191551-cc572f00d9d1/test/testshared/directives.go (about) 1 package testshared 2 3 import ( 4 "bufio" 5 "go/build/constraint" 6 "os" 7 "runtime" 8 "strconv" 9 "strings" 10 "testing" 11 12 hcversion "github.com/hashicorp/go-version" 13 "github.com/stretchr/testify/require" 14 15 "github.com/vanstinator/golangci-lint/pkg/exitcodes" 16 ) 17 18 // RunContext the information extracted from directives. 19 type RunContext struct { 20 Args []string 21 ConfigPath string 22 ExpectedLinter string 23 ExitCode int 24 } 25 26 // ParseTestDirectives parses test directives from sources files. 27 // 28 //nolint:gocyclo,funlen 29 func ParseTestDirectives(tb testing.TB, sourcePath string) *RunContext { 30 tb.Helper() 31 32 f, err := os.Open(sourcePath) 33 require.NoError(tb, err) 34 tb.Cleanup(func() { _ = f.Close() }) 35 36 rc := &RunContext{ 37 ExitCode: exitcodes.IssuesFound, 38 } 39 40 scanner := bufio.NewScanner(f) 41 for scanner.Scan() { 42 line := scanner.Text() 43 if strings.HasPrefix(line, "/*") { 44 skipMultilineComment(scanner) 45 continue 46 } 47 if strings.TrimSpace(line) == "" { 48 continue 49 } 50 if !strings.HasPrefix(line, "//") { 51 break 52 } 53 54 if constraint.IsGoBuild(line) { 55 if !evaluateBuildTags(tb, line) { 56 return nil 57 } 58 59 continue 60 } 61 62 if !strings.HasPrefix(line, "//golangcitest:") { 63 require.Failf(tb, "invalid prefix of comment line %s", line) 64 } 65 66 before, after, found := strings.Cut(line, " ") 67 require.Truef(tb, found, "invalid prefix of comment line %s", line) 68 69 after = strings.TrimSpace(after) 70 71 switch before { 72 case "//golangcitest:args": 73 require.Nil(tb, rc.Args) 74 require.NotEmpty(tb, after) 75 rc.Args = strings.Split(after, " ") 76 continue 77 78 case "//golangcitest:config_path": 79 require.NotEmpty(tb, after) 80 rc.ConfigPath = after 81 continue 82 83 case "//golangcitest:expected_linter": 84 require.NotEmpty(tb, after) 85 rc.ExpectedLinter = after 86 continue 87 88 case "//golangcitest:expected_exitcode": 89 require.NotEmpty(tb, after) 90 val, err := strconv.Atoi(after) 91 require.NoError(tb, err) 92 93 rc.ExitCode = val 94 continue 95 96 default: 97 require.Failf(tb, "invalid prefix of comment line %s", line) 98 } 99 } 100 101 // guess the expected linter if none is specified 102 if rc.ExpectedLinter == "" { 103 for _, arg := range rc.Args { 104 if strings.HasPrefix(arg, "-E") && !strings.Contains(arg, ",") { 105 require.Empty(tb, rc.ExpectedLinter, "could not infer expected linter for errors because multiple linters are enabled. Please use the `//golangcitest:expected_linter ` directive in your test to indicate the linter-under-test.") //nolint:lll 106 rc.ExpectedLinter = arg[2:] 107 } 108 } 109 } 110 111 return rc 112 } 113 114 func skipMultilineComment(scanner *bufio.Scanner) { 115 for line := scanner.Text(); !strings.Contains(line, "*/") && scanner.Scan(); { 116 line = scanner.Text() 117 } 118 } 119 120 // evaluateBuildTags Naive implementation of the evaluation of the build tags. 121 // Inspired by https://github.com/golang/go/blob/1dcef7b3bdcea4a829ea22c821e6a9484c325d61/src/cmd/go/internal/modindex/build.go#L914-L972 122 func evaluateBuildTags(tb testing.TB, line string) bool { 123 parse, err := constraint.Parse(line) 124 require.NoError(tb, err) 125 126 return parse.Eval(func(tag string) bool { 127 if tag == runtime.GOOS { 128 return true 129 } 130 131 if buildTagGoVersion(tag) { 132 return true 133 } 134 135 return false 136 }) 137 } 138 139 func buildTagGoVersion(tag string) bool { 140 vRuntime, err := hcversion.NewVersion(strings.TrimPrefix(runtime.Version(), "go")) 141 if err != nil { 142 return false 143 } 144 145 vTag, err := hcversion.NewVersion(strings.TrimPrefix(tag, "go")) 146 if err != nil { 147 return false 148 } 149 150 return vRuntime.GreaterThanOrEqual(vTag) 151 }