From df5d42ebe2fb001c160965ea36103bc598b45a7c Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Mon, 18 Sep 2023 13:20:22 -0400 Subject: [PATCH 1/7] feat: add method for extracting licenses from expression --- spdxexp/helpers.go | 24 ++++++++++++++++++++++++ spdxexp/satisfies.go | 19 +++++++++++++++++++ spdxexp/satisfies_test.go | 22 ++++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 spdxexp/helpers.go diff --git a/spdxexp/helpers.go b/spdxexp/helpers.go new file mode 100644 index 0000000..20fad38 --- /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 +} + +// removeDuplicates will remove all duplicates from a slice +func removeDuplicates[T string](sliceList []T) []T { + allKeys := make(map[T]bool) + list := []T{} + for _, item := range sliceList { + if _, value := allKeys[item]; !value { + allKeys[item] = true + list = append(list, item) + } + } + return list +} diff --git a/spdxexp/satisfies.go b/spdxexp/satisfies.go index abd4778..e34fce5 100644 --- a/spdxexp/satisfies.go +++ b/spdxexp/satisfies.go @@ -50,6 +50,25 @@ func Satisfies(testExpression string, allowedList []string) (bool, error) { return false, nil } +// ExtractLicenses extracts licenses from the given expression without duplicates. +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 _, license := range allLicenses { + licenses = append(licenses, license.lic.license) + } + + licenses = removeDuplicates(licenses) + + return licenses, nil +} + // stringsToNodes converts an array of single license strings to to an array of license nodes. func stringsToNodes(licenseStrings []string) ([]*node, error) { nodes := make([]*node, len(licenseStrings)) diff --git a/spdxexp/satisfies_test.go b/spdxexp/satisfies_test.go index 0a240d0..2468e1b 100644 --- a/spdxexp/satisfies_test.go +++ b/spdxexp/satisfies_test.go @@ -37,6 +37,28 @@ func TestValidateLicenses(t *testing.T) { } } +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"}}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + licenses, err := ExtractLicenses(test.inputExpression) + assert.ElementsMatch(t, test.extractedLicenses, licenses) + assert.NoError(t, err) + }) + } +} + // TestSatisfiesSingle lets you quickly test a single call to Satisfies with a specific license expression and allowed list of licenses. // To test a different expression, change the expression, allowed licenses, and expected result in the function body. // TO RUN: go test ./expression -run TestSatisfiesSingle From cb36f68515b092bac5e1ceb2c10fb8ffd4edb1f3 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Mon, 18 Sep 2023 13:28:05 -0400 Subject: [PATCH 2/7] docs: add public method docs --- spdxexp/satisfies.go | 1 + 1 file changed, 1 insertion(+) diff --git a/spdxexp/satisfies.go b/spdxexp/satisfies.go index e34fce5..c61701f 100644 --- a/spdxexp/satisfies.go +++ b/spdxexp/satisfies.go @@ -51,6 +51,7 @@ func Satisfies(testExpression string, allowedList []string) (bool, error) { } // 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 { From 674a5c96bf2c1a6e8ecad3dd26243b90c9265830 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Mon, 18 Sep 2023 16:45:00 -0400 Subject: [PATCH 3/7] fix: Make modifiers present and more to extracts file --- spdxexp/extracts.go | 27 +++++++++++++++++++++++++++ spdxexp/extracts_test.go | 29 +++++++++++++++++++++++++++++ spdxexp/helpers.go | 8 ++++---- spdxexp/satisfies.go | 20 -------------------- spdxexp/satisfies_test.go | 22 ---------------------- 5 files changed, 60 insertions(+), 46 deletions(-) create mode 100644 spdxexp/extracts.go create mode 100644 spdxexp/extracts_test.go diff --git a/spdxexp/extracts.go b/spdxexp/extracts.go new file mode 100644 index 0000000..2d8e59c --- /dev/null +++ b/spdxexp/extracts.go @@ -0,0 +1,27 @@ +package spdxexp + +import "errors" + +// 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 { + if licenseNode == nil { + return nil, errors.New("license node is nil") + } + + 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..02f0d4c --- /dev/null +++ b/spdxexp/extracts_test.go @@ -0,0 +1,29 @@ +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"}}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + licenses, err := ExtractLicenses(test.inputExpression) + assert.ElementsMatch(t, test.extractedLicenses, licenses) + assert.NoError(t, err) + }) + } +} diff --git a/spdxexp/helpers.go b/spdxexp/helpers.go index 20fad38..8ad73df 100644 --- a/spdxexp/helpers.go +++ b/spdxexp/helpers.go @@ -10,10 +10,10 @@ func flatten[T any](lists [][]T) []T { return res } -// removeDuplicates will remove all duplicates from a slice -func removeDuplicates[T string](sliceList []T) []T { - allKeys := make(map[T]bool) - list := []T{} +// 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 diff --git a/spdxexp/satisfies.go b/spdxexp/satisfies.go index c61701f..abd4778 100644 --- a/spdxexp/satisfies.go +++ b/spdxexp/satisfies.go @@ -50,26 +50,6 @@ func Satisfies(testExpression string, allowedList []string) (bool, error) { return false, nil } -// 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 _, license := range allLicenses { - licenses = append(licenses, license.lic.license) - } - - licenses = removeDuplicates(licenses) - - return licenses, nil -} - // stringsToNodes converts an array of single license strings to to an array of license nodes. func stringsToNodes(licenseStrings []string) ([]*node, error) { nodes := make([]*node, len(licenseStrings)) diff --git a/spdxexp/satisfies_test.go b/spdxexp/satisfies_test.go index 2468e1b..0a240d0 100644 --- a/spdxexp/satisfies_test.go +++ b/spdxexp/satisfies_test.go @@ -37,28 +37,6 @@ func TestValidateLicenses(t *testing.T) { } } -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"}}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - licenses, err := ExtractLicenses(test.inputExpression) - assert.ElementsMatch(t, test.extractedLicenses, licenses) - assert.NoError(t, err) - }) - } -} - // TestSatisfiesSingle lets you quickly test a single call to Satisfies with a specific license expression and allowed list of licenses. // To test a different expression, change the expression, allowed licenses, and expected result in the function body. // TO RUN: go test ./expression -run TestSatisfiesSingle From 16353ede453549eb59f7798243b003c6937ae16a Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Tue, 19 Sep 2023 09:41:26 -0400 Subject: [PATCH 4/7] test: update to include error test --- spdxexp/extracts_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spdxexp/extracts_test.go b/spdxexp/extracts_test.go index 02f0d4c..d9e1b67 100644 --- a/spdxexp/extracts_test.go +++ b/spdxexp/extracts_test.go @@ -17,13 +17,18 @@ func TestExtractLicenses(t *testing.T) { {"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) - assert.NoError(t, err) + if test.extractedLicenses == nil { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } }) } } From 7fe1a57f224bfb7bad17dc0783a9e94717048f24 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Tue, 19 Sep 2023 09:42:04 -0400 Subject: [PATCH 5/7] fix: remove nil guard --- spdxexp/extracts.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/spdxexp/extracts.go b/spdxexp/extracts.go index 2d8e59c..55afff3 100644 --- a/spdxexp/extracts.go +++ b/spdxexp/extracts.go @@ -1,7 +1,5 @@ package spdxexp -import "errors" - // 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) { @@ -14,10 +12,6 @@ func ExtractLicenses(expression string) ([]string, error) { licenses := make([]string, 0) allLicenses := flatten(expanded) for _, licenseNode := range allLicenses { - if licenseNode == nil { - return nil, errors.New("license node is nil") - } - licenses = append(licenses, *licenseNode.reconstructedLicenseString()) } From 4f63854de4e185fa77290cab8f026aa32bd15b81 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Tue, 19 Sep 2023 09:45:34 -0400 Subject: [PATCH 6/7] docs: update docs to include extract licenses --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index a10b2b5..258aee7 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,30 @@ 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 a `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 From 0f05021d0fe4c46f1938f44d7bd606ae30a8eedc Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Tue, 19 Sep 2023 10:01:16 -0400 Subject: [PATCH 7/7] docs: update readme with extractlicenses --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 258aee7..093b1a4 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ Function `ExtractLicenses` is used to extract licenses from the given expression Function `ExtractLicenses` has 2 return values. First is `[]string` which contains all of the SPDX licenses without duplicates. -The second return value is a `error` which is not `nil` if the given expression is not a valid SPDX expression. +The second return value is an `error` which is not `nil` if the given expression is not a valid SPDX expression. #### Example