Skip to content

Commit dbbda01

Browse files
authored
Merge pull request #149 from github/elr/normalize
add function ValidateAndNormalizeLicensesWithOptions
2 parents 0315d0b + 74a38f6 commit dbbda01

4 files changed

Lines changed: 303 additions & 16 deletions

File tree

spdxexp/node.go

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package spdxexp
22

33
import (
4+
"fmt"
45
"sort"
56
"strings"
67
)
@@ -144,10 +145,12 @@ func (n *node) hasDocumentRef() bool {
144145
return n.ref.hasDocumentRef
145146
}
146147

147-
// reconstructedLicenseString returns the string representation of the license or license ref.
148+
// reconstructedLicenseString returns the string representation of a license, license ref, or expression.
148149
// TODO: Original had "NOASSERTION". Does that still apply?
149150
func (n *node) reconstructedLicenseString() *string {
150151
switch n.role {
152+
case expressionNode:
153+
return n.reconstructedExpressionString()
151154
case licenseNode:
152155
license := *n.license()
153156
if n.hasPlus() && !strings.HasSuffix(strings.ToLower(license), "-or-later") {
@@ -167,6 +170,69 @@ func (n *node) reconstructedLicenseString() *string {
167170
return nil
168171
}
169172

173+
func (n *node) reconstructedExpressionString() *string {
174+
if n == nil || !n.isExpression() {
175+
return nil
176+
}
177+
178+
left := n.left()
179+
right := n.right()
180+
if left == nil || right == nil {
181+
return nil
182+
}
183+
184+
leftStr := left.reconstructedLicenseString()
185+
rightStr := right.reconstructedLicenseString()
186+
if leftStr == nil || rightStr == nil {
187+
return nil
188+
}
189+
190+
conj := n.conjunction()
191+
if conj == nil {
192+
return nil
193+
}
194+
195+
operator := strings.ToUpper(*conj)
196+
if operator != "AND" && operator != "OR" {
197+
return nil
198+
}
199+
200+
parentPrec := nodePrecedence(n)
201+
leftRendered := *leftStr
202+
if left.isExpression() && nodePrecedence(left) < parentPrec {
203+
leftRendered = "(" + leftRendered + ")"
204+
}
205+
rightRendered := *rightStr
206+
if right.isExpression() && nodePrecedence(right) < parentPrec {
207+
rightRendered = "(" + rightRendered + ")"
208+
}
209+
210+
s := fmt.Sprintf("%s %s %s", leftRendered, operator, rightRendered)
211+
return &s
212+
}
213+
214+
func nodePrecedence(n *node) int {
215+
if n == nil {
216+
return 0
217+
}
218+
if !n.isExpression() {
219+
// atomic (license/licenseRef)
220+
return 3
221+
}
222+
conj := n.conjunction()
223+
if conj == nil {
224+
return 0
225+
}
226+
switch strings.ToLower(*conj) {
227+
case "and":
228+
return 2
229+
case "or":
230+
return 1
231+
default:
232+
return 0
233+
}
234+
}
235+
170236
// sortLicenses sorts an array of license and license reference nodes alphabetically based
171237
// on their reconstructedLicenseString() representation. The sort function does not expect
172238
// expression nodes, but if one is in the nodes list, it will sort to the end.

spdxexp/node_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ func TestReconstructedLicenseString(t *testing.T) {
4545
licenseRef: "MIT-Style-2",
4646
},
4747
}, "DocumentRef-spdx-tool-1.2:LicenseRef-MIT-Style-2"},
48+
{"Expression node - AND", getParsedNode("MIT AND Apache-2.0"), "MIT AND Apache-2.0"},
49+
{"Expression node - parentheses required (OR under AND)", getParsedNode("(MIT OR Apache-2.0) AND BSD-3-Clause"), "(MIT OR Apache-2.0) AND BSD-3-Clause"},
50+
{"Expression node - parentheses required (OR on right)", getParsedNode("MIT AND (Apache-2.0 OR BSD-3-Clause)"), "MIT AND (Apache-2.0 OR BSD-3-Clause)"},
51+
{"Expression node - precedence (AND under OR)", getParsedNode("MIT OR Apache-2.0 AND BSD-3-Clause"), "MIT OR Apache-2.0 AND BSD-3-Clause"},
4852
}
4953

