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  }