fix: use canonical SPDX license IDs from embeded licenses.json (#10053)

This commit is contained in:
DmitriyLewen
2026-01-19 15:31:30 +06:00
committed by GitHub
parent 5bb654074e
commit c233735b02
9 changed files with 131 additions and 148 deletions

View File

@@ -264,6 +264,7 @@ license:
- Artistic-1.0
- Artistic-2.0
- BSL-1.0
- BSD-1-Clause
- BSD-2-Clause-FreeBSD
- BSD-2-Clause-NetBSD
- BSD-2-Clause

View File

@@ -11,7 +11,6 @@ var StandardizeKeyAndSuffix = standardizeKeyAndSuffix
var NormalizeLicense = normalizeLicense
// Mapping exports mapping for testing.
func Mapping() map[string]expression.SimpleExpr {
return mapping
}

View File

@@ -37,7 +37,7 @@ const (
Artistic10 = "Artistic-1.0"
Artistic20 = "Artistic-2.0"
BCL = "BCL"
Beerware = "Beerware"
BSD1Clause = "BSD-1-Clause"
BSD2ClauseFreeBSD = "BSD-2-Clause-FreeBSD"
BSD2ClauseNetBSD = "BSD-2-Clause-NetBSD"
BSD2Clause = "BSD-2-Clause"
@@ -85,7 +85,6 @@ const (
CommonsClause = "Commons-Clause"
CPAL10 = "CPAL-1.0"
CPL10 = "CPL-1.0"
EGenix = "eGenix"
EPL10 = "EPL-1.0"
EPL20 = "EPL-2.0"
EUPL10 = "EUPL-1.0"
@@ -121,13 +120,11 @@ const (
LGPL20 = "LGPL-2.0"
LGPL21 = "LGPL-2.1"
LGPL30 = "LGPL-3.0"
LGPLLR = "LGPLLR"
Libpng = "Libpng"
Lil10 = "Lil-1.0"
LinuxOpenIB = "Linux-OpenIB"
LPL102 = "LPL-1.02"
LPL10 = "LPL-1.0"
LPPL13c = "LPPL-1.3c"
MIT = "MIT"
MPL10 = "MPL-1.0"
MPL11 = "MPL-1.1"
@@ -155,7 +152,6 @@ const (
SGIB10 = "SGI-B-1.0"
SGIB11 = "SGI-B-1.1"
SGIB20 = "SGI-B-2.0"
SISSL12 = "SISSL-1.2"
SISSL = "SISSL"
Sleepycat = "Sleepycat"
UnicodeTOU = "Unicode-TOU"
@@ -306,6 +302,7 @@ var (
Artistic10,
Artistic20,
BSL10,
BSD1Clause,
BSD2ClauseFreeBSD,
BSD2ClauseNetBSD,
BSD2Clause,
@@ -420,6 +417,13 @@ func ValidateSPDXLicense(license string) bool {
return spdxLicenses.Contains(license)
}
// SPDXLicenseID returns the canonical (properly cased) SPDX license ID.
// Returns empty string and false if the license is not in the SPDX list
func SPDXLicenseID(license string) (string, bool) {
initSpdxLicenses()
return spdxLicenses.Find(license)
}
// ValidateSPDXException returns true if SPDX exception list contain exceptionID
func ValidateSPDXException(exception string) bool {
initSpdxExceptions()

View File

@@ -46,30 +46,14 @@ var mapping = map[string]expr.SimpleExpr{
"ZOPE": licence(expr.ZPL21, false),
// Non-ambiguous simple mappings
"0BSD": licence(expr.ZeroBSD, false),
"AFL-1.1": licence(expr.AFL11, false),
"AFL-1.2": licence(expr.AFL12, false),
"AFL-2": licence(expr.AFL20, false),
"AFL-2.0": licence(expr.AFL20, false),
"AFL-2.1": licence(expr.AFL21, false),
"AFL-3.0": licence(expr.AFL30, false),
"AGPL-1.0": licence(expr.AGPL10, false),
"AGPL-3.0": licence(expr.AGPL30, false),
"APACHE-1": licence(expr.Apache10, false),
"APACHE-1.0": licence(expr.Apache10, false),
"APACHE-1.1": licence(expr.Apache11, false),
"APACHE-2": licence(expr.Apache20, false),
"APACHE-2.0": licence(expr.Apache20, false),
"APL-2": licence(expr.Apache20, false),
"APL-2.0": licence(expr.Apache20, false),
"APSL-1.0": licence(expr.APSL10, false),
"APSL-1.1": licence(expr.APSL11, false),
"APSL-1.2": licence(expr.APSL12, false),
"APSL-2.0": licence(expr.APSL20, false),
"ARTISTIC-1.0": licence(expr.Artistic10, false),
"ARTISTIC-1.0-CL-8": licence(expr.Artistic10cl8, false),
"ARTISTIC-1.0-PERL": licence(expr.Artistic10Perl, false),
"ARTISTIC-2.0": licence(expr.Artistic20, false),
"ASF-1": licence(expr.Apache10, false),
"ASF-1.0": licence(expr.Apache10, false),
"ASF-1.1": licence(expr.Apache11, false),
@@ -81,79 +65,28 @@ var mapping = map[string]expr.SimpleExpr{
"ASL-2": licence(expr.Apache20, false),
"ASL-2.0": licence(expr.Apache20, false),
"BCL": licence(expr.BCL, false),
"BEERWARE": licence(expr.Beerware, false),
"BOOST": licence(expr.BSL10, false),
"BOOST-1.0": licence(expr.BSL10, false),
"BOUNCY": licence(expr.MIT, false),
"BSD-1": licence(expr.BSD1Clause, false),
"BSD-2": licence(expr.BSD2Clause, false),
"BSD-2-CLAUSE": licence(expr.BSD2Clause, false),
"BSD-2-CLAUSE-FREEBSD": licence(expr.BSD2ClauseFreeBSD, false),
"BSD-2-CLAUSE-NETBSD": licence(expr.BSD2ClauseNetBSD, false),
"BSD-3": licence(expr.BSD3Clause, false),
"BSD-3-CLAUSE": licence(expr.BSD3Clause, false),
"BSD-3-CLAUSE-ATTRIBUTION": licence(expr.BSD3ClauseAttribution, false),
"BSD-3-CLAUSE-CLEAR": licence(expr.BSD3ClauseClear, false),
"BSD-3-CLAUSE-LBNL": licence(expr.BSD3ClauseLBNL, false),
"BSD-4": licence(expr.BSD4Clause, false),
"BSD-4-CLAUSE": licence(expr.BSD4Clause, false),
"BSD-4-CLAUSE-UC": licence(expr.BSD4ClauseUC, false),
"BSD-PROTECTION": licence(expr.BSDProtection, false),
"BSL": licence(expr.BSL10, false),
"BSL-1.0": licence(expr.BSL10, false),
"CC-BY-1.0": licence(expr.CCBY10, false),
"CC-BY-2.0": licence(expr.CCBY20, false),
"CC-BY-2.5": licence(expr.CCBY25, false),
"CC-BY-3.0": licence(expr.CCBY30, false),
"CC-BY-4.0": licence(expr.CCBY40, false),
"CC-BY-NC-1.0": licence(expr.CCBYNC10, false),
"CC-BY-NC-2.0": licence(expr.CCBYNC20, false),
"CC-BY-NC-2.5": licence(expr.CCBYNC25, false),
"CC-BY-NC-3.0": licence(expr.CCBYNC30, false),
"CC-BY-NC-4.0": licence(expr.CCBYNC40, false),
"CC-BY-NC-ND-1.0": licence(expr.CCBYNCND10, false),
"CC-BY-NC-ND-2.0": licence(expr.CCBYNCND20, false),
"CC-BY-NC-ND-2.5": licence(expr.CCBYNCND25, false),
"CC-BY-NC-ND-3.0": licence(expr.CCBYNCND30, false),
"CC-BY-NC-ND-4.0": licence(expr.CCBYNCND40, false),
"CC-BY-NC-SA-1.0": licence(expr.CCBYNCSA10, false),
"CC-BY-NC-SA-2.0": licence(expr.CCBYNCSA20, false),
"CC-BY-NC-SA-2.5": licence(expr.CCBYNCSA25, false),
"CC-BY-NC-SA-3.0": licence(expr.CCBYNCSA30, false),
"CC-BY-NC-SA-4.0": licence(expr.CCBYNCSA40, false),
"CC-BY-ND-1.0": licence(expr.CCBYND10, false),
"CC-BY-ND-2.0": licence(expr.CCBYND20, false),
"CC-BY-ND-2.5": licence(expr.CCBYND25, false),
"CC-BY-ND-3.0": licence(expr.CCBYND30, false),
"CC-BY-ND-4.0": licence(expr.CCBYND40, false),
"CC-BY-SA-1.0": licence(expr.CCBYSA10, false),
"CC-BY-SA-2.0": licence(expr.CCBYSA20, false),
"CC-BY-SA-2.5": licence(expr.CCBYSA25, false),
"CC-BY-SA-3.0": licence(expr.CCBYSA30, false),
"CC-BY-SA-4.0": licence(expr.CCBYSA40, false),
"CC0": licence(expr.CC010, false),
"CC0-1.0": licence(expr.CC010, false),
"CDDL-1": licence(expr.CDDL10, false),
"CDDL-1.0": licence(expr.CDDL10, false),
"CDDL-1.1": licence(expr.CDDL11, false),
"COMMONS-CLAUSE": licence(expr.CommonsClause, false),
"CPAL": licence(expr.CPAL10, false),
"CPAL-1.0": licence(expr.CPAL10, false),
"CPL": licence(expr.CPL10, false),
"CPL-1.0": licence(expr.CPL10, false),
"ECLIPSE-1.0": licence(expr.EPL10, false),
"ECLIPSE-2.0": licence(expr.EPL20, false),
"EDL-1.0": licence(expr.BSD3Clause, false),
"EGENIX": licence(expr.EGenix, false),
"EPL-1.0": licence(expr.EPL10, false),
"EPL-2.0": licence(expr.EPL20, false),
"EUPL-1.0": licence(expr.EUPL10, false),
"EUPL-1.1": licence(expr.EUPL11, false),
"EXPAT": licence(expr.MIT, false),
"FACEBOOK-2-CLAUSE": licence(expr.Facebook2Clause, false),
"FACEBOOK-3-CLAUSE": licence(expr.Facebook3Clause, false),
"FACEBOOK-EXAMPLES": licence(expr.FacebookExamples, false),
"FREEIMAGE": licence(expr.FreeImage, false),
"FTL": licence(expr.FTL, false),
"GFDL-1.1": licence(expr.GFDL11, false),
"GFDL-1.1-INVARIANTS": licence(expr.GFDL11WithInvariants, false),
"GFDL-1.1-NO-INVARIANTS": licence(expr.GFDL11NoInvariants, false),
@@ -185,9 +118,6 @@ var mapping = map[string]expr.SimpleExpr{
"GPLV2+CE": licence(expr.GPL20withclasspathexception, true),
"GUST-FONT": licence(expr.GUSTFont, false),
"HSQLDB": licence(expr.BSD3Clause, false),
"IMAGEMAGICK": licence(expr.ImageMagick, false),
"IPL-1.0": licence(expr.IPL10, false),
"ISC": licence(expr.ISC, false),
"ISCL": licence(expr.ISC, false),
"JQUERY": licence(expr.MIT, false),
"LGPL-2": licence(expr.LGPL20, false),
@@ -195,78 +125,28 @@ var mapping = map[string]expr.SimpleExpr{
"LGPL-2.1": licence(expr.LGPL21, false),
"LGPL-3": licence(expr.LGPL30, false),
"LGPL-3.0": licence(expr.LGPL30, false),
"LGPLLR": licence(expr.LGPLLR, false),
"LIBPNG": licence(expr.Libpng, false),
"LIL-1.0": licence(expr.Lil10, false),
"LINUX-OPENIB": licence(expr.LinuxOpenIB, false),
"LPL-1.0": licence(expr.LPL10, false),
"LPL-1.02": licence(expr.LPL102, false),
"LPPL-1.3C": licence(expr.LPPL13c, false),
"MIT": licence(expr.MIT, false),
// MIT No Attribution (MIT-0) is not yet supported by google/licenseclassifier
"MIT-0": licence(expr.MIT, false),
"MIT-LIKE": licence(expr.MIT, false),
"MIT-STYLE": licence(expr.MIT, false),
"MPL-1": licence(expr.MPL10, false),
"MPL-1.0": licence(expr.MPL10, false),
"MPL-1.1": licence(expr.MPL11, false),
"MPL-2": licence(expr.MPL20, false),
"MPL-2.0": licence(expr.MPL20, false),
"MS-PL": licence(expr.MSPL, false),
"NCSA": licence(expr.NCSA, false),
"NPL-1.0": licence(expr.NPL10, false),
"NPL-1.1": licence(expr.NPL11, false),
"OFL-1.1": licence(expr.OFL11, false),
"OPENSSL": licence(expr.OpenSSL, false),
"OPENVISION": licence(expr.OpenVision, false),
"OSL-1": licence(expr.OSL10, false),
"OSL-1.0": licence(expr.OSL10, false),
"OSL-1.1": licence(expr.OSL11, false),
"OSL-2": licence(expr.OSL20, false),
"OSL-2.0": licence(expr.OSL20, false),
"OSL-2.1": licence(expr.OSL21, false),
"OSL-3": licence(expr.OSL30, false),
"OSL-3.0": licence(expr.OSL30, false),
"PHP-3.0": licence(expr.PHP30, false),
"PHP-3.01": licence(expr.PHP301, false),
"PIL": licence(expr.PIL, false),
"POSTGRESQL": licence(expr.PostgreSQL, false),
"PYTHON-2": licence(expr.Python20, false),
"PYTHON-2.0": licence(expr.Python20, false),
"PYTHON-2.0-COMPLETE": licence(expr.Python20complete, false),
"QPL-1": licence(expr.QPL10, false),
"QPL-1.0": licence(expr.QPL10, false),
"RUBY": licence(expr.Ruby, false),
"SGI-B-1.0": licence(expr.SGIB10, false),
"SGI-B-1.1": licence(expr.SGIB11, false),
"SGI-B-2.0": licence(expr.SGIB20, false),
"SISSL": licence(expr.SISSL, false),
"SISSL-1.2": licence(expr.SISSL12, false),
"SLEEPYCAT": licence(expr.Sleepycat, false),
"UNICODE-DFS-2015": licence(expr.UnicodeDFS2015, false),
"UNICODE-DFS-2016": licence(expr.UnicodeDFS2016, false),
"UNICODE-TOU": licence(expr.UnicodeTOU, false),
"UNLICENSE": licence(expr.Unlicense, false),
"UPL-1": licence(expr.UPL10, false),
"UPL-1.0": licence(expr.UPL10, false),
"W3C": licence(expr.W3C, false),
"W3C-19980720": licence(expr.W3C19980720, false),
"W3C-20150513": licence(expr.W3C20150513, false),
"W3CL": licence(expr.W3C, false),
"WTF": licence(expr.WTFPL, false),
"WTFPL": licence(expr.WTFPL, false),
"X11": licence(expr.X11, false),
"XNET": licence(expr.Xnet, false),
"ZEND-2": licence(expr.Zend20, false),
"ZEND-2.0": licence(expr.Zend20, false),
"ZLIB": licence(expr.Zlib, false),
"ZLIB-ACKNOWLEDGEMENT": licence(expr.ZlibAcknowledgement, false),
"ZOPE-1.1": licence(expr.ZPL11, false),
"ZOPE-2.0": licence(expr.ZPL20, false),
"ZOPE-2.1": licence(expr.ZPL21, false),
"ZPL-1.1": licence(expr.ZPL11, false),
"ZPL-2.0": licence(expr.ZPL20, false),
"ZPL-2.1": licence(expr.ZPL21, false),
"MIT-LIKE": licence(expr.MIT, false),
"MIT-STYLE": licence(expr.MIT, false),
"MPL-1": licence(expr.MPL10, false),
"MPL-2": licence(expr.MPL20, false),
"OFL-1.1": licence(expr.OFL11, false),
"OPENSSL": licence(expr.OpenSSL, false),
"OPENVISION": licence(expr.OpenVision, false),
"OSL-1": licence(expr.OSL10, false),
"OSL-2": licence(expr.OSL20, false),
"OSL-3": licence(expr.OSL30, false),
"PIL": licence(expr.PIL, false),
"PYTHON-2": licence(expr.Python20, false),
"PYTHON-2.0-COMPLETE": licence(expr.Python20complete, false),
"QPL-1": licence(expr.QPL10, false),
"UPL-1": licence(expr.UPL10, false),
"W3CL": licence(expr.W3C, false),
"WTF": licence(expr.WTFPL, false),
"ZEND-2": licence(expr.Zend20, false),
"ZOPE-1.1": licence(expr.ZPL11, false),
"ZOPE-2.0": licence(expr.ZPL20, false),
"ZOPE-2.1": licence(expr.ZPL21, false),
// Non simple declared mappings
// modified from https://github.com/oss-review-toolkit/ort/blob/fc5389c2cfd9c8b009794c8a11f5c91321b7a730/utils/spdx/src/main/resources/declared-license-mapping.yml
@@ -707,6 +587,12 @@ func normalizeSimpleExpr(e expr.SimpleExpr) expr.Expression {
if found, ok := mapping[normalized.License]; ok {
return expr.SimpleExpr{License: found.License, HasPlus: e.HasPlus || found.HasPlus || normalized.HasPlus}
}
// If not found in mapping, try to get the canonical SPDX license ID
if canonical, ok := expr.SPDXLicenseID(normalized.License); ok {
return expr.SimpleExpr{License: canonical, HasPlus: e.HasPlus || normalized.HasPlus}
}
return expr.SimpleExpr{License: name, HasPlus: e.HasPlus}
}

View File

@@ -384,6 +384,16 @@ func TestNormalize(t *testing.T) {
want: "GPL-2.0-with-classpath-exception",
wantLicense: expression.SimpleExpr{License: "GPL-2.0-with-classpath-exception"},
},
// Test canonical SPDX license casing
{
licenses: []expression.Expression{
expression.SimpleExpr{License: "Tcl"},
expression.SimpleExpr{License: "tcl"},
expression.SimpleExpr{License: "TCL"},
},
want: "TCL",
wantLicense: expression.SimpleExpr{License: "TCL"},
},
}
for _, tt := range tests {
t.Run(tt.want, func(t *testing.T) {

View File

@@ -47,6 +47,12 @@ func (s caseInsensitiveStringSet) Contains(item string) bool {
return exists
}
// Find returns the stored value with its original casing
func (s caseInsensitiveStringSet) Find(item string) (string, bool) {
value, exists := s[strings.ToLower(item)]
return value, exists
}
// Size returns the number of items in the set
func (s caseInsensitiveStringSet) Size() int {
return len(s)

View File

@@ -431,3 +431,70 @@ func TestCaseInsensitiveSet_Difference(t *testing.T) {
})
}
}
func TestCaseInsensitiveSet_Find(t *testing.T) {
tests := []struct {
name string
set set.Set[string]
lookup string
wantValue string
wantFound bool
}{
{
name: "exact match",
set: set.NewCaseInsensitive("Hello", "World"),
lookup: "Hello",
wantValue: "Hello",
wantFound: true,
},
{
name: "lowercase lookup",
set: set.NewCaseInsensitive("Hello", "World"),
lookup: "hello",
wantValue: "Hello",
wantFound: true,
},
{
name: "uppercase lookup",
set: set.NewCaseInsensitive("Hello", "World"),
lookup: "HELLO",
wantValue: "Hello",
wantFound: true,
},
{
name: "mixed case lookup",
set: set.NewCaseInsensitive("Hello", "World"),
lookup: "hElLo",
wantValue: "Hello",
wantFound: true,
},
{
name: "not found",
set: set.NewCaseInsensitive("Hello", "World"),
lookup: "Foo",
wantValue: "",
wantFound: false,
},
{
name: "preserves first casing",
set: set.NewCaseInsensitive("TCL", "Tcl"),
lookup: "tcl",
wantValue: "TCL",
wantFound: true,
},
{
name: "empty set",
set: set.NewCaseInsensitive(),
lookup: "test",
wantValue: "",
wantFound: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotValue, gotFound := tt.set.Find(tt.lookup)
assert.Equal(t, tt.wantValue, gotValue)
assert.Equal(t, tt.wantFound, gotFound)
})
}
}

View File

@@ -13,6 +13,10 @@ type Set[T comparable] interface {
// Contains checks if an item exists in the set
Contains(item T) bool
// Find returns the stored value for the given item if it exists
// For case-insensitive sets, this returns the canonical (original) casing
Find(item T) (T, bool)
// Size returns the number of items in the set
Size() int

View File

@@ -38,6 +38,12 @@ func (s unsafeSet[T]) Contains(item T) bool {
return exists
}
// Find returns the stored value for the given item if it exists
func (s unsafeSet[T]) Find(item T) (T, bool) {
_, exists := s[item]
return item, exists
}
// Size returns the number of items in the set
func (s unsafeSet[T]) Size() int {
return len(s)