5054
for _, test := range tests {

spdxexp/satisfies.go

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,47 +34,66 @@ type ValidateLicensesOptions struct {
3434
// Returns all the invalid licenses contained in the `licenses` argument.
3535
func ValidateLicensesWithOptions(licenses []string, options ValidateLicensesOptions) (bool, []string) {
3636
// handle all other cases with parsing, which will cover both single and multiple licenses and expressions
37-
valid := true
38-
invalidLicenses := []string{}
37+
_, invalidLicenses := ValidateAndNormalizeLicensesWithOptions(licenses, options)
38+
return len(invalidLicenses) == 0, invalidLicenses
39+
}
40+
41+
// ValidateAndNormalizeLicensesWithOptions checks if given licenses are valid according to SPDX.
42+
// Supports validation options as defined in ValidateLicensesOptions.
43+
// Returns all validated licenses in their normalized form as the first return value.
44+
// Returns any invalid licenses as the second return value.
45+
func ValidateAndNormalizeLicensesWithOptions(licenses []string, options ValidateLicensesOptions) (normalizedLicenses, invalidLicenses []string) {
46+
normalizedLicenses = []string{}
47+
invalidLicenses = []string{}
48+
seenNormalized := make(map[string]struct{}, len(licenses))
49+
50+
addNormalized := func(license string) {
51+
if _, ok := seenNormalized[license]; ok {
52+
return
53+
}
54+
seenNormalized[license] = struct{}{}
55+
normalizedLicenses = append(normalizedLicenses, license)
56+
}
57+
3958
for _, license := range licenses {
4059
// MIT is the most common license, so check for it first before doing any processing to optimize for this case.
4160
// By putting the isMIT check here, we can avoid the overhead of parsing for the most common case of MIT.
4261
// Having it before trimming means that licenses with leading/trailing whitespace will not be validated
4362
// as MIT by isMIT, but will still be correctly identified using activeLicense. As this is uncommon, it
4463
// is an acceptable tradeoff to avoid the overhead of trimming for the more common case.
4564
if isMIT(license) {
65+
addNormalized("MIT")
4666
continue
4767
}
4868

4969
license = strings.TrimSpace(license)
5070

5171
isAtomic := isAtomicLicense(license)
5272
if isAtomic {
53-
if ok, _ := activeLicense(license); ok {
73+
if ok, normalizedLicense := activeLicense(license); ok {
74+
addNormalized(normalizedLicense)
5475
continue
5576
}
5677

57-
if ok, _ := deprecatedLicense(license); ok {
78+
if ok, normalizedLicense := deprecatedLicense(license); ok {
5879
if options.FailDeprecatedLicenses {
59-
valid = false
6080
invalidLicenses = append(invalidLicenses, license)
6181
continue
6282
}
83+
addNormalized(normalizedLicense)
6384
// if FailDeprecatedLicenses is false, then consider the deprecated license valid and continue
6485
continue
6586
}
6687

6788
if options.FailAllLicenseRefs {
6889
if strings.HasPrefix(license, "LicenseRef-") {
69-
valid = false
7090
invalidLicenses = append(invalidLicenses, license)
7191
continue
7292
}
7393
}
7494

7595
if options.FailAllDocumentRefs {
7696
if strings.HasPrefix(license, "DocumentRef-") {
77-
valid = false
7897
invalidLicenses = append(invalidLicenses, license)
7998
continue
8099
}
@@ -86,17 +105,18 @@ func ValidateLicensesWithOptions(licenses []string, options ValidateLicensesOpti
86105
if !isAtomic {
87106
if hasException, licensePart, exceptionPart := isLicenseWithException(license); hasException {
88107
// matches pattern "licensePart WITH exceptionPart", so validate both parts separately
89-
if ok, _ := exceptionLicense(exceptionPart); ok {
90-
if ok, _ := activeLicense(licensePart); ok {
108+
if ok, normalizedException := exceptionLicense(exceptionPart); ok {
109+
if ok, normalizedLicense := activeLicense(licensePart); ok {
110+
addNormalized(normalizedLicense + " WITH " + normalizedException)
91111
continue
92112
}
93113
if !options.FailDeprecatedLicenses {
94-
if ok, _ := deprecatedLicense(licensePart); ok {
114+
if ok, normalizedLicense := deprecatedLicense(licensePart); ok {
115+
addNormalized(normalizedLicense + " WITH " + normalizedException)
95116
continue
96117
}
97118
}
98119
}
99-
valid = false
100120
invalidLicenses = append(invalidLicenses, license)
101121
continue
102122
}
@@ -105,19 +125,22 @@ func ValidateLicensesWithOptions(licenses []string, options ValidateLicensesOpti
105125
// all other non-atomic expressions are complex expressions with conjunctions (e.g. "MIT AND Apache-2.0"),
106126
// so fail if complex expressions are not allowed
107127
if options.FailComplexExpressions && !isAtomic {
108-
valid = false
109128
invalidLicenses = append(invalidLicenses, license)
110129
continue
111130
}
112131

113132
// need to parse if allowing any of LicenseRef, DocumentRef, or complex expressions to be able to determine
114133
// whether the license expression is valid
115-
if _, err := parse(license); err != nil {
116-
valid = false
134+
var parsedLicense *node
135+
var err error
136+
if parsedLicense, err = parse(license); err != nil {
117137
invalidLicenses = append(invalidLicenses, license)
138+
} else {
139+
normalizedLicense := *parsedLicense.reconstructedLicenseString()
140+
addNormalized(normalizedLicense)
118141
}
119142
}
120-
return valid, invalidLicenses
143+
return normalizedLicenses, invalidLicenses
121144
}
122145

123146
// Satisfies determines if the allowed list of licenses satisfies the test license expression.

0 commit comments

Comments
 (0)