Skip to content

Commit d392f44

Browse files
authored
Add SPDX identifier rewriting (#23)
1 parent a8b6675 commit d392f44

5 files changed

Lines changed: 246 additions & 2 deletions

File tree

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,26 @@ expr, err := spdx.ParseStrict("MIT OR Apache-2.0") // succeeds
4848
expr, err := spdx.ParseStrict("Apache 2 OR MIT") // fails
4949
```
5050

51+
### Rewrite expression identifiers
52+
53+
Parse an expression once, then replace its identifiers while keeping its
54+
operators and precedence:
55+
56+
```go
57+
expr, err := spdx.ParseStrict(
58+
"MIT OR GPL-2.0-only WITH Classpath-exception-2.0",
59+
)
60+
rewritten := spdx.RewriteIdentifiers(expr, func(id string) string {
61+
return strings.ToLower(id)
62+
})
63+
fmt.Println(rewritten)
64+
// "mit OR (gpl-2.0-only WITH classpath-exception-2.0)"
65+
```
66+
67+
The callback receives license identifiers, exception identifiers, and complete
68+
`LicenseRef` or `DocumentRef` values in expression order. Replacement values
69+
are not validated as SPDX identifiers.
70+
5171
### Validate licenses
5272

5373
```go

parse.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ type Expression interface {
1313
String() string
1414
// Licenses returns all license identifiers in the expression.
1515
Licenses() []string
16+
rewriteIdentifiers(func(string) string) Expression
1617
isExpr()
1718
}
1819

1920
// License represents a single SPDX license identifier.
2021
type License struct {
21-
ID string // The canonical license ID
22-
Plus bool // True if followed by +
22+
ID string // The canonical license ID
23+
Plus bool // True if followed by +
2324
Exception string // Exception ID if using WITH
2425
}
2526

rewrite.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package spdx
2+
3+
// RewriteIdentifiers applies rewrite to each license identifier, exception
4+
// identifier, and complete license reference in expression order. Operators,
5+
// modifiers, and operator precedence are preserved. Replacements are inserted
6+
// verbatim and are not validated as SPDX identifiers.
7+
//
8+
// A nil rewrite function returns expression.String().
9+
func RewriteIdentifiers(expression Expression, rewrite func(string) string) string {
10+
if rewrite == nil {
11+
return expression.String()
12+
}
13+
return expression.rewriteIdentifiers(rewrite).String()
14+
}
15+
16+
func (license *License) rewriteIdentifiers(rewrite func(string) string) Expression {
17+
rewritten := &License{
18+
ID: rewrite(license.ID),
19+
Plus: license.Plus,
20+
}
21+
if license.Exception != "" {
22+
rewritten.Exception = rewrite(license.Exception)
23+
}
24+
return rewritten
25+
}
26+
27+
func (reference *LicenseRef) rewriteIdentifiers(rewrite func(string) string) Expression {
28+
return &License{ID: rewrite(reference.String())}
29+
}
30+
31+
func (expression *AndExpression) rewriteIdentifiers(rewrite func(string) string) Expression {
32+
return &AndExpression{
33+
Left: expression.Left.rewriteIdentifiers(rewrite),
34+
Right: expression.Right.rewriteIdentifiers(rewrite),
35+
}
36+
}
37+
38+
func (expression *OrExpression) rewriteIdentifiers(rewrite func(string) string) Expression {
39+
return &OrExpression{
40+
Left: expression.Left.rewriteIdentifiers(rewrite),
41+
Right: expression.Right.rewriteIdentifiers(rewrite),
42+
}
43+
}
44+
45+
func (special *SpecialValue) rewriteIdentifiers(func(string) string) Expression {
46+
return &SpecialValue{Value: special.Value}
47+
}

rewrite_external_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package spdx_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/git-pkgs/spdx"
8+
)
9+
10+
type expressionWrapper struct {
11+
spdx.Expression
12+
}
13+
14+
func TestRewriteIdentifiersEmbeddedExpression(t *testing.T) {
15+
t.Parallel()
16+
17+
expression, err := spdx.ParseStrict("MIT OR Apache-2.0")
18+
if err != nil {
19+
t.Fatal(err)
20+
}
21+
wrapped := expressionWrapper{Expression: expression}
22+
if got := spdx.RewriteIdentifiers(wrapped, strings.ToLower); got != "mit OR apache-2.0" {
23+
t.Errorf("RewriteIdentifiers = %q", got)
24+
}
25+
}

