diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index f550475..4d02e69 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -26,11 +26,11 @@ jobs: name: Build runs-on: ubuntu-latest steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 with: - go-version: 1.17 - - - uses: actions/checkout@v3 + go-version: stable - name: Build run: make build diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 3d7ddee..cc9eead 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -28,11 +28,11 @@ jobs: name: lint runs-on: ubuntu-latest steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 with: - go-version: 1.17 - - - uses: actions/checkout@v3 + go-version: stable - name: golangci-lint uses: golangci/golangci-lint-action@v3 diff --git a/.golangci.yml b/.golangci.yml index 0fbf6c0..a0e6fd5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -60,7 +60,6 @@ linters: enable: - asciicheck - bodyclose - - deadcode - dogsled - dupl - durationcheck @@ -100,7 +99,6 @@ linters: - rowserrcheck - sqlclosecheck - staticcheck - - structcheck - stylecheck - testpackage - thelper @@ -109,6 +107,5 @@ linters: - unconvert - unparam - unused - - varcheck - whitespace - wsl diff --git a/.goreleaser.yml b/.goreleaser.yml index 3daecfd..f3675a9 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -3,12 +3,12 @@ builds: env: - CGO_ENABLED=0 archives: -- replacements: - darwin: Darwin - linux: Linux - windows: Windows - 386: i386 - amd64: x86_64 +- name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} checksum: name_template: 'checksums.txt' dockers: @@ -21,7 +21,6 @@ dockers: dockerfile: Dockerfile.goreleaser build_flag_templates: - "--pull" - - "--build-arg=gomodguard_VERSION={{.Version}}" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.name={{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" diff --git a/Dockerfile b/Dockerfile index 719a0eb..2f1d334 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,12 @@ -ARG GO_VERSION=1.14.2 -ARG ALPINE_VERSION=3.11 -ARG gomodguard_VERSION= - # ---- Build container -FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder +FROM golang:alpine AS builder WORKDIR /gomodguard COPY . . RUN apk add --no-cache git RUN go build -o gomodguard cmd/gomodguard/main.go # ---- App container -FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} +FROM golang:alpine WORKDIR / RUN apk --no-cache add ca-certificates COPY --from=builder gomodguard/gomodguard / diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser index 57a042a..ccaaa89 100644 --- a/Dockerfile.goreleaser +++ b/Dockerfile.goreleaser @@ -1,9 +1,5 @@ -ARG GO_VERSION=1.14.2 -ARG ALPINE_VERSION=3.11 -ARG gomodguard_VERSION= - # ---- App container -FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} +FROM golang:alpine WORKDIR / RUN apk --no-cache add ca-certificates COPY gomodguard /gomodguard diff --git a/Makefile b/Makefile index 7666757..5235d5a 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,10 @@ cover: dockerrun: dockerbuild docker run -v "${current_dir}/.gomodguard.yaml:/.gomodguard.yaml" ryancurrah/gomodguard:latest +.PHONY: snapshot +snapshot: + goreleaser --rm-dist --snapshot + .PHONY: release release: goreleaser --rm-dist @@ -39,4 +43,4 @@ install-tools-mac: .PHONY: install-go-tools install-go-tools: - go get github.com/t-yuki/gocover-cobertura + go install -v github.com/t-yuki/gocover-cobertura diff --git a/README.md b/README.md index 8e2e416..4945f01 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ Resulting checkstyle file ## Install ``` -go get -u github.com/ryancurrah/gomodguard/cmd/gomodguard +go install github.com/ryancurrah/gomodguard/cmd/gomodguard ``` ## Develop diff --git a/_example/.gomodguard.yaml b/_example/.gomodguard.yaml index 8581f90..dc01612 100644 --- a/_example/.gomodguard.yaml +++ b/_example/.gomodguard.yaml @@ -11,7 +11,7 @@ blocked: modules: # List of blocked modules - github.com/uudashr/go-module: # Blocked module recommendations: # Recommended modules that should be used instead (Optional) - - golang.org/x/mod + - golang.org/x/mod reason: "`mod` is the official go.mod parser library." # Reason why the recommended module should be used (Optional) - github.com/gofrs/uuid: recommendations: diff --git a/_example/blocked_example.go b/_example/blocked_example.go index 13862d2..cb321b1 100644 --- a/_example/blocked_example.go +++ b/_example/blocked_example.go @@ -1,7 +1,7 @@ package gomodguard import ( - "io/ioutil" + "os" "github.com/gofrs/uuid" "github.com/mitchellh/go-homedir" @@ -9,8 +9,8 @@ import ( module "github.com/uudashr/go-module" ) -func aBlockedImport() { // nolint: deadcode,unused - b, err := ioutil.ReadFile("go.mod") +func aBlockedImport() { //nolint: deadcode,unused + b, err := os.ReadFile("go.mod") if err != nil { panic(err) } diff --git a/allowed.go b/allowed.go new file mode 100644 index 0000000..5b0d26f --- /dev/null +++ b/allowed.go @@ -0,0 +1,39 @@ +package gomodguard + +import "strings" + +// Allowed is a list of modules and module +// domains that are allowed to be used. +type Allowed struct { + Modules []string `yaml:"modules"` + Domains []string `yaml:"domains"` +} + +// IsAllowedModule returns true if the given module +// name is in the allowed modules list. +func (a *Allowed) IsAllowedModule(moduleName string) bool { + allowedModules := a.Modules + + for i := range allowedModules { + if strings.TrimSpace(moduleName) == strings.TrimSpace(allowedModules[i]) { + return true + } + } + + return false +} + +// IsAllowedModuleDomain returns true if the given modules domain is +// in the allowed module domains list. +func (a *Allowed) IsAllowedModuleDomain(moduleName string) bool { + allowedDomains := a.Domains + + for i := range allowedDomains { + if strings.HasPrefix(strings.TrimSpace(strings.ToLower(moduleName)), + strings.TrimSpace(strings.ToLower(allowedDomains[i]))) { + return true + } + } + + return false +} diff --git a/allowed_test.go b/allowed_test.go new file mode 100644 index 0000000..d042b73 --- /dev/null +++ b/allowed_test.go @@ -0,0 +1,70 @@ +package gomodguard_test + +import ( + "reflect" + "testing" + + "github.com/ryancurrah/gomodguard" +) + +func TestAllowedIsAllowedModule(t *testing.T) { + var tests = []struct { + testName string + allowedModules gomodguard.Allowed + lintedModuleName string + wantIsAllowedModule bool + }{ + { + "module is allowed", + gomodguard.Allowed{Modules: []string{"github.com/someallowed/module"}}, + "github.com/someallowed/module", + true, + }, + { + "module not allowed", + gomodguard.Allowed{}, + "github.com/someblocked/module", + false, + }, + } + + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + isAllowedModule := tt.allowedModules.IsAllowedModule(tt.lintedModuleName) + if !reflect.DeepEqual(isAllowedModule, tt.wantIsAllowedModule) { + t.Errorf("got '%v' want '%v'", isAllowedModule, tt.wantIsAllowedModule) + } + }) + } +} + +func TestAllowedIsAllowedModuleDomain(t *testing.T) { + var tests = []struct { + testName string + allowedModules gomodguard.Allowed + lintedModuleName string + wantIsAllowedModuleDomain bool + }{ + { + "module is allowed", + gomodguard.Allowed{Domains: []string{"github.com"}}, + "github.com/someallowed/module", + true, + }, + { + "module not allowed", + gomodguard.Allowed{}, + "github.com/someblocked/module", + false, + }, + } + + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + isAllowedModuleDomain := tt.allowedModules.IsAllowedModuleDomain(tt.lintedModuleName) + if !reflect.DeepEqual(isAllowedModuleDomain, tt.wantIsAllowedModuleDomain) { + t.Errorf("got '%v' want '%v'", isAllowedModuleDomain, tt.wantIsAllowedModuleDomain) + } + }) + } +} diff --git a/blocked.go b/blocked.go new file mode 100644 index 0000000..2a6e5c2 --- /dev/null +++ b/blocked.go @@ -0,0 +1,189 @@ +package gomodguard + +import ( + "fmt" + "strings" + + "github.com/Masterminds/semver" +) + +// Blocked is a list of modules that are +// blocked and not to be used. +type Blocked struct { + Modules BlockedModules `yaml:"modules"` + Versions BlockedVersions `yaml:"versions"` + LocalReplaceDirectives bool `yaml:"local_replace_directives"` +} + +// BlockedVersion has a version constraint a reason why the the module version is blocked. +type BlockedVersion struct { + Version string `yaml:"version"` + Reason string `yaml:"reason"` +} + +// IsLintedModuleVersionBlocked returns true if a version constraint is specified and the +// linted module version matches the constraint. +func (r *BlockedVersion) IsLintedModuleVersionBlocked(lintedModuleVersion string) bool { + if r.Version == "" { + return false + } + + constraint, err := semver.NewConstraint(r.Version) + if err != nil { + return false + } + + version, err := semver.NewVersion(lintedModuleVersion) + if err != nil { + return false + } + + meet := constraint.Check(version) + + return meet +} + +// Message returns the reason why the module version is blocked. +func (r *BlockedVersion) Message(lintedModuleVersion string) string { + var sb strings.Builder + + // Add version contraint to message. + _, _ = fmt.Fprintf(&sb, "version `%s` is blocked because it does not meet the version constraint `%s`.", + lintedModuleVersion, r.Version) + + if r.Reason == "" { + return sb.String() + } + + // Add reason to message. + _, _ = fmt.Fprintf(&sb, " %s.", strings.TrimRight(r.Reason, ".")) + + return sb.String() +} + +// BlockedModule has alternative modules to use and a reason why the module is blocked. +type BlockedModule struct { + Recommendations []string `yaml:"recommendations"` + Reason string `yaml:"reason"` +} + +// IsCurrentModuleARecommendation returns true if the current module is in the Recommendations list. +// +// If the current go.mod file being linted is a recommended module of a +// blocked module and it imports that blocked module, do not set as blocked. +// This could mean that the linted module is a wrapper for that blocked module. +func (r *BlockedModule) IsCurrentModuleARecommendation(currentModuleName string) bool { + if r == nil { + return false + } + + for n := range r.Recommendations { + if strings.TrimSpace(currentModuleName) == strings.TrimSpace(r.Recommendations[n]) { + return true + } + } + + return false +} + +// Message returns the reason why the module is blocked and a list of recommended modules if provided. +func (r *BlockedModule) Message() string { + var sb strings.Builder + + // Add recommendations to message + for i := range r.Recommendations { + switch { + case len(r.Recommendations) == 1: + _, _ = fmt.Fprintf(&sb, "`%s` is a recommended module.", r.Recommendations[i]) + case (i+1) != len(r.Recommendations) && (i+1) == (len(r.Recommendations)-1): + _, _ = fmt.Fprintf(&sb, "`%s` ", r.Recommendations[i]) + case (i + 1) != len(r.Recommendations): + _, _ = fmt.Fprintf(&sb, "`%s`, ", r.Recommendations[i]) + default: + _, _ = fmt.Fprintf(&sb, "and `%s` are recommended modules.", r.Recommendations[i]) + } + } + + if r.Reason == "" { + return sb.String() + } + + // Add reason to message + if sb.Len() == 0 { + _, _ = fmt.Fprintf(&sb, "%s.", strings.TrimRight(r.Reason, ".")) + } else { + _, _ = fmt.Fprintf(&sb, " %s.", strings.TrimRight(r.Reason, ".")) + } + + return sb.String() +} + +// HasRecommendations returns true if the blocked package has +// recommended modules. +func (r *BlockedModule) HasRecommendations() bool { + if r == nil { + return false + } + + return len(r.Recommendations) > 0 +} + +// BlockedVersions a list of blocked modules by a version constraint. +type BlockedVersions []map[string]BlockedVersion + +// Get returns the module names that are blocked. +func (b BlockedVersions) Get() []string { + modules := make([]string, len(b)) + + for n := range b { + for module := range b[n] { + modules[n] = module + break + } + } + + return modules +} + +// GetBlockReason returns a block version if one is set for the provided linted module name. +func (b BlockedVersions) GetBlockReason(lintedModuleName string) *BlockedVersion { + for _, blockedModule := range b { + for blockedModuleName, blockedVersion := range blockedModule { + if strings.TrimSpace(lintedModuleName) == strings.TrimSpace(blockedModuleName) { + return &blockedVersion + } + } + } + + return nil +} + +// BlockedModules a list of blocked modules. +type BlockedModules []map[string]BlockedModule + +// Get returns the module names that are blocked. +func (b BlockedModules) Get() []string { + modules := make([]string, len(b)) + + for n := range b { + for module := range b[n] { + modules[n] = module + break + } + } + + return modules +} + +// GetBlockReason returns a block module if one is set for the provided linted module name. +func (b BlockedModules) GetBlockReason(lintedModuleName string) *BlockedModule { + for _, blockedModule := range b { + for blockedModuleName, blockedModule := range blockedModule { + if strings.TrimSpace(lintedModuleName) == strings.TrimSpace(blockedModuleName) { + return &blockedModule + } + } + } + + return nil +} diff --git a/gomodguard_test.go b/blocked_test.go similarity index 57% rename from gomodguard_test.go rename to blocked_test.go index fc99bf5..0050997 100644 --- a/gomodguard_test.go +++ b/blocked_test.go @@ -1,9 +1,6 @@ -// nolint:scopelint package gomodguard_test import ( - "fmt" - "os" "reflect" "strings" "testing" @@ -11,33 +8,6 @@ import ( "github.com/ryancurrah/gomodguard" ) -var ( - config *gomodguard.Configuration - cwd string -) - -func TestMain(m *testing.M) { - err := os.Chdir("_example") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - cwd, err = os.Getwd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - config, err = gomodguard.GetConfig(".gomodguard.yaml") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - os.Exit(m.Run()) -} - func TestBlockedModuleIsAllowed(t *testing.T) { var tests = []struct { testName string @@ -286,155 +256,3 @@ func TestBlockedModulesGetBlockedModule(t *testing.T) { }) } } - -func TestAllowedIsAllowedModule(t *testing.T) { - var tests = []struct { - testName string - allowedModules gomodguard.Allowed - lintedModuleName string - wantIsAllowedModule bool - }{ - { - "module is allowed", - gomodguard.Allowed{Modules: []string{"github.com/someallowed/module"}}, - "github.com/someallowed/module", - true, - }, - { - "module not allowed", - gomodguard.Allowed{}, - "github.com/someblocked/module", - false, - }, - } - - for _, tt := range tests { - t.Run(tt.testName, func(t *testing.T) { - isAllowedModule := tt.allowedModules.IsAllowedModule(tt.lintedModuleName) - if !reflect.DeepEqual(isAllowedModule, tt.wantIsAllowedModule) { - t.Errorf("got '%v' want '%v'", isAllowedModule, tt.wantIsAllowedModule) - } - }) - } -} - -func TestAllowedIsAllowedModuleDomain(t *testing.T) { - var tests = []struct { - testName string - allowedModules gomodguard.Allowed - lintedModuleName string - wantIsAllowedModuleDomain bool - }{ - { - "module is allowed", - gomodguard.Allowed{Domains: []string{"github.com"}}, - "github.com/someallowed/module", - true, - }, - { - "module not allowed", - gomodguard.Allowed{}, - "github.com/someblocked/module", - false, - }, - } - - for _, tt := range tests { - t.Run(tt.testName, func(t *testing.T) { - isAllowedModuleDomain := tt.allowedModules.IsAllowedModuleDomain(tt.lintedModuleName) - if !reflect.DeepEqual(isAllowedModuleDomain, tt.wantIsAllowedModuleDomain) { - t.Errorf("got '%v' want '%v'", isAllowedModuleDomain, tt.wantIsAllowedModuleDomain) - } - }) - } -} - -func TestResultString(t *testing.T) { - var tests = []struct { - testName string - result gomodguard.Issue - wantString string - }{ - { - "reason lint failed", - gomodguard.Issue{FileName: "test.go", LineNumber: 1, Reason: "Some reason."}, - "test.go:1:1 Some reason.", - }, - } - - for _, tt := range tests { - t.Run(tt.testName, func(t *testing.T) { - result := tt.result.String() - if !strings.EqualFold(result, tt.wantString) { - t.Errorf("got '%s' want '%s'", result, tt.wantString) - } - }) - } -} - -func TestProcessorNewProcessor(t *testing.T) { - _, err := gomodguard.NewProcessor(config) - if err != nil { - t.Error(err) - } -} - -func TestProcessorProcessFiles(t *testing.T) { - processor, err := gomodguard.NewProcessor(config) - if err != nil { - t.Error(err) - } - - filteredFiles := gomodguard.GetFilteredFiles(cwd, false, []string{"./..."}) - - var tests = []struct { - testName string - processor gomodguard.Processor - wantReason string - }{ - { - "module blocked because of recommendation", - gomodguard.Processor{Config: config, Modfile: processor.Modfile}, - "blocked_example.go:9:1 import of package `github.com/uudashr/go-module` is blocked because the " + - "module is in the blocked modules list. `golang.org/x/mod` is a recommended module. `mod` " + - "is the official go.mod parser library.", - }, - { - "module blocked because of version constraint", - gomodguard.Processor{Config: config, Modfile: processor.Modfile}, - "blocked_example.go:7:1 import of package `github.com/mitchellh/go-homedir` is blocked because " + - "the module is in the blocked modules list. version `v1.1.0` is blocked because it does not " + - "meet the version constraint `<= 1.1.0`. testing if blocked version constraint works.", - }, - { - "module blocked because of local replace directive", - gomodguard.Processor{Config: config, Modfile: processor.Modfile}, - "blocked_example.go:8:1 import of package `github.com/ryancurrah/gomodguard` is blocked because " + - "the module has a local replace directive.", - }, - } - - for _, tt := range tests { - t.Run(tt.testName, func(t *testing.T) { - tt.processor.SetBlockedModules() - results := tt.processor.ProcessFiles(filteredFiles) - if len(results) == 0 { - t.Fatal("result should be greater than zero") - } - - foundWantReason := false - allReasons := make([]string, 0, len(results)) - for _, result := range results { - allReasons = append(allReasons, result.String()) - - if strings.EqualFold(result.String(), tt.wantReason) { - foundWantReason = true - } - } - - if !foundWantReason { - t.Errorf("got '%+v' want '%s'", allReasons, tt.wantReason) - } - }) - } -} diff --git a/cmd/gomodguard/main.go b/cmd/gomodguard/main.go index caa3a92..92aea86 100644 --- a/cmd/gomodguard/main.go +++ b/cmd/gomodguard/main.go @@ -3,9 +3,9 @@ package main import ( "os" - "github.com/ryancurrah/gomodguard" + "github.com/ryancurrah/gomodguard/internal/cli" ) func main() { - os.Exit(gomodguard.Run()) + os.Exit(cli.Run()) } diff --git a/cmd_test.go b/cmd_test.go deleted file mode 100644 index 99cfafa..0000000 --- a/cmd_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package gomodguard_test - -import ( - "testing" - - "github.com/ryancurrah/gomodguard" -) - -func TestCmdRun(t *testing.T) { - wantExitCode := 2 - exitCode := gomodguard.Run() - - if exitCode != wantExitCode { - t.Errorf("got exit code '%d' want '%d'", exitCode, wantExitCode) - } -} diff --git a/go.mod b/go.mod index b0e9ce0..4c8dbbd 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,13 @@ module github.com/ryancurrah/gomodguard -go 1.16 +go 1.19 require ( github.com/Masterminds/semver v1.5.0 - github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b + github.com/go-xmlfmt/xmlfmt v1.1.2 github.com/mitchellh/go-homedir v1.1.0 github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d + github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c golang.org/x/mod v0.7.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 426b168..5c1863f 100644 --- a/go.sum +++ b/go.sum @@ -1,36 +1,15 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo= -github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U= +github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d h1:CdDQnGF8Nq9ocOS/xlSptM1N3BbrA6/kmaep5ggwaIA= github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c h1:+aPplBwWcHBo6q9xrfWdMrT9o4kltkmmvpemgIjep/8= +github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c/go.mod h1:SbErYREK7xXdsRiigaQiQkI9McGRzYMvlKYaP3Nimdk= golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/cmd.go b/internal/cli/cli.go similarity index 71% rename from cmd.go rename to internal/cli/cli.go index a26fac8..8e3a0eb 100644 --- a/cmd.go +++ b/internal/cli/cli.go @@ -1,9 +1,8 @@ -package gomodguard +package cli import ( "flag" "fmt" - "io/ioutil" "log" "os" "path/filepath" @@ -12,6 +11,8 @@ import ( "github.com/go-xmlfmt/xmlfmt" "github.com/mitchellh/go-homedir" "github.com/phayes/checkstyle" + "github.com/ryancurrah/gomodguard" + "github.com/ryancurrah/gomodguard/internal/filesearch" "gopkg.in/yaml.v2" ) @@ -28,6 +29,7 @@ var ( ) // Run the gomodguard linter. Returns the exit code to use. +// //nolint:funlen func Run() int { var ( @@ -82,9 +84,9 @@ func Run() int { logger.Fatalf("error: %s", err) } - filteredFiles := GetFilteredFiles(cwd, noTest, args) + filteredFiles := filesearch.Find(cwd, noTest, args) - processor, err := NewProcessor(config) + processor, err := gomodguard.NewProcessor(config) if err != nil { logger.Fatalf("error: %s", err) } @@ -115,8 +117,8 @@ func Run() int { } // GetConfig from YAML file. -func GetConfig(configFile string) (*Configuration, error) { - config := Configuration{} +func GetConfig(configFile string) (*gomodguard.Configuration, error) { + config := gomodguard.Configuration{} home, err := homedir.Dir() if err != nil { @@ -135,7 +137,7 @@ func GetConfig(configFile string) (*Configuration, error) { return nil, fmt.Errorf("%w: %s %s", errFindingConfigFile, configFile, homeDirCfgFile) } - data, err := ioutil.ReadFile(cfgFile) + data, err := os.ReadFile(cfgFile) if err != nil { return nil, fmt.Errorf(errReadingConfigFile, err) } @@ -148,47 +150,6 @@ func GetConfig(configFile string) (*Configuration, error) { return &config, nil } -// GetFilteredFiles returns files based on search string arguments and filters. -func GetFilteredFiles(cwd string, skipTests bool, args []string) []string { - var ( - foundFiles = []string{} - filteredFiles = []string{} - ) - - for _, f := range args { - if strings.HasSuffix(f, "/...") { - dir, _ := filepath.Split(f) - - foundFiles = append(foundFiles, expandGoWildcard(dir)...) - - continue - } - - if _, err := os.Stat(f); err == nil { - foundFiles = append(foundFiles, f) - } - } - - // Use relative path to print shorter names, sort out test foundFiles if chosen. - for _, f := range foundFiles { - if skipTests { - if strings.HasSuffix(f, "_test.go") { - continue - } - } - - if relativePath, err := filepath.Rel(cwd, f); err == nil { - filteredFiles = append(filteredFiles, relativePath) - - continue - } - - filteredFiles = append(filteredFiles, f) - } - - return filteredFiles -} - // showHelp text for command line. func showHelp() { helpText := `Usage: gomodguard [files...] @@ -199,7 +160,7 @@ Flags:` } // WriteCheckstyle takes the results and writes them to a checkstyle formated file. -func WriteCheckstyle(checkstyleFilePath string, results []Issue) error { +func WriteCheckstyle(checkstyleFilePath string, results []gomodguard.Issue) error { check := checkstyle.New() for i := range results { @@ -210,7 +171,7 @@ func WriteCheckstyle(checkstyleFilePath string, results []Issue) error { checkstyleXML := fmt.Sprintf("\n%s", check.String()) - err := ioutil.WriteFile(checkstyleFilePath, []byte(xmlfmt.FormatXML(checkstyleXML, "", " ")), 0644) // nolint:gosec + err := os.WriteFile(checkstyleFilePath, []byte(xmlfmt.FormatXML(checkstyleXML, "", " ")), 0644) //nolint:gosec if err != nil { return err } @@ -227,21 +188,3 @@ func fileExists(filename string) bool { return !info.IsDir() } - -// expandGoWildcard path provided. -func expandGoWildcard(root string) []string { - foundFiles := []string{} - - _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - // Only append go foundFiles. - if !strings.HasSuffix(info.Name(), ".go") { - return nil - } - - foundFiles = append(foundFiles, path) - - return nil - }) - - return foundFiles -} diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go new file mode 100644 index 0000000..427b984 --- /dev/null +++ b/internal/cli/cli_test.go @@ -0,0 +1,28 @@ +package cli_test + +import ( + "fmt" + "os" + "testing" + + "github.com/ryancurrah/gomodguard/internal/cli" +) + +func TestMain(m *testing.M) { + err := os.Chdir("../../_example") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + os.Exit(m.Run()) +} + +func TestCmdRun(t *testing.T) { + wantExitCode := 2 + exitCode := cli.Run() + + if exitCode != wantExitCode { + t.Errorf("got exit code '%d' want '%d'", exitCode, wantExitCode) + } +} diff --git a/internal/filesearch/filesearch.go b/internal/filesearch/filesearch.go new file mode 100644 index 0000000..82e7aa2 --- /dev/null +++ b/internal/filesearch/filesearch.go @@ -0,0 +1,66 @@ +package filesearch + +import ( + "os" + "path/filepath" + "strings" +) + +// Find returns files based on search string arguments and filters. +func Find(cwd string, skipTests bool, args []string) []string { + var ( + foundFiles = []string{} + filteredFiles = []string{} + ) + + for _, f := range args { + if strings.HasSuffix(f, "/...") { + dir, _ := filepath.Split(f) + + foundFiles = append(foundFiles, expandGoWildcard(dir)...) + + continue + } + + if _, err := os.Stat(f); err == nil { + foundFiles = append(foundFiles, f) + } + } + + // Use relative path to print shorter names, sort out test foundFiles if chosen. + for _, f := range foundFiles { + if skipTests { + if strings.HasSuffix(f, "_test.go") { + continue + } + } + + if relativePath, err := filepath.Rel(cwd, f); err == nil { + filteredFiles = append(filteredFiles, relativePath) + + continue + } + + filteredFiles = append(filteredFiles, f) + } + + return filteredFiles +} + +// expandGoWildcard path provided. +func expandGoWildcard(root string) []string { + foundFiles := []string{} + + _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + // Only append go foundFiles. + if !strings.HasSuffix(info.Name(), ".go") { + return nil + } + + foundFiles = append(foundFiles, path) + + return nil + }) + + return foundFiles +} diff --git a/internal_test.go b/internal_test.go deleted file mode 100644 index 849d115..0000000 --- a/internal_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package gomodguard - -import "testing" - -func TestIsModuleBlocked(t *testing.T) { - var tests = []struct { - testName string - processor Processor - testModule string - }{ - { - "previous version blocked", - Processor{ - blockedModulesFromModFile: map[string][]string{ - "github.com/foo/bar": {blockReasonNotInAllowedList}, - }, - }, - "github.com/foo/bar/v2", - }, - } - - for _, tt := range tests { - t.Run(tt.testName, func(t *testing.T) { - blockReasons := tt.processor.isBlockedPackageFromModFile(tt.testModule) - if len(blockReasons) > 0 { - t.Logf("Testing %v, expected allowed, was blocked: %v", tt.testModule, blockReasons) - t.Fail() - } - }) - } -} diff --git a/issue.go b/issue.go new file mode 100644 index 0000000..d60fc3a --- /dev/null +++ b/issue.go @@ -0,0 +1,20 @@ +package gomodguard + +import ( + "fmt" + "go/token" +) + +// Issue represents the result of one error. +type Issue struct { + FileName string + LineNumber int + Position token.Position + Reason string +} + +// String returns the filename, line +// number and reason of a Issue. +func (r *Issue) String() string { + return fmt.Sprintf("%s:%d:1 %s", r.FileName, r.LineNumber, r.Reason) +} diff --git a/issue_test.go b/issue_test.go new file mode 100644 index 0000000..ac4b84c --- /dev/null +++ b/issue_test.go @@ -0,0 +1,31 @@ +package gomodguard_test + +import ( + "strings" + "testing" + + "github.com/ryancurrah/gomodguard" +) + +func TestResultString(t *testing.T) { + var tests = []struct { + testName string + result gomodguard.Issue + wantString string + }{ + { + "reason lint failed", + gomodguard.Issue{FileName: "test.go", LineNumber: 1, Reason: "Some reason."}, + "test.go:1:1 Some reason.", + }, + } + + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + result := tt.result.String() + if !strings.EqualFold(result, tt.wantString) { + t.Errorf("got '%s' want '%s'", result, tt.wantString) + } + }) + } +} diff --git a/gomodguard.go b/processor.go similarity index 51% rename from gomodguard.go rename to processor.go index efd0d17..8457e3b 100644 --- a/gomodguard.go +++ b/processor.go @@ -7,14 +7,11 @@ import ( "fmt" "go/parser" "go/token" - "io/ioutil" "os" "os/exec" "regexp" "strings" - "github.com/Masterminds/semver" - "golang.org/x/mod/modfile" ) @@ -33,248 +30,17 @@ var ( "local replace directive." // startsWithVersion is used to test when a string begins with the version identifier of a module, - // after having stripped the prefix base module name. IE "github.com/foo/bar/v2/baz" => "/v2/baz" + // after having stripped the prefix base module name. IE "github.com/foo/bar/v2/baz" => "v2/baz" // probably indicates that the module is actually github.com/foo/bar/v2, not github.com/foo/bar. - startsWithVersion = regexp.MustCompile(`^\/v[0-9]+`) + startsWithVersion = regexp.MustCompile(`^v[0-9]+`) ) -// BlockedVersion has a version constraint a reason why the the module version is blocked. -type BlockedVersion struct { - Version string `yaml:"version"` - Reason string `yaml:"reason"` -} - -// IsLintedModuleVersionBlocked returns true if a version constraint is specified and the -// linted module version matches the constraint. -func (r *BlockedVersion) IsLintedModuleVersionBlocked(lintedModuleVersion string) bool { - if r.Version == "" { - return false - } - - constraint, err := semver.NewConstraint(r.Version) - if err != nil { - return false - } - - version, err := semver.NewVersion(lintedModuleVersion) - if err != nil { - return false - } - - meet := constraint.Check(version) - - return meet -} - -// Message returns the reason why the module version is blocked. -func (r *BlockedVersion) Message(lintedModuleVersion string) string { - var sb strings.Builder - - // Add version contraint to message. - _, _ = fmt.Fprintf(&sb, "version `%s` is blocked because it does not meet the version constraint `%s`.", - lintedModuleVersion, r.Version) - - if r.Reason == "" { - return sb.String() - } - - // Add reason to message. - _, _ = fmt.Fprintf(&sb, " %s.", strings.TrimRight(r.Reason, ".")) - - return sb.String() -} - -// BlockedModule has alternative modules to use and a reason why the module is blocked. -type BlockedModule struct { - Recommendations []string `yaml:"recommendations"` - Reason string `yaml:"reason"` -} - -// IsCurrentModuleARecommendation returns true if the current module is in the Recommendations list. -// -// If the current go.mod file being linted is a recommended module of a -// blocked module and it imports that blocked module, do not set as blocked. -// This could mean that the linted module is a wrapper for that blocked module. -func (r *BlockedModule) IsCurrentModuleARecommendation(currentModuleName string) bool { - if r == nil { - return false - } - - for n := range r.Recommendations { - if strings.TrimSpace(currentModuleName) == strings.TrimSpace(r.Recommendations[n]) { - return true - } - } - - return false -} - -// Message returns the reason why the module is blocked and a list of recommended modules if provided. -func (r *BlockedModule) Message() string { - var sb strings.Builder - - // Add recommendations to message - for i := range r.Recommendations { - switch { - case len(r.Recommendations) == 1: - _, _ = fmt.Fprintf(&sb, "`%s` is a recommended module.", r.Recommendations[i]) - case (i+1) != len(r.Recommendations) && (i+1) == (len(r.Recommendations)-1): - _, _ = fmt.Fprintf(&sb, "`%s` ", r.Recommendations[i]) - case (i + 1) != len(r.Recommendations): - _, _ = fmt.Fprintf(&sb, "`%s`, ", r.Recommendations[i]) - default: - _, _ = fmt.Fprintf(&sb, "and `%s` are recommended modules.", r.Recommendations[i]) - } - } - - if r.Reason == "" { - return sb.String() - } - - // Add reason to message - if sb.Len() == 0 { - _, _ = fmt.Fprintf(&sb, "%s.", strings.TrimRight(r.Reason, ".")) - } else { - _, _ = fmt.Fprintf(&sb, " %s.", strings.TrimRight(r.Reason, ".")) - } - - return sb.String() -} - -// HasRecommendations returns true if the blocked package has -// recommended modules. -func (r *BlockedModule) HasRecommendations() bool { - if r == nil { - return false - } - - return len(r.Recommendations) > 0 -} - -// BlockedVersions a list of blocked modules by a version constraint. -type BlockedVersions []map[string]BlockedVersion - -// Get returns the module names that are blocked. -func (b BlockedVersions) Get() []string { - modules := make([]string, len(b)) - - for n := range b { - for module := range b[n] { - modules[n] = module - break - } - } - - return modules -} - -// GetBlockReason returns a block version if one is set for the provided linted module name. -func (b BlockedVersions) GetBlockReason(lintedModuleName string) *BlockedVersion { - for _, blockedModule := range b { - for blockedModuleName, blockedVersion := range blockedModule { - if strings.TrimSpace(lintedModuleName) == strings.TrimSpace(blockedModuleName) { - return &blockedVersion - } - } - } - - return nil -} - -// BlockedModules a list of blocked modules. -type BlockedModules []map[string]BlockedModule - -// Get returns the module names that are blocked. -func (b BlockedModules) Get() []string { - modules := make([]string, len(b)) - - for n := range b { - for module := range b[n] { - modules[n] = module - break - } - } - - return modules -} - -// GetBlockReason returns a block module if one is set for the provided linted module name. -func (b BlockedModules) GetBlockReason(lintedModuleName string) *BlockedModule { - for _, blockedModule := range b { - for blockedModuleName, blockedModule := range blockedModule { - if strings.TrimSpace(lintedModuleName) == strings.TrimSpace(blockedModuleName) { - return &blockedModule - } - } - } - - return nil -} - -// Allowed is a list of modules and module -// domains that are allowed to be used. -type Allowed struct { - Modules []string `yaml:"modules"` - Domains []string `yaml:"domains"` -} - -// IsAllowedModule returns true if the given module -// name is in the allowed modules list. -func (a *Allowed) IsAllowedModule(moduleName string) bool { - allowedModules := a.Modules - - for i := range allowedModules { - if strings.TrimSpace(moduleName) == strings.TrimSpace(allowedModules[i]) { - return true - } - } - - return false -} - -// IsAllowedModuleDomain returns true if the given modules domain is -// in the allowed module domains list. -func (a *Allowed) IsAllowedModuleDomain(moduleName string) bool { - allowedDomains := a.Domains - - for i := range allowedDomains { - if strings.HasPrefix(strings.TrimSpace(strings.ToLower(moduleName)), - strings.TrimSpace(strings.ToLower(allowedDomains[i]))) { - return true - } - } - - return false -} - -// Blocked is a list of modules that are -// blocked and not to be used. -type Blocked struct { - Modules BlockedModules `yaml:"modules"` - Versions BlockedVersions `yaml:"versions"` - LocalReplaceDirectives bool `yaml:"local_replace_directives"` -} - // Configuration of gomodguard allow and block lists. type Configuration struct { Allowed Allowed `yaml:"allowed"` Blocked Blocked `yaml:"blocked"` } -// Issue represents the result of one error. -type Issue struct { - FileName string - LineNumber int - Position token.Position - Reason string -} - -// String returns the filename, line -// number and reason of a Issue. -func (r *Issue) String() string { - return fmt.Sprintf("%s:%d:1 %s", r.FileName, r.LineNumber, r.Reason) -} - // Processor processes Go files. type Processor struct { Config *Configuration @@ -308,7 +74,7 @@ func NewProcessor(config *Configuration) (*Processor, error) { // and lints them. func (p *Processor) ProcessFiles(filenames []string) (issues []Issue) { for _, filename := range filenames { - data, err := ioutil.ReadFile(filename) + data, err := os.ReadFile(filename) if err != nil { issues = append(issues, Issue{ FileName: filename, @@ -443,14 +209,7 @@ func (p *Processor) SetBlockedModules() { //nolint:gocognit,funlen // isBlockedPackageFromModFile returns the block reason if the package is blocked. func (p *Processor) isBlockedPackageFromModFile(packageName string) []string { for blockedModuleName, blockReasons := range p.blockedModulesFromModFile { - if strings.HasPrefix(strings.TrimSpace(packageName), strings.TrimSpace(blockedModuleName)) { - // Test if a versioned module matched its base version - // ie github.com/foo/bar/v2 matched github.com/foo/bar, even though the former may be allowed. - suffix := strings.TrimPrefix(strings.TrimSpace(packageName), strings.TrimSpace(blockedModuleName)) - if startsWithVersion.MatchString(suffix) { - continue - } - + if isPackageInModule(packageName, blockedModuleName) { formattedReasons := make([]string, 0, len(blockReasons)) for _, blockReason := range blockReasons { @@ -470,7 +229,7 @@ func loadGoModFile() ([]byte, error) { _ = cmd.Start() if stdout == nil { - return ioutil.ReadFile(goModFilename) + return os.ReadFile(goModFilename) } buf := new(bytes.Buffer) @@ -480,20 +239,53 @@ func loadGoModFile() ([]byte, error) { err := json.Unmarshal(buf.Bytes(), &goEnv) if err != nil { - return ioutil.ReadFile(goModFilename) + return os.ReadFile(goModFilename) } if _, ok := goEnv["GOMOD"]; !ok { - return ioutil.ReadFile(goModFilename) + return os.ReadFile(goModFilename) } if _, err = os.Stat(goEnv["GOMOD"]); os.IsNotExist(err) { - return ioutil.ReadFile(goModFilename) + return os.ReadFile(goModFilename) } if goEnv["GOMOD"] == "/dev/null" { return nil, errors.New("current working directory must have a go.mod file") } - return ioutil.ReadFile(goEnv["GOMOD"]) + return os.ReadFile(goEnv["GOMOD"]) +} + +// isPackageInModule determines if a package is apart of the specified go module. +func isPackageInModule(pkg, mod string) bool { + // Split pkg and mod paths into parts + pkgPart := strings.Split(pkg, "/") + modPart := strings.Split(mod, "/") + + pkgPartMatches := 0 + + // Count number of times pkg path matches the mod path + for i, m := range modPart { + if len(pkgPart) > i && pkgPart[i] == m { + pkgPartMatches++ + } + } + + // If pkgPartMatches are not the same length as modPart + // than the package is not in this module + if pkgPartMatches != len(modPart) { + return false + } + + if len(pkgPart) > len(modPart) { + // If pkgPart path starts with a major version + // than the package is not in this module as + // major versions are completely different modules + if startsWithVersion.MatchString(pkgPart[len(modPart)]) { + return false + } + } + + return true } diff --git a/processor_internal_test.go b/processor_internal_test.go new file mode 100644 index 0000000..604890b --- /dev/null +++ b/processor_internal_test.go @@ -0,0 +1,170 @@ +package gomodguard + +import ( + "testing" +) + +func TestIsModuleBlocked(t *testing.T) { + var tests = []struct { + testName string + processor Processor + testModule string + wantBlockReasons int + }{ + { + "previous version blocked", + Processor{ + blockedModulesFromModFile: map[string][]string{ + "github.com/foo/bar": {blockReasonNotInAllowedList}, + }, + }, + "github.com/foo/bar/v2", + 0, + }, + { + "ensure modules with similar prefixes are not blocked", + Processor{ + blockedModulesFromModFile: map[string][]string{ + "github.com/aws/aws-sdk-go": {blockReasonNotInAllowedList}, + }, + }, + "github.com/aws/aws-sdk-go-v2", + 0, + }, + { + "ensure the same module is blocked", + Processor{ + blockedModulesFromModFile: map[string][]string{ + "github.com/foo/bar/v3": {blockReasonNotInAllowedList}, + }, + }, + "github.com/foo/bar/v3", + 1, + }, + } + + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + blockReasons := tt.processor.isBlockedPackageFromModFile(tt.testModule) + if len(blockReasons) != tt.wantBlockReasons { + t.Logf( + "Testing %v, expected %v blockReasons, got %v blockReasons", + tt.testModule, + tt.wantBlockReasons, + len(blockReasons), + ) + t.Fail() + } + }) + } +} + +func Test_packageInModule(t *testing.T) { //nolint:funlen + type args struct { + pkg string + mod string + } + + tests := []struct { + name string + args args + wantPkgIsInMod bool + }{ + { + name: "should return bar package path", + args: args{ + pkg: "github.com/acme/foo/bar", + mod: "github.com/acme/foo", + }, + wantPkgIsInMod: true, + }, + { + name: "should return bar/baz package path", + args: args{ + pkg: "github.com/acme/foo/bar/baz", + mod: "github.com/acme/foo", + }, + wantPkgIsInMod: true, + }, + { + name: "aws v1 package should not match v2 module", + args: args{ + pkg: "github.com/aws/aws-sdk-go", + mod: "github.com/aws/aws-sdk-go-v2", + }, + wantPkgIsInMod: false, + }, + { + name: "aws v1 package should not match v2 module with a package path", + args: args{ + pkg: "github.com/aws/aws-sdk-go/foo", + mod: "github.com/aws/aws-sdk-go-v2", + }, + wantPkgIsInMod: false, + }, + { + name: "aws v2 package should match v2 module", + args: args{ + pkg: "github.com/aws/aws-sdk-go-v2", + mod: "github.com/aws/aws-sdk-go-v2", + }, + wantPkgIsInMod: true, + }, + { + name: "aws v2 package should match v2 module with a package path", + args: args{ + pkg: "github.com/aws/aws-sdk-go", + mod: "github.com/aws/aws-sdk-go-v2", + }, + wantPkgIsInMod: false, + }, + { + name: "package path with different major version", + args: args{ + pkg: "github.com/foo/bar/v20", + mod: "github.com/foo/bar", + }, + wantPkgIsInMod: false, + }, + { + name: "package path with no major version", + args: args{ + pkg: "github.com/foo/bar", + mod: "github.com/foo/bar/v10", + }, + wantPkgIsInMod: false, + }, + { + name: "module with different major version", + args: args{ + pkg: "github.com/foo/bar/v40", + mod: "github.com/foo/bar/v41", + }, + wantPkgIsInMod: false, + }, + { + name: "same major version", + args: args{ + pkg: "github.com/foo/bar/v50", + mod: "github.com/foo/bar/v50", + }, + wantPkgIsInMod: true, + }, + { + name: "same major version with path", + args: args{ + pkg: "github.com/foo/bar/v60/baz/taz", + mod: "github.com/foo/bar/v60", + }, + wantPkgIsInMod: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotPkgIsInMod := isPackageInModule(tt.args.pkg, tt.args.mod) + if gotPkgIsInMod != tt.wantPkgIsInMod { + t.Errorf("packageInModule() gotPkgIsInMod = %v, want %v", gotPkgIsInMod, tt.wantPkgIsInMod) + } + }) + } +} diff --git a/processor_test.go b/processor_test.go new file mode 100644 index 0000000..aeb512e --- /dev/null +++ b/processor_test.go @@ -0,0 +1,133 @@ +//nolint:scopelint +package gomodguard_test + +import ( + "os" + "strings" + "testing" + + "github.com/ryancurrah/gomodguard" + "github.com/ryancurrah/gomodguard/internal/filesearch" +) + +func TestProcessorNewProcessor(t *testing.T) { + _, err := gomodguard.NewProcessor(&gomodguard.Configuration{ + Allowed: gomodguard.Allowed{ + Modules: []string{ + "github.com/foo/bar", + }, + }, + }) + if err != nil { + t.Error(err) + } +} + +func TestProcessorProcessFiles(t *testing.T) { //nolint:funlen + err := os.Chdir("_example") + if err != nil { + t.Error(err) + } + + cwd, err := os.Getwd() + if err != nil { + t.Error(err) + } + + config := &gomodguard.Configuration{ + Allowed: gomodguard.Allowed{ + Modules: []string{ + "gopkg.in/yaml.v2", + "github.com/go-xmlfmt/xmlfmt", + "github.com/Masterminds/semver", + "github.com/ryancurrah/gomodguard", + }, + Domains: []string{ + "golang.org", + }, + }, + Blocked: gomodguard.Blocked{ + Modules: gomodguard.BlockedModules{ + { + "github.com/uudashr/go-module": gomodguard.BlockedModule{ + Recommendations: []string{"golang.org/x/mod"}, + Reason: "`mod` is the official go.mod parser library.", + }, + }, + { + "github.com/gofrs/uuid": gomodguard.BlockedModule{ + Recommendations: []string{"github.com/ryancurrah/gomodguard"}, + Reason: "testing if module is not blocked when it is recommended.", + }, + }, + }, + Versions: gomodguard.BlockedVersions{ + { + "github.com/mitchellh/go-homedir": gomodguard.BlockedVersion{ + Version: "<= 1.1.0", + Reason: "testing if blocked version constraint works.", + }, + }, + }, + LocalReplaceDirectives: true, + }, + } + + processor, err := gomodguard.NewProcessor(config) + if err != nil { + t.Error(err) + } + + filteredFiles := filesearch.Find(cwd, false, []string{"./..."}) + + var tests = []struct { + testName string + processor gomodguard.Processor + wantReason string + }{ + { + "module blocked because of recommendation", + gomodguard.Processor{Config: config, Modfile: processor.Modfile}, + "blocked_example.go:9:1 import of package `github.com/uudashr/go-module` is blocked because the " + + "module is in the blocked modules list. `golang.org/x/mod` is a recommended module. `mod` " + + "is the official go.mod parser library.", + }, + { + "module blocked because of version constraint", + gomodguard.Processor{Config: config, Modfile: processor.Modfile}, + "blocked_example.go:7:1 import of package `github.com/mitchellh/go-homedir` is blocked because " + + "the module is in the blocked modules list. version `v1.1.0` is blocked because it does not " + + "meet the version constraint `<= 1.1.0`. testing if blocked version constraint works.", + }, + { + "module blocked because of local replace directive", + gomodguard.Processor{Config: config, Modfile: processor.Modfile}, + "blocked_example.go:8:1 import of package `github.com/ryancurrah/gomodguard` is blocked because " + + "the module has a local replace directive.", + }, + } + + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + tt.processor.SetBlockedModules() + results := tt.processor.ProcessFiles(filteredFiles) + if len(results) == 0 { + t.Fatal("result should be greater than zero") + } + + foundWantReason := false + allReasons := make([]string, 0, len(results)) + for _, result := range results { + allReasons = append(allReasons, result.String()) + + if strings.EqualFold(result.String(), tt.wantReason) { + foundWantReason = true + } + } + + if !foundWantReason { + t.Errorf("got '%+v' want '%s'", allReasons, tt.wantReason) + } + }) + } +} diff --git a/tools.go b/tools.go new file mode 100644 index 0000000..d56bcc7 --- /dev/null +++ b/tools.go @@ -0,0 +1,5 @@ +//go:build tools + +package gomodguard + +import _ "github.com/t-yuki/gocover-cobertura"