diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7be3962..ac5e0c3 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -12,8 +12,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 with: go-version: "1.18.5" diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index b0d3294..e5a7c74 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -12,13 +12,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 with: go-version: "1.18.5" - name: golangci-lint - uses: golangci/golangci-lint-action@v3.4.0 + uses: golangci/golangci-lint-action@v3.7.0 with: version: v1.47.3 args: --verbose --config .golangci.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 6106937..6b59173 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,2 +1,2 @@ # This repository is maintained by: -* @elrayle @ajhenry +* @elrayle @ajhenry @dangoor diff --git a/README.md b/README.md index 3027123..093b1a4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Go Reference](https://pkg.go.dev/badge/github.com/github/go-spdx/v2@v2.1.2/spdxexp.svg)](https://pkg.go.dev/github.com/github/go-spdx/v2@v2.1.2/spdxexp) + # go-spdx Golang implementation of a checker for determining if a set of SPDX IDs satisfies an SPDX Expression. @@ -64,6 +66,8 @@ to first check if all of the expressions from `allowedList` are valid. #### Examples: Satisfies returns true +[Go Playground for Satisfies](https://go.dev/play/p/Ul8H15hyEpQ) + ```go Satisfies("MIT", []string{"MIT"}) Satisfies("MIT", []string{"MIT", "Apache-2.0"}) @@ -119,6 +123,39 @@ assert.Contains(invalidLicenses, "NON-EXISTENT-LICENSE") assert.NotContains(invalidLicenses, "MIT") ``` +#### Examples: ValidateLicenses works with SPDX expressions + +```go +valid, invalidLicenses := ValidateLicenses([]string{"MIT AND APACHE-2.0"}) +assert.True(valid) +assert.NotContains(invalidLicenses, "MIT AND APACHE-2.0") +``` + +### ExtractLicenses + +```go +func ExtractLicenses(expression string) ([]string, error) +``` + +Function `ExtractLicenses` is used to extract licenses from the given expression without duplicates. + +**parameter: expression** + +`expression` is an SPDX expression string. + +**returns** + +Function `ExtractLicenses` has 2 return values. First is `[]string` which contains all of the SPDX licenses without duplicates. + +The second return value is an `error` which is not `nil` if the given expression is not a valid SPDX expression. + +#### Example + +```go +licenses, err := ExtractLicenses("(MIT AND APACHE-2.0) OR (APACHE-2.0)") +assert.Equal(licenses, []string{"MIT", "Apache-2.0"}) +``` + ## Background This package was developed to support testing whether a repository's license requirements are met by an allowed-list of licenses. diff --git a/spdxexp/extracts.go b/spdxexp/extracts.go new file mode 100644 index 0000000..55afff3 --- /dev/null +++ b/spdxexp/extracts.go @@ -0,0 +1,21 @@ +package spdxexp + +// ExtractLicenses extracts licenses from the given expression without duplicates. +// Returns an array of licenses or error if error occurs during processing. +func ExtractLicenses(expression string) ([]string, error) { + node, err := parse(expression) + if err != nil { + return nil, err + } + + expanded := node.expand(true) + licenses := make([]string, 0) + allLicenses := flatten(expanded) + for _, licenseNode := range allLicenses { + licenses = append(licenses, *licenseNode.reconstructedLicenseString()) + } + + licenses = removeDuplicateStrings(licenses) + + return licenses, nil +} diff --git a/spdxexp/extracts_test.go b/spdxexp/extracts_test.go new file mode 100644 index 0000000..d9e1b67 --- /dev/null +++ b/spdxexp/extracts_test.go @@ -0,0 +1,34 @@ +package spdxexp + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExtractLicenses(t *testing.T) { + tests := []struct { + name string + inputExpression string + extractedLicenses []string + }{ + {"Single license", "MIT", []string{"MIT"}}, + {"AND'ed licenses", "MIT AND Apache-2.0", []string{"MIT", "Apache-2.0"}}, + {"AND'ed & OR'ed licenses", "(MIT AND Apache-2.0) OR GPL-3.0", []string{"GPL-3.0", "MIT", "Apache-2.0"}}, + {"ONLY modifiers", "LGPL-2.1-only OR MIT OR BSD-3-Clause", []string{"MIT", "BSD-3-Clause", "LGPL-2.1-only"}}, + {"WITH modifiers", "GPL-2.0-or-later WITH Bison-exception-2.2", []string{"GPL-2.0-or-later+ WITH Bison-exception-2.2"}}, + {"Invalid SPDX expression", "MIT OR INVALID", nil}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + licenses, err := ExtractLicenses(test.inputExpression) + assert.ElementsMatch(t, test.extractedLicenses, licenses) + if test.extractedLicenses == nil { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/spdxexp/helpers.go b/spdxexp/helpers.go new file mode 100644 index 0000000..8ad73df --- /dev/null +++ b/spdxexp/helpers.go @@ -0,0 +1,24 @@ +package spdxexp + +// flatten will take an array of nested array and return +// all nested elements in an array. e.g. [[1,2,[3]],4] -> [1,2,3,4] +func flatten[T any](lists [][]T) []T { + var res []T + for _, list := range lists { + res = append(res, list...) + } + return res +} + +// removeDuplicateStrings will remove all duplicates from a slice +func removeDuplicateStrings(sliceList []string) []string { + allKeys := make(map[string]bool) + list := []string{} + for _, item := range sliceList { + if _, value := allKeys[item]; !value { + allKeys[item] = true + list = append(list, item) + } + } + return list +} diff --git a/spdxexp/parse_test.go b/spdxexp/parse_test.go index 8ee3d9d..707331d 100644 --- a/spdxexp/parse_test.go +++ b/spdxexp/parse_test.go @@ -919,7 +919,7 @@ func TestParse(t *testing.T) { require.Equal(t, test.err, err) if test.err != nil { // when error, check that returned node is nil - var nilNode *node = nil + var nilNode *node assert.Equal(t, nilNode, startNode, "Expected nil node when error occurs.") return } @@ -1115,7 +1115,7 @@ func TestParseTokens(t *testing.T) { require.Equal(t, test.err, test.tokens.err) if test.err != nil { // when error, check that returned node is nil - var nilNode *node = nil + var nilNode *node assert.Equal(t, nilNode, startNode, "Expected nil node when error occurs.") return } @@ -1257,7 +1257,7 @@ func TestParseWith(t *testing.T) { require.Equal(t, test.err, test.tokens.err) if test.expectNil { // exception license is nil when error occurs or WITH operator is not found - var nilString *string = nil + var nilString *string assert.Equal(t, nilString, exceptionLicense) return } diff --git a/spdxexp/satisfies_test.go b/spdxexp/satisfies_test.go index cc32199..0a240d0 100644 --- a/spdxexp/satisfies_test.go +++ b/spdxexp/satisfies_test.go @@ -20,6 +20,12 @@ func TestValidateLicenses(t *testing.T) { {"Some invalid", []string{"MTI", "Apche-2.0", "GPL-2.0"}, false, []string{"MTI", "Apche-2.0"}}, {"GPL-2.0", []string{"GPL-2.0"}, true, []string{}}, {"GPL-2.0-only", []string{"GPL-2.0-only"}, true, []string{}}, + {"SPDX Expressions are valid", []string{ + "MIT AND APACHE-2.0", + "MIT AND APCHE-2.0", + "LGPL-2.1-only OR MIT OR BSD-3-Clause", + "GPL-2.0-or-later WITH Bison-exception-2.2", + }, false, []string{"MIT AND APCHE-2.0"}}, } for _, test := range tests { diff --git a/spdxexp/scan.go b/spdxexp/scan.go index 0af567c..294dbe5 100644 --- a/spdxexp/scan.go +++ b/spdxexp/scan.go @@ -269,10 +269,8 @@ func (exp *expressionStream) normalizeLicense(license string) *token { return token } } - if token := deprecatedLicenseLookup(license); token != nil { - return token - } - return nil + + return deprecatedLicenseLookup(license) } // Lookup license identifier in active and exception lists to determine if it is a supported SPDX id diff --git a/spdxexp/scan_test.go b/spdxexp/scan_test.go index 266ec62..27471b5 100644 --- a/spdxexp/scan_test.go +++ b/spdxexp/scan_test.go @@ -127,7 +127,7 @@ func TestParseToken(t *testing.T) { require.Equal(t, test.err, test.exp.err) if test.err != nil { // token is nil when error occurs or token is not recognized - var nilToken *token = nil + var nilToken *token assert.Equal(t, nilToken, tokn) return } @@ -298,7 +298,7 @@ func TestReadDocumentRef(t *testing.T) { require.Equal(t, test.err, test.exp.err) if test.err != nil { // ref should be nil when error occurs or a ref is not found - var nilToken *token = nil + var nilToken *token assert.Equal(t, nilToken, ref, "Expected nil token when error occurs.") return } @@ -331,7 +331,7 @@ func TestReadLicenseRef(t *testing.T) { require.Equal(t, test.err, test.exp.err) if test.err != nil { // ref should be nil when error occurs or a ref is not found - var nilToken *token = nil + var nilToken *token assert.Equal(t, nilToken, ref) return } @@ -371,7 +371,7 @@ func TestReadLicense(t *testing.T) { require.Equal(t, test.err, test.exp.err) if test.err != nil { // license should be nil when error occurs or a license is not found - var nilToken *token = nil + var nilToken *token assert.Equal(t, nilToken, license) return }