rewrite_test.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package spdx
2+
3+
import (
4+
"slices"
5+
"strings"
6+
"testing"
7+
)
8+
9+
func TestRewriteIdentifiers(t *testing.T) {
10+
t.Parallel()
11+
12+
tests := []struct {
13+
name string
14+
input string
15+
rewrite map[string]string
16+
want string
17+
identifiers []string
18+
}{
19+
{
20+
name: "license",
21+
input: "BSD-3-Clause",
22+
rewrite: map[string]string{"BSD-3-Clause": "bsd-new"},
23+
want: "bsd-new",
24+
identifiers: []string{
25+
"BSD-3-Clause",
26+
},
27+
},
28+
{
29+
name: "compound expression",
30+
input: "MIT OR Apache-2.0 AND BSD-3-Clause",
31+
rewrite: map[string]string{
32+
"MIT": "mit",
33+
"Apache-2.0": "apache-2.0",
34+
"BSD-3-Clause": "bsd-new",
35+
},
36+
want: "mit OR (apache-2.0 AND bsd-new)",
37+
identifiers: []string{
38+
"MIT",
39+
"Apache-2.0",
40+
"BSD-3-Clause",
41+
},
42+
},
43+
{
44+
name: "plus and exception",
45+
input: "GPL-2.0+ OR GPL-2.0-only WITH Classpath-exception-2.0",
46+
rewrite: map[string]string{
47+
"GPL-2.0": "gpl-2.0",
48+
"GPL-2.0-only": "gpl-2.0",
49+
"Classpath-exception-2.0": "classpath-exception-2.0",
50+
},
51+
want: "gpl-2.0+ OR (gpl-2.0 WITH classpath-exception-2.0)",
52+
identifiers: []string{
53+
"GPL-2.0",
54+
"GPL-2.0-only",
55+
"Classpath-exception-2.0",
56+
},
57+
},
58+
{
59+
name: "license references",
60+
input: "LicenseRef-scancode-mit AND DocumentRef-vendor:LicenseRef-custom",
61+
rewrite: map[string]string{
62+
"LicenseRef-scancode-mit": "mit",
63+
"DocumentRef-vendor:LicenseRef-custom": "unknown-spdx",
64+
},
65+
want: "mit AND unknown-spdx",
66+
identifiers: []string{
67+
"LicenseRef-scancode-mit",
68+
"DocumentRef-vendor:LicenseRef-custom",
69+
},
70+
},
71+
{
72+
name: "repeated identifier",
73+
input: "MIT AND MIT",
74+
rewrite: map[string]string{
75+
"MIT": "mit",
76+
},
77+
want: "mit AND mit",
78+
identifiers: []string{"MIT", "MIT"},
79+
},
80+
}
81+
82+
for _, test := range tests {
83+
t.Run(test.name, func(t *testing.T) {
84+
t.Parallel()
85+
86+
expression, err := ParseStrict(test.input)
87+
if err != nil {
88+
t.Fatal(err)
89+
}
90+
original := expression.String()
91+
var identifiers []string
92+
got := RewriteIdentifiers(expression, func(identifier string) string {
93+
identifiers = append(identifiers, identifier)
94+
return test.rewrite[identifier]
95+
})
96+
if got != test.want {
97+
t.Errorf("RewriteIdentifiers(%q) = %q, want %q", test.input, got, test.want)
98+
}
99+
if !slices.Equal(identifiers, test.identifiers) {
100+
t.Errorf("identifiers = %q, want %q", identifiers, test.identifiers)
101+
}
102+
if expression.String() != original {
103+
t.Errorf("RewriteIdentifiers mutated expression: got %q, want %q", expression, original)
104+
}
105+
})
106+
}
107+
}
108+
109+
func TestRewriteIdentifiersSpecialValues(t *testing.T) {
110+
t.Parallel()
111+
112+
for _, input := range []string{"NONE", "NOASSERTION"} {
113+
expression, err := ParseStrict(input)
114+
if err != nil {
115+
t.Fatal(err)
116+
}
117+
got := RewriteIdentifiers(expression, func(identifier string) string {
118+
t.Fatalf("rewrite called with %q", identifier)
119+
return ""
120+
})
121+
if got != input {
122+
t.Errorf("RewriteIdentifiers(%q) = %q", input, got)
123+
}
124+
}
125+
}
126+
127+
func TestRewriteIdentifiersNilFunction(t *testing.T) {
128+
t.Parallel()
129+
130+
expression, err := ParseStrict("MIT OR Apache-2.0")
131+
if err != nil {
132+
t.Fatal(err)
133+
}
134+
if got := RewriteIdentifiers(expression, nil); got != expression.String() {
135+
t.Errorf("RewriteIdentifiers with nil function = %q", got)
136+
}
137+
}
138+
139+
func BenchmarkRewriteIdentifiers(b *testing.B) {
140+
expression, err := ParseStrict(
141+
"MIT OR GPL-2.0-only WITH Classpath-exception-2.0",
142+
)
143+
if err != nil {
144+
b.Fatal(err)
145+
}
146+
147+
b.ReportAllocs()
148+
for b.Loop() {
149+
RewriteIdentifiers(expression, strings.ToLower)
150+
}
151+
}

0 commit comments

Comments
 (0)